diff options
Diffstat (limited to 'app')
763 files changed, 10885 insertions, 6279 deletions
diff --git a/app/assets/images/icons.json b/app/assets/images/icons.json deleted file mode 100644 index 19843d24e22..00000000000 --- a/app/assets/images/icons.json +++ /dev/null @@ -1 +0,0 @@ -{"iconCount":191,"spriteSize":86607,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-down","arrow-right","assignee","bold","book","bookmark","branch","bullhorn","calendar","cancel","chart","chevron-down","chevron-left","chevron-right","chevron-up","clock","close","code","collapse","comment-dots","comment-next","comment","comments","commit","credit-card","cut","dashboard","disk","doc_code","doc_image","doc_text","double-headed-arrow","download","duplicate","earth","ellipsis_v","emoji_slightly_smiling_face","emoji_smile","emoji_smiley","epic","external-link","eye-slash","eye","file-addition","file-deletion","file-modified","filter","folder-o","folder-open","folder","fork","geo-nodes","git-merge","group","history","home","hook","hourglass","image-comment-dark","image-comment-light","import","issue-block","issue-child","issue-close","issue-duplicate","issue-external","issue-new","issue-open-m","issue-open","issue-parent","issues","italic","key-2","key","label","labels","leave","level-up","license","link","list-bulleted","list-numbered","location-dot","location","lock-open","lock","log","mail","menu","merge-request-close","messages","mobile-issue-close","monitor","more","notifications-off","notifications","overview","pencil-square","pencil","pipeline","play","plus-square-o","plus-square","plus","podcast","preferences","profile","project","push-rules","question-o","question","quote","redo","remove","repeat","retry","scale","screen-full","screen-normal","scroll_down","scroll_up","search","settings","shield","slight-frown","slight-smile","smile","smiley","snippet","soft-unwrap","soft-wrap","spam","spinner","staged","star-o","star","status_canceled_borderless","status_canceled","status_closed","status_created_borderless","status_created","status_failed_borderless","status_failed","status_manual_borderless","status_manual","status_notfound_borderless","status_notfound","status_open","status_pending_borderless","status_pending","status_running_borderless","status_running","status_skipped_borderless","status_skipped","status_success_borderless","status_success_solid","status_success","status_warning_borderless","status_warning","stop","task-done","template","terminal","thumb-down","thumb-up","thumbtack","timer","todo-add","todo-done","token","unapproval","unassignee","unlink","unstaged","user","users","volume-up","warning","work"]}
\ No newline at end of file diff --git a/app/assets/images/icons.svg b/app/assets/images/icons.svg deleted file mode 100644 index 6aec54d0543..00000000000 --- a/app/assets/images/icons.svg +++ /dev/null @@ -1 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><symbol viewBox="0 0 16 16" id="abuse" xmlns="http://www.w3.org/2000/svg"><path d="M11.408.328l4.029 3.222A1.5 1.5 0 0 1 16 4.72v6.555a1.5 1.5 0 0 1-.563 1.171l-4.026 3.224a1.5 1.5 0 0 1-.937.329H5.529a1.5 1.5 0 0 1-.937-.328L.563 12.45A1.5 1.5 0 0 1 0 11.28V4.724a1.5 1.5 0 0 1 .563-1.171L4.589.329A1.5 1.5 0 0 1 5.526 0h4.945c.34 0 .67.116.937.328zM10.296 2H5.702L2 4.964v6.074L5.704 14h4.594L14 11.036V4.962L10.296 2zM8 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V5a1 1 0 0 1 1-1zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="account" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9.195 9.965l-.568-.875a.25.25 0 0 1 .015-.294l.405-.5a.25.25 0 0 1 .283-.075l.938.36c.257-.183.543-.325.851-.42l.322-.988A.25.25 0 0 1 11.679 7h.642a.25.25 0 0 1 .238.173l.322.988c.308.095.594.237.851.42l.938-.36a.25.25 0 0 1 .283.076l.405.5a.25.25 0 0 1 .015.293l-.568.875c.113.297.18.616.193.95l.898.54a.25.25 0 0 1 .115.27l-.144.626a.25.25 0 0 1-.222.193l-1.115.098a3.015 3.015 0 0 1-.512.608l.165 1.18a.25.25 0 0 1-.138.259l-.577.281a.25.25 0 0 1-.29-.05l-.874-.905a3.035 3.035 0 0 1-.608 0l-.875.904a.25.25 0 0 1-.289.051l-.577-.281a.25.25 0 0 1-.138-.26l.165-1.18a3.015 3.015 0 0 1-.512-.607l-1.115-.098a.25.25 0 0 1-.222-.193l-.144-.626a.25.25 0 0 1 .115-.27l.898-.54c.013-.334.08-.653.193-.95zM6.789 8.023A12.845 12.845 0 0 0 6 8c-5.036 0-6 2.74-6 4.48C0 14.22.076 15 6 15c.553 0 1.055-.006 1.51-.02A5.977 5.977 0 0 1 6 11c0-1.083.287-2.1.79-2.977zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM12 12a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="admin" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.162 2.5a3.5 3.5 0 0 1-3.163 5.479L6.08 14.766a1.5 1.5 0 0 1-2.598-1.5L7.4 6.479A3.5 3.5 0 0 1 10.564 1L8.9 3.88l2.599 1.5 1.663-2.88zm-8.63 11.949a.5.5 0 1 0 .5-.866.5.5 0 0 0-.5.866z"/></symbol><symbol viewBox="0 0 16 16" id="angle-double-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.414 7.95l4.243-4.243a1 1 0 0 0-1.414-1.414l-4.95 4.95a.997.997 0 0 0 0 1.414l4.95 4.95a1 1 0 1 0 1.414-1.415L10.414 7.95zm-7 0l4.243-4.243a1 1 0 0 0-1.414-1.414l-4.95 4.95a.997.997 0 0 0 0 1.414l4.95 4.95a1 1 0 0 0 1.414-1.415L3.414 7.95z"/></symbol><symbol viewBox="0 0 16 16" id="angle-double-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.536 7.95L1.293 3.707a1 1 0 0 1 1.414-1.414l4.95 4.95a.997.997 0 0 1 0 1.414l-4.95 4.95a1 1 0 1 1-1.414-1.415L5.536 7.95zm7 0L8.293 3.707a1 1 0 0 1 1.414-1.414l4.95 4.95a.997.997 0 0 1 0 1.414l-4.95 4.95a1 1 0 0 1-1.414-1.415l4.243-4.242z"/></symbol><symbol viewBox="0 0 16 16" id="angle-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 10.243l-4.95-4.95a1 1 0 0 0-1.414 1.414l5.657 5.657a.997.997 0 0 0 1.414 0l5.657-5.657a1 1 0 0 0-1.414-1.414L8 10.243z"/></symbol><symbol viewBox="0 0 16 16" id="angle-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.757 8l4.95-4.95a1 1 0 1 0-1.414-1.414L3.636 7.293a.997.997 0 0 0 0 1.414l5.657 5.657a1 1 0 0 0 1.414-1.414L5.757 8z"/></symbol><symbol viewBox="0 0 16 16" id="angle-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.243 8l-4.95-4.95a1 1 0 0 1 1.414-1.414l5.657 5.657a.997.997 0 0 1 0 1.414l-5.657 5.657a1 1 0 0 1-1.414-1.414L10.243 8z"/></symbol><symbol viewBox="0 0 16 16" id="angle-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 6.757l-4.95 4.95a1 1 0 1 1-1.414-1.414l5.657-5.657a.997.997 0 0 1 1.414 0l5.657 5.657a1 1 0 0 1-1.414 1.414L8 6.757z"/></symbol><symbol viewBox="0 0 16 16" id="appearance" xmlns="http://www.w3.org/2000/svg"><path d="M11.161 12.456l.232.121c.1.053.175.094.249.137.53.318.844.75.857 1.402.012 1.397-1.116 1.756-3.12 1.858a23.85 23.85 0 0 1-1.38.026A8 8 0 0 1 0 8a8 8 0 0 1 8-8c4.417 0 7.998 3.582 7.998 7.977.06 2.621-1.312 3.586-4.48 3.648-.602.008-1.068.043-1.4.104.228.192.598.47 1.043.727zm-3.287-.943c-.019-1.495 1.228-1.856 3.611-1.888C13.67 9.582 14.028 9.33 13.998 8A6 6 0 1 0 8 14c.603 0 .91-.004 1.277-.023a9.7 9.7 0 0 0 .478-.035c-1.172-.738-1.868-1.47-1.88-2.43zM6 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm-2-3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM4 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="applications" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 0h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm6-6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 1v2h2V1H7zm0 5h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm6-6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm0 1v2h2V7h-2zM1 12h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1zm0 1v2h2v-2H1zm6-1h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1zm6 0h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="approval" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.536 10.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 1 1 9.12 9.243l1.415 1.414zM7.632 8.109A2 2 0 0 0 7 11.364l2.121 2.121a1.996 1.996 0 0 0 2.807.021C11.686 14.554 10.627 15 6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8c.6 0 1.142.038 1.632.109zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="arrow-down" xmlns="http://www.w3.org/2000/svg"><path d="M10.472 7.282a.862.862 0 0 1 1.26-.006c.357.364.357.958 0 1.285L8.627 11.73A.886.886 0 0 1 8 12a.849.849 0 0 1-.627-.27L4.275 8.561a.904.904 0 0 1-.013-1.285.861.861 0 0 1 1.26-.007l2.486 2.527z"/></symbol><symbol viewBox="0 0 16 16" id="arrow-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 6H2a2 2 0 1 0 0 4h7v2.586a1 1 0 0 0 1.707.707l4.586-4.586a1 1 0 0 0 0-1.414l-4.586-4.586A1 1 0 0 0 9 3.414V6z"/></symbol><symbol viewBox="0 0 16 16" id="assignee" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12 5V4a1 1 0 0 1 2 0v1h1a1 1 0 0 1 0 2h-1v1a1 1 0 0 1-2 0V7h-1a1 1 0 0 1 0-2h1zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="bold" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M4 12.5v-9A1.5 1.5 0 0 1 5.5 2h2.104c2.182 0 3.879.681 3.879 2.982 0 1.067-.517 2.227-1.374 2.595v.073C11.176 7.963 12 8.865 12 10.466 12 12.914 10.19 14 7.911 14H5.5A1.5 1.5 0 0 1 4 12.5zm2.376-5.696H7.49c1.164 0 1.665-.552 1.665-1.417 0-.94-.534-1.289-1.649-1.289h-1.13v2.706zm0 5.098h1.341c1.293 0 1.956-.515 1.956-1.62 0-1.049-.647-1.472-1.956-1.472H6.376v3.092z"/></symbol><symbol viewBox="0 0 16 16" id="book" xmlns="http://www.w3.org/2000/svg"><path d="M7 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2v4.191a.5.5 0 0 1-.724.447l-1.052-.526a.5.5 0 0 0-.448 0l-1.052.526A.5.5 0 0 1 7 6.191V2zM5 0h6a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"/></symbol><symbol viewBox="0 0 16 16" id="bookmark" xmlns="http://www.w3.org/2000/svg"><path d="M6.746 10.505a2 2 0 0 1 2.508 0L11 11.911V3H5v8.91l1.746-1.405zM5 1h6a2 2 0 0 1 2 2v10.999a1 1 0 0 1-1.627.779L8 12.064l-3.373 2.714A1 1 0 0 1 3 13.998V3a2 2 0 0 1 2-2z"/></symbol><symbol viewBox="0 0 16 16" id="branch" xmlns="http://www.w3.org/2000/svg"><path d="M6 11.978v.29a2 2 0 1 1-2 0V3.732a2 2 0 1 1 2 0v3.849c.592-.491 1.31-.854 2.15-1.081 1.308-.353 1.875-.882 1.893-1.743a2 2 0 1 1 2.002-.051C12.053 6.54 10.857 7.84 8.67 8.43 7.056 8.867 6.195 9.98 6 11.978zM5 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm6 1a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM5 15a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="bullhorn" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6.143 10H7V4H3a3 3 0 1 0 0 6h.143l.734 5.141a1 1 0 0 0 .99.859h1.556a.5.5 0 0 0 .495-.57L6.143 10zM8 4c1.034.02 2.039-.274 3.014-.883.727-.455 1.836-1.334 3.328-2.637A1 1 0 0 1 16 1.233v10.764a1 1 0 0 1-1.595.803c-1.658-1.227-2.788-1.992-3.392-2.294-.781-.39-1.785-.559-3.013-.506V4z"/></symbol><symbol viewBox="0 0 16 16" id="calendar" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12 2h2a2 2 0 0 1 2 2H0a2 2 0 0 1 2-2h2V1a1 1 0 1 1 2 0v1h4V1a1 1 0 1 1 2 0v1zM0 4h16v9a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V4zm2 2.5V13a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6.5a.5.5 0 0 0-.5-.5h-11a.5.5 0 0 0-.5.5zM5 8h2a1 1 0 1 1 0 2H5a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="cancel" xmlns="http://www.w3.org/2000/svg"><path d="M3.11 4.523a6 6 0 0 0 8.367 8.367L3.109 4.524zM4.522 3.11l8.368 8.368A6 6 0 0 0 4.524 3.11zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/></symbol><symbol viewBox="0 0 16 16" id="chart" xmlns="http://www.w3.org/2000/svg"><path d="M15 14a1 1 0 0 1 0 2H2a2 2 0 0 1-2-2V1a1 1 0 1 1 2 0v13h13zM3.142 8.735l2.502-2.561a.5.5 0 0 1 .714-.003L8 7.833l3.592-4.553a.5.5 0 0 1 .796.015l2.516 3.454a.5.5 0 0 1 .096.295V12.5a.5.5 0 0 1-.5.5h-11a.5.5 0 0 1-.5-.5V9.085a.5.5 0 0 1 .142-.35z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.078 8.2l3.535-3.536a2 2 0 0 1 2.828 2.828l-4.949 4.95c-.39.39-.902.586-1.414.586a1.994 1.994 0 0 1-1.414-.586l-4.95-4.95a2 2 0 1 1 2.828-2.828l3.536 3.535z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.977 7.998l3.535-3.535a2 2 0 1 0-2.828-2.828l-4.95 4.949c-.39.39-.586.902-.586 1.414 0 .512.196 1.024.586 1.414l4.95 4.95a2 2 0 1 0 2.828-2.828L7.977 7.998z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.22 7.998L4.683 4.463a2 2 0 0 1 2.828-2.828l4.95 4.949c.39.39.586.902.586 1.414a1.99 1.99 0 0 1-.586 1.414l-4.95 4.95a2 2 0 0 1-2.828-2.828l3.535-3.536z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.778 8.957l3.535 3.535a2 2 0 1 0 2.828-2.828l-4.949-4.95a1.994 1.994 0 0 0-1.414-.586c-.512 0-1.024.196-1.414.586l-4.95 4.95a2 2 0 1 0 2.828 2.828l3.536-3.535z"/></symbol><symbol viewBox="0 0 16 16" id="clock" xmlns="http://www.w3.org/2000/svg"><path d="M9 7h1a1 1 0 0 1 0 2H8a.997.997 0 0 1-1-1V5a1 1 0 1 1 2 0v2zm-1 9A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="close" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9.414 8l4.95-4.95a1 1 0 0 0-1.414-1.414L8 6.586l-4.95-4.95A1 1 0 0 0 1.636 3.05L6.586 8l-4.95 4.95a1 1 0 1 0 1.414 1.414L8 9.414l4.95 4.95a1 1 0 1 0 1.414-1.414L9.414 8z"/></symbol><symbol viewBox="0 0 16 16" id="code" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M15.871 8.243a.997.997 0 0 0-.293-.707L12.75 4.707a1 1 0 0 0-1.414 1.414l2.12 2.122-2.12 2.121a1 1 0 0 0 1.414 1.414l2.828-2.828a.997.997 0 0 0 .293-.707zm-13.243 0L4.75 6.12a1 1 0 1 0-1.414-1.414L.507 7.536a.997.997 0 0 0 0 1.414l2.829 2.828a1 1 0 1 0 1.414-1.414L2.628 8.243zm6.407-4.107a1 1 0 0 1 .707 1.225L8.19 11.157a1 1 0 1 1-1.931-.518L7.81 4.843a1 1 0 0 1 1.224-.707z"/></symbol><symbol viewBox="0 0 9 13" id="collapse"><path d="M.084.25C.01.18-.015.12.008.071.031.024.093 0 .194 0h8.521c.1 0 .162.024.185.072.023.048-.002.107-.075.177l-4.11 3.935a.372.372 0 0 1-.11.072h-.301a.508.508 0 0 1-.11-.072L.084.249zM.377 6.88a.364.364 0 0 1-.26-.105.334.334 0 0 1-.11-.25v-.709c0-.096.036-.179.11-.249a.364.364 0 0 1 .26-.105h8.15c.101 0 .188.035.261.105.074.07.11.153.11.25v.709c0 .096-.036.179-.11.249a.364.364 0 0 1-.26.105H.377zM.084 12.132c-.074.07-.099.129-.076.177.023.048.085.072.186.072h8.521c.1 0 .162-.024.185-.072.023-.048-.002-.107-.075-.177l-4.11-3.935a.372.372 0 0 0-.11-.072h-.301a.508.508 0 0 0-.11.072l-4.11 3.935z"/></symbol><symbol viewBox="0 0 16 16" id="comment" xmlns="http://www.w3.org/2000/svg"><path d="M1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586z"/></symbol><symbol viewBox="0 0 16 16" id="comment-dots" xmlns="http://www.w3.org/2000/svg"><path d="M1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586zM5 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="comment-next" xmlns="http://www.w3.org/2000/svg"><path d="M8 5V4a.5.5 0 0 1 .8-.4l2.667 2a.5.5 0 0 1 0 .8L8.8 8.4A.5.5 0 0 1 8 8V7H6a1 1 0 1 1 0-2h2zM1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586z"/></symbol><symbol viewBox="0 0 16 16" id="comments" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3.75 10L0 13V3a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2H3.75zM13 5h1a2 2 0 0 1 2 2v8l-2.667-2H8a2 2 0 0 1-2-2h4a3 3 0 0 0 3-3V5z"/></symbol><symbol viewBox="0 0 16 16" id="commit" xmlns="http://www.w3.org/2000/svg"><path d="M8 10a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm3.876-1.008a4.002 4.002 0 0 1-7.752 0A1.01 1.01 0 0 1 4 9H1a1 1 0 1 1 0-2h3c.042 0 .083.003.124.008a4.002 4.002 0 0 1 7.752 0A1.01 1.01 0 0 1 12 7h3a1 1 0 0 1 0 2h-3a1.01 1.01 0 0 1-.124-.008z"/></symbol><symbol viewBox="0 0 16 16" id="credit-card" xmlns="http://www.w3.org/2000/svg"><path d="M14 5a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1h12zm0 3H2v3a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V8zM3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm6.5 8h3a.5.5 0 1 1 0 1h-3a.5.5 0 1 1 0-1z"/></symbol><symbol viewBox="0 0 16 16" id="cut" xmlns="http://www.w3.org/2000/svg"><rect width="16" height="2" y="7" fill-rule="evenodd" rx="1"/></symbol><symbol viewBox="0 0 16 16" id="dashboard" xmlns="http://www.w3.org/2000/svg"><path d="M7.709 10.021l.696-2.6a.5.5 0 0 1 .966.26l-.657 2.45A2 2 0 0 1 10 12H6a2 2 0 0 1 1.709-1.979zM0 8.9a8 8 0 0 1 15.998 0H16v3.6a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5V8.9zM14 9A6 6 0 1 0 2 9v3.5a.5.5 0 0 0 .5.5h11a.5.5 0 0 0 .5-.5V9zM3.5 9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm9 0a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-7-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm5 0a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1z"/></symbol><symbol viewBox="0 0 16 16" id="disk" xmlns="http://www.w3.org/2000/svg"><path d="M16 11.764V3a3 3 0 0 0-3-3H3a3 3 0 0 0-3 3v8.764A2.989 2.989 0 0 1 2 11V3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v8c.768 0 1.47.289 2 .764zM2 12h12a2 2 0 1 1 0 4H2a2 2 0 1 1 0-4zm10 1a1 1 0 1 0 0 2 1 1 0 0 0 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="doc_code" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zm1.036 7.607a.498.498 0 0 1-.147.354l-1.414 1.414a.5.5 0 0 1-.707-.707l1.06-1.06-1.06-1.061a.5.5 0 0 1 .707-.707l1.414 1.414a.498.498 0 0 1 .147.353zm-4.822 0l1.06 1.061a.5.5 0 0 1-.706.707l-1.414-1.414a.498.498 0 0 1 0-.707l1.414-1.414a.5.5 0 1 1 .707.707l-1.06 1.06zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"/></symbol><symbol viewBox="0 0 16 16" id="doc_image" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zM7.333 9.667l1.313-1.313a.5.5 0 0 1 .708 0L12 11H4l2.188-1.75a.5.5 0 0 1 .624 0l.521.417zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm.5 8a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zM4 11h8v.7a.3.3 0 0 1-.3.3H4.3a.3.3 0 0 1-.3-.3V11z"/></symbol><symbol viewBox="0 0 16 16" id="doc_text" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm.5 11h5a.5.5 0 1 1 0 1h-5a.5.5 0 1 1 0-1zm0-2h5a.5.5 0 1 1 0 1h-5a.5.5 0 0 1 0-1zm0-2h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1z"/></symbol><symbol viewBox="0 0 105 26" id="double-headed-arrow" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1.018 11.089L15.138.614c1.23-.911 3.086-.795 4.147.26.461.46.715 1.045.715 1.651v20.95C20 24.869 18.684 26 17.06 26a3.238 3.238 0 0 1-1.921-.614L1.019 14.911C-.212 14-.347 12.405.714 11.35c.094-.094.195-.18.303-.261zm102.964 0c.108.08.21.167.303.26 1.061 1.056.925 2.65-.303 3.562l-14.12 10.475A3.238 3.238 0 0 1 87.94 26C86.316 26 85 24.87 85 23.475V2.525c0-.606.254-1.192.715-1.65 1.061-1.056 2.917-1.172 4.146-.26l14.12 10.474zM35 17a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm18 0a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm18 0a4 4 0 1 1 0-8 4 4 0 0 1 0 8z"/></symbol><symbol viewBox="0 0 16 16" id="download" xmlns="http://www.w3.org/2000/svg"><path d="M9 12h1a.5.5 0 0 1 .4.8l-2 2.667a.5.5 0 0 1-.8 0l-2-2.667A.5.5 0 0 1 6 12h1V8a1 1 0 1 1 2 0v4zM4 9a1 1 0 1 1 0 2 4 4 0 0 1-1.971-7.481 4 4 0 0 1 6.633-2.505 3.999 3.999 0 0 1 3.82 2.014A4 4 0 0 1 12 11a1 1 0 0 1 0-2 2 2 0 1 0 0-4h-1a2 2 0 0 0-3.112-1.662A2 2 0 1 0 4.268 5H4a2 2 0 1 0 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="duplicate" xmlns="http://www.w3.org/2000/svg"><path d="M14 10h-3a1 1 0 0 1-1-1V6H8.527A.527.527 0 0 0 8 6.527V13a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1v-3zm-4-7H8.527c-.18 0-.355.013-.527.04V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2v2H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h4a3 3 0 0 1 3 3zM8.527 4h2.323a.5.5 0 0 1 .35.143l4.65 4.551a.5.5 0 0 1 .15.357V13a3 3 0 0 1-3 3H9a3 3 0 0 1-3-3V6.527A2.527 2.527 0 0 1 8.527 4z"/></symbol><symbol viewBox="0 0 16 16" id="earth" xmlns="http://www.w3.org/2000/svg"><path d="M8.7 2.04l-.082.177c.283.223.422.413.417.571-.008.237-.311.057-.444.274-.133.218.038.542-.112.637-.15.096-.398-.386-.479-.46-.054-.049-.166-.257-.336-.625l-.216-.225a.844.844 0 0 0-.418-.035c-.177.038-.075.1-.035.132.04.032.32.037.452.2.132.164.03.224-.05.298-.054.05-.157.062-.31.035H5.952l-.402.398.03.325.229.455.324-.463c.008-.206.058-.342.15-.41.14-.1.342-.15.534-.085.191.066-.057.218.011.271.068.053.204-.098.313-.02.11.08.07.155.104.322.036.167.254.114.398.328.144.215.19.29.147.483-.043.195-.168.26-.305.232-.138-.028-.107-.246-.275-.348-.168-.102-.266-.114-.386-.054-.12.06-.016.129.023.235.04.106.274.321.224.43-.05.107-.108.116-.42 0-.21-.077-.414-.007-.615.212l-.76.722c-.153.715-.3 1.13-.44 1.243-.211.17-.177-.483-.483-.656-.306-.174-.494-.047-.8-.07-.307-.023-.42.65-.38.873a.434.434 0 0 0 .221.321c.236-.141.39-.184.465-.128.11.084-.144.267-.074.425.07.158.314.069.386.283.073.213.084.48-.05.706-.135.227-.275.178-.4.053-.127-.126-.033-.375-.255-.704-.223-.329-.381-.337-.63-.787-.158-.287-.35-.743-.575-1.366a6 6 0 0 0 3.21 7.198l.001-.075c0-.577-.004-.944-.012-1.102-.011-.236-.95-.945-1.104-1.2-.154-.256-.34-.595-.355-.746-.016-.151.185-.232.344-.325.16-.093-.11-.367.028-.626.137-.258.395-.438.496-.356.101.081.058.228.267.333.209.104.077-.213.456-.178.38.035.143.201.252.216.11.016.113-.127.299-.143.186-.015.282.445.471.622.19.178.452.008.611.043.159.034.267.09.402.255.136.166-.03.352.073.557.103.205 1.07.22 1.433.255.364.034.371.011.371.324s-.166.314-.453.507c-.286.193-.166.462-.38.762-.212.3-.316.062-.622.14-.306.077-.413.382-.452.568-.039.186-.386.094-.877.232-.29.082-.429.144-.569.204a6.002 6.002 0 0 0 7.682-4.3c-.094-.384-.18-.63-.258-.74-.213-.297-.36.21-.924.49-.564.278-.57-.288-.81-.49-.16-.133-.212-.44-.158-.92-.005-.478.02-.828.077-1.049.057-.221.126-.543.207-.965.351-.373.606-.572.764-.595.237-.034.336.374.658.3a.315.315 0 0 0 .035-.01 5.993 5.993 0 0 0-.475-.824l-.309-.043a.646.646 0 0 0-.332-.117c-.205-.02-.025.128-.089.24-.064.112-.235.724-.437.685-.201-.039-.204-.374-.17-.668.036-.294-.077-.35-.2-.412-.124-.062-.325-.213-.556-.295-.232-.082-.123-.175-.093-.274.03-.1.208-.015.193-.058-.014-.044-.313-.135-.266-.167.03-.02.2-.02.506.003l.216-.012.293-.163a.58.58 0 0 0-.376-.22c-.233-.036-.513-.034-.73-.142-.205-.103-.458-.36-.643-.638A5.965 5.965 0 0 0 8.7 2.04zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/></symbol><symbol viewBox="0 0 1600 1600" id="ellipsis_v" xmlns="http://www.w3.org/2000/svg"><path d="M1088 1248v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68v-192q0-40 28-68t68-28h192q40 0 68 28t28 68zm0-512v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68V736q0-40 28-68t68-28h192q40 0 68 28t28 68zm0-512v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68V224q0-40 28-68t68-28h192q40 0 68 28t28 68z"/></symbol><symbol viewBox="0 0 18 18" id="emoji_slightly_smiling_face" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369.721.721 0 0 1 .568.047.715.715 0 0 1 .37.445 2.91 2.91 0 0 0 1.084 1.518A2.93 2.93 0 0 0 9 12.75a2.93 2.93 0 0 0 1.775-.58 2.913 2.913 0 0 0 1.084-1.518.711.711 0 0 1 .375-.445.737.737 0 0 1 .575-.047c.195.063.34.186.433.37.094.183.11.372.047.568zM7.5 6c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 6 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 4.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 6 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm6 0c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 12 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 10.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 12 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm3 3a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6A7.29 7.29 0 0 0 9 16.5a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39A7.29 7.29 0 0 0 16.5 9zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 18 18" id="emoji_smile" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369c.195-.062 7.41-.062 7.606 0 .195.063.34.186.433.37.094.183.11.372.047.568zM14 6.37c0 .398-.04.755-.513.755-.473 0-.498-.272-1.237-.272-.74 0-.74.215-1.165.215-.425 0-.585-.3-.585-.698 0-.397.17-.736.513-1.017.341-.281.754-.422 1.237-.422.483 0 .896.14 1.237.422.342.28.513.62.513 1.017zm-6.5 0c0 .398-.04.755-.513.755-.473 0-.498-.272-1.237-.272-.74 0-.74.215-1.165.215-.425 0-.585-.3-.585-.698 0-.397.17-.736.513-1.017.341-.281.754-.422 1.237-.422.483 0 .896.14 1.237.422.342.28.513.62.513 1.017zm9 2.63a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6A7.29 7.29 0 0 0 9 16.5a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39A7.29 7.29 0 0 0 16.5 9zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 18 18" id="emoji_smiley" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369c.195-.062 7.41-.062 7.606 0 .195.063.34.186.433.37.094.183.11.372.047.568h.001zM7.5 6c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 6 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 4.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 6 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm6 0c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 12 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 10.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 12 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm3 3a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6c.92.397 1.91.6 2.912.598a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39c.397-.92.6-1.91.598-2.912zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z"/></symbol><symbol viewBox="0 0 16 16" id="epic" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M14.985 8.044l-.757 2.272a1 1 0 0 1-.949.684H1.618a1 1 0 0 1-.894-1.447l.318-.637A2 2 0 0 0 1.618 9h11.661a2 2 0 0 0 1.706-.956zm0 3l-.757 2.272a1 1 0 0 1-.949.684H1.618a1 1 0 0 1-.894-1.447l.318-.637a2 2 0 0 0 .576.084h11.661a2 2 0 0 0 1.706-.956zM3.618 2h10.995a1 1 0 0 1 .948 1.316l-1.333 4a1 1 0 0 1-.949.684H1.618a1 1 0 0 1-.894-1.447l2-4A1 1 0 0 1 3.618 2zm-.382 4h9.322l.667-2H4.236l-1 2z"/></symbol><symbol viewBox="0 0 16 16" id="external-link" xmlns="http://www.w3.org/2000/svg"><path d="M13.121 4.177l-4.95 4.95a1 1 0 1 1-1.414-1.414l4.95-4.95-1.386-1.386a.5.5 0 0 1 .299-.85l4.709-.524a.5.5 0 0 1 .552.552l-.523 4.71a.5.5 0 0 1-.851.297l-1.386-1.385zM12 8.884a1 1 0 0 1 2 0v4a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3v-8a3 3 0 0 1 3-3h4a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-4z"/></symbol><symbol viewBox="0 0 16 16" id="eye" xmlns="http://www.w3.org/2000/svg"><path d="M8 14C4.816 14 2.253 12.284.393 8.981a2 2 0 0 1 0-1.962C2.253 3.716 4.816 2 8 2s5.747 1.716 7.607 5.019a2 2 0 0 1 0 1.962C13.747 12.284 11.184 14 8 14zm0-2c2.41 0 4.338-1.29 5.864-4C12.338 5.29 10.411 4 8 4 5.59 4 3.662 5.29 2.136 8 3.662 10.71 5.589 12 8 12zm0-1a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm1-3a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="eye-slash" xmlns="http://www.w3.org/2000/svg"><path d="M13.618 2.62L1.62 14.619a1 1 0 0 1-.985-1.668l1.525-1.526C1.516 10.742.926 9.927.393 8.981a2 2 0 0 1 0-1.962C2.253 3.716 4.816 2 8 2c1.074 0 2.076.195 3.006.58l.944-.944a1 1 0 0 1 1.668.985zM8.068 11a3 3 0 0 0 2.931-2.932l-2.931 2.931zm-3.02-2.462a3 3 0 0 1 3.49-3.49l.884-.884A6.044 6.044 0 0 0 8 4C5.59 4 3.662 5.29 2.136 8c.445.79.924 1.46 1.439 2.011l1.473-1.473zm.421 5.06l1.658-1.658c.283.04.575.06.873.06 2.41 0 4.338-1.29 5.864-4a11.023 11.023 0 0 0-1.133-1.664l1.418-1.418a12.799 12.799 0 0 1 1.458 2.1 2 2 0 0 1 0 1.963C13.747 12.284 11.184 14 8 14a7.883 7.883 0 0 1-2.53-.402z"/></symbol><symbol viewBox="0 0 16 16" id="file-addition" xmlns="http://www.w3.org/2000/svg"><path d="M7 7V5a1 1 0 1 1 2 0v2h2a1 1 0 0 1 0 2H9v2a1 1 0 0 1-2 0V9H5a1 1 0 1 1 0-2h2zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3z"/></symbol><symbol viewBox="0 0 16 16" id="file-deletion" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3zm2 6h6a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="file-modified" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3zm5 4a3 3 0 1 1 0 6 3 3 0 0 1 0-6z"/></symbol><symbol viewBox="0 0 16 16" id="filter" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 6v9l-3.724-1.862A.5.5 0 0 1 6 12.691V6L1.854 1.854A.5.5 0 0 1 2.207 1h11.586a.5.5 0 0 1 .353.854L10 6z"/></symbol><symbol viewBox="0 0 16 16" id="folder" xmlns="http://www.w3.org/2000/svg"><path d="M13 3a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a2 2 0 0 1 2-2h3.81a3 3 0 0 1 2.827 1.995L13 3z"/></symbol><symbol viewBox="0 0 16 16" id="folder-o" xmlns="http://www.w3.org/2000/svg"><path d="M13 5l-4.365-.005a2 2 0 0 1-1.882-1.33A1 1 0 0 0 5.81 3H2v9a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1zm0-2a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a2 2 0 0 1 2-2h3.81a3 3 0 0 1 2.827 1.995L13 3z"/></symbol><symbol viewBox="0 0 16 16" id="folder-open" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M14.59 5.464a2.998 2.998 0 0 1 1.096 3.845l-1.666 3.436A4 4 0 0 1 10.46 15H3a3 3 0 0 1-3-3V3a2 2 0 0 1 2-2h3.558a2 2 0 0 1 1.898 1.368l.21.632h4.973a2 2 0 0 1 2 2 2 2 0 0 1-.027.329l-.023.135zM5.285 7a1 1 0 0 0-.9.564l-1.939 4a1 1 0 0 0 .9 1.436h7.074a2 2 0 0 0 1.8-1.128l1.665-3.436a1 1 0 0 0-.9-1.436h-7.7z"/></symbol><symbol viewBox="0 0 16 16" id="fork" xmlns="http://www.w3.org/2000/svg"><path d="M9 12.268a2 2 0 1 1-2 0V8.874A4.002 4.002 0 0 1 4 5V3.732a2 2 0 1 1 2 0V5a2 2 0 1 0 4 0V3.732a2 2 0 1 1 2 0V5a4.002 4.002 0 0 1-3 3.874v3.394zM11 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM5 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm3 12a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="geo-nodes" xmlns="http://www.w3.org/2000/svg"><path d="M9.7 13.1l-.2.2c-.7.8-2 .9-2.8.1-.1 0-.1-.1-.1-.1l-.2-.2c-2 .2-3.4.7-3.4 1.4 0 .8 2.2 1.5 5 1.5s5-.7 5-1.5c0-.7-1.4-1.2-3.3-1.4M7.3 12.7c.4.4 1 .3 1.4-.1C11.6 9.5 13 7 13 5.3 13 2.4 10.8 0 8 0S3 2.4 3 5.3C3 7 4.4 9.5 7.3 12.7M8 2c1.6 0 3 1.4 3 3.3 0 1-1 2.8-3 5.2-2-2.4-3-4.2-3-5.2C5 3.4 6.4 2 8 2"/><circle cx="8" cy="5" r="1"/></symbol><symbol viewBox="0 0 16 16" id="git-merge" xmlns="http://www.w3.org/2000/svg"><path d="M11 12.268V5a1 1 0 0 0-1-1v1a.5.5 0 0 1-.8.4l-2.667-2a.5.5 0 0 1 0-.8L9.2.6a.5.5 0 0 1 .8.4v1a3 3 0 0 1 3 3v7.268a2 2 0 1 1-2 0zm-6 0a2 2 0 1 1-2 0V4.732a2 2 0 1 1 2 0v7.536zM4 4a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm0 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm8 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="group" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3.048 11.997C-.377 11.975.013 11.782.013 10.56.013 9.235.653 8 4 8c.444 0 .84.022 1.194.062.164.435.426.82.76 1.132-1.786.389-2.721 1.353-2.906 2.803zm2.94-7.222a2.993 2.993 0 0 0-.976 1.95 2 2 0 1 1 .975-1.95zm6.964 7.222c-.185-1.45-1.12-2.414-2.906-2.803.334-.311.596-.697.76-1.132C11.16 8.022 11.556 8 12 8c3.346 0 3.987 1.235 3.987 2.56 0 1.222.39 1.415-3.035 1.437zm-1.964-5.272a2.993 2.993 0 0 0-.976-1.95 2 2 0 1 1 .976 1.95zM8 9a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 5c-2.177 0-3.987-.115-3.987-1.44S4.653 10 8 10c3.346 0 3.987 1.235 3.987 2.56S10.177 14 8 14z"/></symbol><symbol viewBox="0 0 16 16" id="history" xmlns="http://www.w3.org/2000/svg"><path d="M2.868 3.24a7 7 0 1 1-.043 9.475 1 1 0 0 1 1.478-1.348 5 5 0 1 0 .124-6.865l.796.645a.5.5 0 0 1-.193.873l-3.232.814a.5.5 0 0 1-.622-.504L1.3 3a.5.5 0 0 1 .814-.37l.754.61zM9 8h1a1 1 0 0 1 0 2H8a.997.997 0 0 1-1-1V6a1 1 0 1 1 2 0v2z"/></symbol><symbol viewBox="0 0 16 16" id="home" xmlns="http://www.w3.org/2000/svg"><path d="M9 13h3v-3H4v3h3v-1a1 1 0 0 1 2 0v1zm5-3v3.659c0 .729-.657 1.341-1.5 1.341h-9c-.843 0-1.5-.612-1.5-1.341V10h-.88C.502 10 0 9.486 0 8.853c0-.307.12-.601.333-.816l6.405-6.463a1.56 1.56 0 0 1 2.374-.052L15.66 8.03c.444.441.455 1.167.024 1.622a1.108 1.108 0 0 1-.804.348H14zM7.95 3.273l-4.595 4.64h9.264l-4.67-4.64z"/></symbol><symbol viewBox="0 0 16 16" id="hook" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 3a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1h4zm0 1H6v1a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V4zM7 8a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h2a3 3 0 0 1 3 3v2a3 3 0 0 1-3 3v4a2 2 0 1 0 4 0h-.44a.3.3 0 0 1-.25-.466l1.44-2.16a.3.3 0 0 1 .5 0l1.44 2.16a.3.3 0 0 1-.25.466H15a4 4 0 0 1-7 2.646A4 4 0 0 1 1 12H.56a.3.3 0 0 1-.25-.466l1.44-2.16a.3.3 0 0 1 .5 0l1.44 2.16a.3.3 0 0 1-.25.466H3a2 2 0 1 0 4 0V8z"/></symbol><symbol viewBox="0 0 16 16" id="hourglass" xmlns="http://www.w3.org/2000/svg"><path d="M10.331 4.889A2.988 2.988 0 0 0 11 3V2H5v1c0 .362.064.709.182 1.03l5.15.859zM3 14v-1c0-1.78.93-3.342 2.33-4.228.447-.327.67-.582.67-.764 0-.19-.242-.46-.725-.815A4.996 4.996 0 0 1 3 3V2H2a1 1 0 1 1 0-2h12a1 1 0 0 1 0 2h-1v1a4.997 4.997 0 0 1-2.39 4.266c-.407.3-.61.545-.61.734 0 .19.203.434.61.734A4.997 4.997 0 0 1 13 13v1h1a1 1 0 0 1 0 2H2a1 1 0 0 1 0-2h1zm8 0v-1a3 3 0 0 0-6 0v1h6z"/></symbol><symbol viewBox="0 0 38 38" id="image-comment-dark" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><circle cx="19" cy="19" r="18" fill="#1F78D1"/><path fill="#FFF" fill-rule="nonzero" d="M19 38C8.507 38 0 29.493 0 19S8.507 0 19 0s19 8.507 19 19-8.507 19-19 19zm0-2c9.389 0 17-7.611 17-17S28.389 2 19 2 2 9.611 2 19s7.611 17 17 17zm-6.293-8.293c-.63.63-1.707.184-1.707-.707V15a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3h-7.586l-3.707 3.707zM13 24.586l2.293-2.293A1 1 0 0 1 16 22h8a1 1 0 0 0 1-1v-6a1 1 0 0 0-1-1H14a1 1 0 0 0-1 1v9.586z"/></g></symbol><symbol viewBox="0 0 38 38" id="image-comment-light" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><circle cx="19" cy="19" r="18" fill="#FFF"/><path fill="#1F78D1" fill-rule="nonzero" d="M19 38C8.507 38 0 29.493 0 19S8.507 0 19 0s19 8.507 19 19-8.507 19-19 19zm0-2c9.389 0 17-7.611 17-17S28.389 2 19 2 2 9.611 2 19s7.611 17 17 17zm-6.293-8.293c-.63.63-1.707.184-1.707-.707V15a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3h-7.586l-3.707 3.707zM13 24.586l2.293-2.293A1 1 0 0 1 16 22h8a1 1 0 0 0 1-1v-6a1 1 0 0 0-1-1H14a1 1 0 0 0-1 1v9.586z"/></g></symbol><symbol viewBox="0 0 16 16" id="import" xmlns="http://www.w3.org/2000/svg"><path d="M9 8h1a.5.5 0 0 1 .4.8l-2 2.667a.5.5 0 0 1-.8 0L5.6 8.8A.5.5 0 0 1 6 8h1V1a1 1 0 1 1 2 0v7zM0 8a1 1 0 1 1 2 0 6 6 0 1 0 12 0 1 1 0 0 1 2 0A8 8 0 1 1 0 8z"/></symbol><symbol viewBox="0 0 16 16" id="issue-block" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.803 8a5.97 5.97 0 0 0-.462 1H4.5a.5.5 0 0 1 0-1h1.303zM4.5 5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1 0-1zm7.5.083a6.04 6.04 0 0 0-2 0V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h2.083a5.96 5.96 0 0 0 .72 2H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v2.083zm1.121 3.796zM11 16a5 5 0 1 1 0-10 5 5 0 0 1 0 10zm-1.293-2.292a3 3 0 0 0 4.001-4.001l-4.001 4zm-1.415-1.415l4.001-4a3 3 0 0 0-4.001 4.001z"/></symbol><symbol viewBox="0 0 16 16" id="issue-child" xmlns="http://www.w3.org/2000/svg"><path d="M11 8H5v1h1a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h2V7a.997.997 0 0 1 1-1h3V4H4.5a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5H9v2h3a.997.997 0 0 1 1 1v2h2a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-5a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h1V8zm-9 3v2h3v-2H2zm9 0v2h3v-2h-3z"/></symbol><symbol viewBox="0 0 16 16" id="issue-close" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 0 1 6.12 7.243l1.415 1.414zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="issue-duplicate" xmlns="http://www.w3.org/2000/svg"><path d="M10.874 2H12a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3h-2c-.918 0-1.74-.413-2.29-1.063a3.987 3.987 0 0 0 1.988-.984A1 1 0 0 0 10 14h2a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1h-1V3c0-.345-.044-.68-.126-1zM4 0h3a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H4z"/></symbol><symbol viewBox="0 0 16 16" id="issue-external" xmlns="http://www.w3.org/2000/svg"><path d="M11 4a5.99 5.99 0 0 0-2 .341V3a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h2.528a6.003 6.003 0 0 0 2.705 1.736A2.99 2.99 0 0 1 8 16H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h4a3 3 0 0 1 3 3v1zM8.212 8.97l-.568-.876A.25.25 0 0 1 7.66 7.8l.404-.5a.25.25 0 0 1 .284-.076l.938.36c.256-.182.543-.325.85-.42l.323-.988a.25.25 0 0 1 .237-.173h.643a.25.25 0 0 1 .238.173l.321.989c.308.094.595.237.852.418l.937-.359a.25.25 0 0 1 .284.076l.404.5a.25.25 0 0 1 .016.293l-.568.875c.113.297.18.616.192.95l.9.54a.25.25 0 0 1 .114.27l-.145.627a.25.25 0 0 1-.221.192l-1.115.098a3.015 3.015 0 0 1-.512.608l.165 1.18a.25.25 0 0 1-.138.259l-.577.282a.25.25 0 0 1-.29-.051l-.874-.905a3.035 3.035 0 0 1-.608 0l-.875.905a.25.25 0 0 1-.29.05l-.577-.281a.25.25 0 0 1-.138-.26L9 12.254a3.015 3.015 0 0 1-.512-.607l-1.114-.098a.25.25 0 0 1-.222-.192l-.145-.627a.25.25 0 0 1 .115-.27l.899-.54c.012-.334.08-.653.192-.95zm2.806 2.034a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="issue-new" xmlns="http://www.w3.org/2000/svg"><path d="M10 2V1a1 1 0 0 1 2 0v1h1a1 1 0 0 1 0 2h-1v1a1 1 0 0 1-2 0V4H9a1 1 0 1 1 0-2h1zm0 6a1 1 0 0 1 2 0v5a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h1a1 1 0 1 1 0 2H5a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V8z"/></symbol><symbol viewBox="0 0 16 16" id="issue-open" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm0-2a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm0-2a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="issue-open-m" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="issue-parent" xmlns="http://www.w3.org/2000/svg"><path d="M11 11H5v1h1.5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-6a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5H3v-2a.997.997 0 0 1 1-1h3V7H5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H9v2h3a.997.997 0 0 1 1 1v2h2.5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-6a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5H11v-1zM6 3v2h4V3H6z"/></symbol><symbol viewBox="0 0 16 16" id="issues" xmlns="http://www.w3.org/2000/svg"><path d="M10.458 15.012l.311.055a3 3 0 0 0 3.476-2.433l1.389-7.879A3 3 0 0 0 13.2 1.28L11.23.933a3.002 3.002 0 0 0-.824-.031c.364.59.58 1.28.593 2.02l1.854.328a1 1 0 0 1 .811 1.158l-1.389 7.879a1 1 0 0 1-1.158.81l-.118-.02a3.98 3.98 0 0 1-.541 1.935zM3 0h4a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="italic" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.5 12l2-8H6a1 1 0 1 1 0-2h6a1 1 0 0 1 0 2h-1.5l-2 8H10a1 1 0 0 1 0 2H4a1 1 0 0 1 0-2h1.5z"/></symbol><symbol viewBox="0 0 16 16" id="key" xmlns="http://www.w3.org/2000/svg"><path d="M7.575 6.689a4.002 4.002 0 0 1 6.274-4.86 4 4 0 0 1-4.86 6.274l-2.21 2.21.706.708a1 1 0 1 1-1.414 1.414l-.707-.707-.707.707.707.707a1 1 0 1 1-1.414 1.414l-.707-.707a1 1 0 0 1-1.414-1.414l5.746-5.746zm2.032-.618a2 2 0 1 0 2.828-2.828A2 2 0 0 0 9.607 6.07z"/></symbol><symbol viewBox="0 0 16 16" id="key-2" xmlns="http://www.w3.org/2000/svg"><path d="M5.172 14.157l-.344.344-2.485.133a.462.462 0 0 1-.497-.503l.14-2.24a.599.599 0 0 1 .177-.382l5.155-5.155a4 4 0 1 1 2.828 2.828l-1.439 1.44-1.06-.354-.708.707.354 1.06-.707.708-1.06-.354-.708.707.354 1.06zm6.01-8.839a1 1 0 1 0 1.414-1.414 1 1 0 0 0-1.414 1.414z"/></symbol><symbol viewBox="0 0 16 16" id="label" xmlns="http://www.w3.org/2000/svg"><path d="M11.782 14.718a3 3 0 0 1-4.242 0L1.652 8.829a2 2 0 0 1-.565-1.702l.54-3.703a2 2 0 0 1 1.69-1.69l3.703-.54a2 2 0 0 1 1.703.564l5.888 5.888a3 3 0 0 1 0 4.243l-2.829 2.829zm1.415-5.657L7.309 3.173l-3.703.54-.54 3.702 5.888 5.888a1 1 0 0 0 1.414 0l2.829-2.828a1 1 0 0 0 0-1.414zM5.732 5.525A1 1 0 1 1 7.146 6.94a1 1 0 0 1-1.414-1.414z"/></symbol><symbol viewBox="0 0 16 16" id="labels" xmlns="http://www.w3.org/2000/svg"><path d="M9.424 2.254l2.08-.905a1 1 0 0 1 1.206.326l3.013 4.12a1 1 0 0 1 .16.849l-1.947 7.264a3 3 0 0 1-3.675 2.122l-.5-.135a3.999 3.999 0 0 0 1.082-1.782 1 1 0 0 0 1.16-.722l1.823-6.802-2.258-3.087-.687.299a2 2 0 0 0-.628-.88l-.829-.667zM.377 3.7L4.4.498a1 1 0 0 1 1.25.003L9.627 3.7a1 1 0 0 1 .373.78V13a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V4.482A1 1 0 0 1 .377 3.7zM2 13a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V4.958L5.02 2.561 2 4.964V13zm3-6a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="leave" xmlns="http://www.w3.org/2000/svg"><path d="M11 7V5.883a.5.5 0 0 1 .757-.429l3.528 2.117a.5.5 0 0 1 0 .858l-3.528 2.117a.5.5 0 0 1-.757-.43V9H7a1 1 0 1 1 0-2h4zm-2 6.256a1 1 0 0 1 2 0A2.744 2.744 0 0 1 8.256 16H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h5.19A2.81 2.81 0 0 1 11 2.81a1 1 0 0 1-2 0A.81.81 0 0 0 8.19 2H3a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h5.256c.41 0 .744-.333.744-.744z"/></symbol><symbol viewBox="0 0 16 16" id="level-up" xmlns="http://www.w3.org/2000/svg"><path fill="#2E2E2E" fill-rule="evenodd" d="M7 6h3.489a.5.5 0 0 0 .373-.832L6.374.117a.5.5 0 0 0-.748 0l-4.488 5.05A.5.5 0 0 0 1.51 6H5v7a3 3 0 0 0 3 3h6a1 1 0 0 0 0-2H8a1 1 0 0 1-1-1V6z"/></symbol><symbol viewBox="0 0 16 16" id="license" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12.56 8.9l2.66 4.606a.3.3 0 0 1-.243.45l-1.678.094a.1.1 0 0 0-.078.044l-.953 1.432a.3.3 0 0 1-.51-.016L9.097 10.9a5.994 5.994 0 0 0 3.464-2zm-5.23 2.063L4.707 15.51a.3.3 0 0 1-.51.016l-.953-1.432a.1.1 0 0 0-.078-.044l-1.678-.094a.3.3 0 0 1-.243-.45l2.48-4.297a5.983 5.983 0 0 0 3.607 1.754zM8 10A5 5 0 1 1 8 0a5 5 0 0 1 0 10zm0-2a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0-1a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="link" xmlns="http://www.w3.org/2000/svg"><path d="M6.986 3.35l2.12-2.122a4 4 0 0 1 5.657 5.657l-2.828 2.829a4 4 0 0 1-5.657 0 1 1 0 0 1 1.414-1.415 2 2 0 0 0 2.829 0l2.828-2.828a2 2 0 1 0-2.828-2.828l-1.001 1a5.018 5.018 0 0 0-2.534-.294zm2.12 9.192l-2.12 2.121a4 4 0 1 1-5.658-5.656l2.829-2.829a4 4 0 0 1 5.657 0 1 1 0 1 1-1.415 1.414 2 2 0 0 0-2.828 0l-2.828 2.829a2 2 0 1 0 2.828 2.828l1.001-1.001a5.018 5.018 0 0 0 2.534.294z"/></symbol><symbol viewBox="0 0 16 16" id="list-bulleted" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm0 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4-7h10a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2zm0 5h10a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2zm-4 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4-2h10a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="list-numbered" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6 2h8a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2zm0 5h8a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2zm0 5h8a1 1 0 0 1 0 2H6a1 1 0 0 1 0-2zM1.156 5v-.828h.816V2.204h-.72v-.636c.432-.084.708-.192.996-.372h.756v2.976h.684V5H1.156zm-.18 5v-.588c.9-.828 1.596-1.464 1.596-1.98 0-.342-.192-.504-.468-.504-.252 0-.444.18-.624.36l-.552-.552c.396-.42.756-.612 1.32-.612.768 0 1.308.492 1.308 1.248 0 .612-.576 1.284-1.092 1.812.192-.024.468-.048.636-.048h.636V10H.976zm1.26 5.072c-.618 0-1.068-.204-1.356-.54l.468-.648c.234.216.51.36.78.36.336 0 .552-.12.552-.36 0-.288-.15-.456-.948-.456v-.72c.636 0 .828-.168.828-.432 0-.228-.138-.348-.396-.348-.252 0-.432.108-.672.312l-.516-.624c.372-.312.768-.492 1.236-.492.84 0 1.38.384 1.38 1.074 0 .366-.204.642-.612.822v.024c.432.132.732.432.732.912 0 .72-.684 1.116-1.476 1.116z"/></symbol><symbol viewBox="0 0 16 16" id="location" xmlns="http://www.w3.org/2000/svg"><path d="M8.755 15.144a1 1 0 0 1-1.51 0C3.748 11.114 2 8.065 2 6a6 6 0 1 1 12 0c0 2.065-1.748 5.113-5.245 9.144zM12 6a4 4 0 1 0-8 0c0 1.314 1.312 3.71 4 6.944C10.688 9.71 12 7.314 12 6zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="location-dot" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6.314 13.087C4.382 13.295 3 13.85 3 14.5c0 .828 2.239 1.5 5 1.5s5-.672 5-1.5c0-.65-1.382-1.205-3.314-1.413l-.202.225a2 2 0 0 1-2.968 0l-.202-.225zm2.428-.445a1 1 0 0 1-1.484 0C4.419 9.5 3 7.037 3 5.252 3 2.353 5.239 0 8 0s5 2.352 5 5.253c0 1.784-1.42 4.247-4.258 7.389zM11 5.252C11 3.436 9.634 2 8 2S5 3.435 5 5.253c0 1.027.974 2.824 3 5.203 2.026-2.38 3-4.176 3-5.203zM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="lock" xmlns="http://www.w3.org/2000/svg"><path d="M10 5V4h2v1a3 3 0 0 1 3 3v5a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V8a3 3 0 0 1 3-3V4h2v1h4zM4 7a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V8a1 1 0 0 0-1-1H4zm0-3a4 4 0 1 1 8 0h-2a2 2 0 1 0-4 0H4z"/></symbol><symbol viewBox="0 0 16 16" id="lock-open" xmlns="http://www.w3.org/2000/svg"><path d="M4.044 4a4 4 0 0 1 6.99-2.658 1 1 0 1 1-1.495 1.33A2 2 0 0 0 6.044 4a.998.998 0 0 1-.07.367v.701H12a3 3 0 0 1 3 3v5a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3v-5a3 3 0 0 1 2.974-3V4h.07zM4 7.07a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-5a1 1 0 0 0-1-1H4z"/></symbol><symbol viewBox="0 0 16 16" id="log" xmlns="http://www.w3.org/2000/svg"><path d="M4 0h8a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H4zm1 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm0 3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3-5h3a1 1 0 0 1 0 2H8a1 1 0 1 1 0-2zm0 3h3a1 1 0 0 1 0 2H8a1 1 0 1 1 0-2zm-3 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3-2h3a1 1 0 0 1 0 2H8a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="mail" xmlns="http://www.w3.org/2000/svg"><path d="M14 5.6L9.338 9.796a2 2 0 0 1-2.676 0L2 5.6V11a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5.6zM3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm.212 2L8 8.31 12.788 4H3.212z"/></symbol><symbol viewBox="0 0 16 16" id="menu" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1.143 2h13.714C15.488 2 16 2.448 16 3s-.512 1-1.143 1H1.143C.512 4 0 3.552 0 3s.512-1 1.143-1zm0 5h13.714C15.488 7 16 7.448 16 8s-.512 1-1.143 1H1.143C.512 9 0 8.552 0 8s.512-1 1.143-1zm0 5h13.714c.631 0 1.143.448 1.143 1s-.512 1-1.143 1H1.143C.512 14 0 13.552 0 13s.512-1 1.143-1z"/></symbol><symbol viewBox="0 0 16 16" id="merge-request-close" xmlns="http://www.w3.org/2000/svg"><path d="M9.414 8l1.414 1.414a1 1 0 1 1-1.414 1.414L8 9.414l-1.414 1.414a1 1 0 1 1-1.414-1.414L6.586 8 5.172 6.586a1 1 0 1 1 1.414-1.414L8 6.586l1.414-1.414a1 1 0 1 1 1.414 1.414L9.414 8zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="messages" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.588 8.942l1.173 5.862A1 1 0 0 1 8.78 16H7.22a1 1 0 0 1-.98-1.196l1.172-5.862a3.014 3.014 0 0 0 1.176 0zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4zM4.464 2.464L5.88 3.88a3 3 0 0 0 0 4.242L4.464 9.536a5 5 0 0 1 0-7.072zm7.072 7.072L10.12 8.12a3 3 0 0 0 0-4.242l1.415-1.415a5 5 0 0 1 0 7.072zM2.343.343l1.414 1.414a6 6 0 0 0 0 8.486l-1.414 1.414a8 8 0 0 1 0-11.314zm11.314 11.314l-1.414-1.414a6 6 0 0 0 0-8.486L13.657.343a8 8 0 0 1 0 11.314z"/></symbol><symbol viewBox="0 0 16 16" id="mobile-issue-close" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.657 10.728L2.12 7.192A1 1 0 1 0 .707 8.607l4.243 4.242a.997.997 0 0 0 1.414 0l8.485-8.485a1 1 0 1 0-1.414-1.414l-7.778 7.778z"/></symbol><symbol viewBox="0 0 16 16" id="monitor" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 13v1h3a1 1 0 0 1 0 2H3a1 1 0 0 1 0-2h3v-1H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3h-3zM3 2a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3zm5.723 6.416l-2.66-1.773-1.71 1.71a.5.5 0 1 1-.707-.707l2-2a.5.5 0 0 1 .631-.062l2.66 1.773 2.71-2.71a.5.5 0 0 1 .707.707l-3 3a.5.5 0 0 1-.631.062z"/></symbol><symbol viewBox="0 0 16 16" id="more" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 4a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 6a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 6a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="notifications" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6 14H2.435a2 2 0 0 1-1.761-2.947c.962-1.788 1.521-3.065 1.68-3.832.322-1.566.947-5.501 4.65-6.134a1 1 0 1 1 1.994-.024c3.755.528 4.375 4.27 4.761 6.043.188.86.742 2.188 1.661 3.982A2 2 0 0 1 13.64 14H10a2 2 0 1 1-4 0zm5.805-6.468c-.325-1.492-.37-1.674-.61-2.288C10.6 3.716 9.742 3 8.07 3c-1.608 0-2.49.718-3.103 2.197-.28.676-.356.982-.654 2.428-.208 1.012-.827 2.424-1.877 4.375H13.64c-.993-1.937-1.6-3.396-1.835-4.468z"/></symbol><symbol viewBox="0 0 16 16" id="notifications-off" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.26 5.089c.243.757.382 1.478.5 2.017.187.86.74 2.188 1.66 3.982A2 2 0 0 1 13.64 14H10a2 2 0 1 1-4 0H4.35l2-2h7.29c-.993-1.937-1.6-3.396-1.835-4.468-.07-.326-.129-.59-.178-.81l1.634-1.633zM10.943 1.75l-1.48 1.48C9.07 3.076 8.612 3 8.069 3c-1.608 0-2.49.718-3.103 2.197-.28.676-.356.982-.654 2.428-.065.317-.17.673-.317 1.073L.45 12.242a1.99 1.99 0 0 1 .224-1.19c.962-1.787 1.521-3.064 1.68-3.831.322-1.566.947-5.501 4.65-6.134a1 1 0 1 1 1.994-.024 4.867 4.867 0 0 1 1.944.688zm2.932-.105a1 1 0 0 1 0 1.415L2.561 14.374a1 1 0 1 1-1.415-1.414L12.46 1.646a1 1 0 0 1 1.414 0z"/></symbol><symbol viewBox="0 0 16 16" id="overview" xmlns="http://www.w3.org/2000/svg"><path d="M2 0h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm0 2v3h3V2H2zm9-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm0 2v3h3V2h-3zM2 9h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2zm0 2v3h3v-3H2zm9-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2zm0 2v3h3v-3h-3z"/></symbol><symbol viewBox="0 0 16 16" id="pencil" xmlns="http://www.w3.org/2000/svg"><path d="M13.02 1.293l1.414 1.414a1 1 0 0 1 0 1.414L4.119 14.436a1 1 0 0 1-.704.293l-2.407.008L1 12.316a1 1 0 0 1 .293-.71L11.605 1.292a1 1 0 0 1 1.414 0zm-1.416 1.415l-.707.707L12.31 4.83l.707-.707-1.414-1.415zM3.411 13.73l1.123-1.122H3.12v-1.415L2 12.312l.005 1.422 1.406-.005z"/></symbol><symbol viewBox="0 0 16 16" id="pencil-square" xmlns="http://www.w3.org/2000/svg"><path d="M12 9a1 1 0 0 1 2 0v4a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h4a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V9zm.778-7.179l1.414 1.415-6.476 6.476a1 1 0 0 1-.498.27l-1.51.325.323-1.512a1 1 0 0 1 .27-.497l6.477-6.477zM15.607.407a1 1 0 0 1 0 1.414l-.708.707-1.414-1.414.707-.707a1 1 0 0 1 1.415 0z"/></symbol><symbol viewBox="0 0 16 16" id="pipeline" xmlns="http://www.w3.org/2000/svg"><path d="M8.969 7.25a2 2 0 1 1-1.938 0A1.002 1.002 0 0 1 7 7V5.083a.2.2 0 0 1 .06-.142l.877-.87a.1.1 0 0 1 .141 0l.864.87A.2.2 0 0 1 9 5.083V7c0 .086-.01.17-.031.25zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm4.5-4a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-2 6a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-5 9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-2 6a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zM8 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="play" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2.765 15.835c-.545.321-1.258.159-1.593-.363A1.075 1.075 0 0 1 1 14.89V1.11C1 .496 1.518 0 2.158 0c.214 0 .424.057.607.165l11.684 6.89c.544.321.714 1.005.38 1.526a1.135 1.135 0 0 1-.38.364l-11.684 6.89z"/></symbol><symbol viewBox="0 0 16 16" id="plus" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7 7H2a1 1 0 1 0 0 2h5v5a1 1 0 0 0 2 0V9h5a1 1 0 0 0 0-2H9V2a1 1 0 1 0-2 0v5z"/></symbol><symbol viewBox="0 0 16 16" id="plus-square" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 7V4a1 1 0 1 0-2 0v3H4a1 1 0 1 0 0 2h3v3a1 1 0 0 0 2 0V9h3a1 1 0 0 0 0-2H9zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3z"/></symbol><symbol viewBox="0 0 16 16" id="plus-square-o" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7 7V5a1 1 0 1 1 2 0v2h2a1 1 0 0 1 0 2H9v2a1 1 0 0 1-2 0V9H5a1 1 0 1 1 0-2h2zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="podcast" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.588 8.942l1.173 5.862a1 1 0 0 1-.785 1.177A1 1 0 0 1 8.78 16H7.22a1 1 0 0 1-1-1 1 1 0 0 1 .02-.196l1.172-5.862a3.014 3.014 0 0 0 1.176 0zM8 7.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zM4.464 2.464A1 1 0 0 1 5.88 3.88a3 3 0 0 0 0 4.242 1 1 0 0 1-1.415 1.415 5 5 0 0 1 0-7.072zm7.072 7.072A1 1 0 0 1 10.12 8.12a3 3 0 0 0 0-4.242 1 1 0 0 1 1.415-1.415 5 5 0 0 1 0 7.072zM2.343.343a1 1 0 1 1 1.414 1.414 6 6 0 0 0 0 8.486 1 1 0 1 1-1.414 1.414 8 8 0 0 1 0-11.314zm11.314 11.314a1 1 0 1 1-1.414-1.414 6 6 0 0 0 0-8.486A1 1 0 0 1 13.657.343a8 8 0 0 1 0 11.314z"/></symbol><symbol viewBox="0 0 16 16" id="preferences" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5 12h10a1 1 0 0 1 0 2H5a1 1 0 0 1-2 0v-2a1 1 0 0 1 2 0zm-3 0H1a1 1 0 0 0 0 2h1v-2zm11-5h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-2 0V7a1 1 0 0 1 2 0zm-3 0H1a1 1 0 1 0 0 2h9V7zM6 2h9a1 1 0 0 1 0 2H6a1 1 0 1 1-2 0V2a1 1 0 1 1 2 0zM3 2H1a1 1 0 1 0 0 2h2V2z"/></symbol><symbol viewBox="0 0 16 16" id="profile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-4.274-3.404C4.412 9.709 5.694 9 8 9c2.313 0 3.595.7 4.28 1.586A4.997 4.997 0 0 1 8 13a4.997 4.997 0 0 1-4.274-2.404zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="project" xmlns="http://www.w3.org/2000/svg"><path d="M8.462 2.177l-.038.044a.505.505 0 0 0 .038-.044zm-.787 0a.5.5 0 0 0 .038.043l-.038-.043zM3.706 7h8.725L8.069 2.585 3.706 7zM7 13.369V12a1 1 0 0 1 2 0v1.369h3V9H4v4.369h3zM14 9v4.836c0 .833-.657 1.533-1.5 1.533h-9c-.843 0-1.5-.7-1.5-1.533V9h-.448a1.1 1.1 0 0 1-.783-1.873L6.934.887a1.5 1.5 0 0 1 2.269 0l6.165 6.24A1.1 1.1 0 0 1 14.585 9H14z"/></symbol><symbol viewBox="0 0 16 16" id="push-rules" xmlns="http://www.w3.org/2000/svg"><path d="M6.268 9a2 2 0 0 1 3.464 0H11a1 1 0 0 1 0 2H9.732a2 2 0 0 1-3.464 0H5a1 1 0 0 1 0-2h1.268zM7 2H4a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1h-1v3.515a.3.3 0 0 1-.434.268l-1.432-.716a.3.3 0 0 0-.268 0l-1.432.716A.3.3 0 0 1 7 5.515V2zM4 0h8a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm4 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="question" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm-1.46-5.602h2.233a3.97 3.97 0 0 1 .051-.558c.029-.17.073-.326.133-.469.06-.143.14-.28.242-.41.102-.13.228-.263.38-.399.26-.24.504-.467.733-.683a5.03 5.03 0 0 0 .598-.668c.17-.23.302-.477.399-.742a2.66 2.66 0 0 0 .144-.907c0-.505-.083-.95-.25-1.335a2.55 2.55 0 0 0-.723-.97 3.2 3.2 0 0 0-1.152-.589 5.441 5.441 0 0 0-1.531-.2c-.516 0-.998.063-1.445.188a3.19 3.19 0 0 0-1.168.59c-.331.268-.594.61-.79 1.027-.195.417-.295.917-.3 1.5h2.64c.006-.224.04-.416.102-.578.062-.161.142-.293.238-.394a.921.921 0 0 1 .332-.227 1.04 1.04 0 0 1 .39-.074c.34 0 .593.095.763.285.169.19.254.488.254.895 0 .328-.106.63-.317.906-.21.276-.499.565-.863.867-.214.182-.39.374-.531.574-.141.2-.253.42-.336.657a3.656 3.656 0 0 0-.176.777 7.89 7.89 0 0 0-.05.937zm-.321 2.375c0 .188.035.362.105.524.07.161.17.3.301.418.13.117.284.21.46.277.178.068.376.102.595.102.218 0 .416-.034.593-.102.178-.068.331-.16.461-.277a1.2 1.2 0 0 0 .301-.418c.07-.162.106-.336.106-.524a1.3 1.3 0 0 0-.106-.523 1.2 1.2 0 0 0-.3-.418 1.461 1.461 0 0 0-.462-.277 1.651 1.651 0 0 0-.593-.102c-.22 0-.417.034-.594.102a1.46 1.46 0 0 0-.461.277 1.2 1.2 0 0 0-.3.418 1.284 1.284 0 0 0-.106.523z"/></symbol><symbol viewBox="0 0 16 16" id="question-o" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-.778-4.151c0-.301.014-.575.044-.82a3.2 3.2 0 0 1 .154-.68c.073-.208.17-.4.294-.575.123-.176.278-.343.465-.503a4.81 4.81 0 0 0 .755-.758c.185-.242.277-.506.277-.793 0-.356-.074-.617-.222-.783-.148-.166-.37-.25-.667-.25a.92.92 0 0 0-.342.065.806.806 0 0 0-.29.199 1.04 1.04 0 0 0-.209.345 1.5 1.5 0 0 0-.088.506H5.082c.005-.51.092-.948.263-1.313.171-.364.401-.664.69-.899.29-.234.63-.406 1.023-.516a4.66 4.66 0 0 1 1.264-.164c.497 0 .944.058 1.34.174.397.117.733.289 1.008.517.276.227.487.51.633.847.146.337.218.727.218 1.17 0 .295-.042.56-.126.792a2.52 2.52 0 0 1-.349.65 4.4 4.4 0 0 1-.523.584c-.2.19-.414.389-.642.598a2.73 2.73 0 0 0-.332.349c-.089.114-.16.233-.212.359a1.868 1.868 0 0 0-.116.41 3.39 3.39 0 0 0-.044.489H7.222zm-.28 2.078c0-.164.03-.317.092-.458a1.05 1.05 0 0 1 .263-.366c.114-.103.248-.183.403-.243a1.45 1.45 0 0 1 .52-.089c.191 0 .364.03.52.09.154.059.289.14.403.242.114.103.201.224.263.366.061.141.092.294.092.458 0 .164-.03.316-.092.458a1.05 1.05 0 0 1-.263.365 1.278 1.278 0 0 1-.404.243 1.43 1.43 0 0 1-.52.089c-.19 0-.364-.03-.519-.089-.155-.06-.29-.14-.403-.243a1.05 1.05 0 0 1-.263-.365 1.135 1.135 0 0 1-.093-.458z"/></symbol><symbol viewBox="0 0 16 16" id="quote" xmlns="http://www.w3.org/2000/svg"><path d="M15 3v8a3 3 0 0 1-3 3 1 1 0 0 1 0-2 1 1 0 0 0 1-1V9h-2a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h3a1 1 0 0 1 1 1zM7 3v8a3 3 0 0 1-3 3 1 1 0 0 1 0-2 1 1 0 0 0 1-1V9H3a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h3a1 1 0 0 1 1 1z"/></symbol><symbol viewBox="0 0 16 16" id="redo" xmlns="http://www.w3.org/2000/svg"><path d="M4.625 4.423A4.897 4.897 0 0 1 8.079 3c2.73 0 4.944 2.239 4.944 5s-2.214 5-4.944 5c-1.41 0-2.723-.6-3.655-1.633a.98.98 0 0 0-1.397-.066 1.008 1.008 0 0 0-.064 1.413A6.87 6.87 0 0 0 8.079 15C11.9 15 15 11.866 15 8s-3.099-7-6.921-7A6.866 6.866 0 0 0 3.08 3.158L1.833 2.137a.49.49 0 0 0-.695.074.504.504 0 0 0-.11.311L1 7.26a.497.497 0 0 0 .6.492l4.576-1.013a.5.5 0 0 0 .206-.877L4.625 4.423z"/></symbol><symbol viewBox="0 0 16 16" id="remove" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 3a1 1 0 1 1 0-2h12a1 1 0 0 1 0 2v10a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V3zm3-2a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1H5zM4 3v10a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V3H4zm2.5 2a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5zm3 0a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5z"/></symbol><symbol viewBox="0 0 16 16" id="repeat" xmlns="http://www.w3.org/2000/svg"><path d="M11.375 4.423A4.897 4.897 0 0 0 7.921 3c-2.73 0-4.944 2.239-4.944 5s2.214 5 4.944 5c1.41 0 2.723-.6 3.655-1.633a.98.98 0 0 1 1.397-.066c.403.373.432 1.005.064 1.413A6.87 6.87 0 0 1 7.921 15C4.1 15 1 11.866 1 8s3.099-7 6.921-7c1.915 0 3.706.792 4.999 2.158l1.247-1.021a.49.49 0 0 1 .695.074c.07.088.11.198.11.311L15 7.26a.497.497 0 0 1-.6.492L9.824 6.739a.5.5 0 0 1-.206-.877l1.757-1.439z"/></symbol><symbol viewBox="0 0 16 16" id="retry" xmlns="http://www.w3.org/2000/svg"><path d="M4.114 6.958a4 4 0 0 0 5.283 4.775 1 1 0 1 1 .712 1.87A6 6 0 0 1 2.182 6.44l-.741-.2a.5.5 0 0 1-.12-.915l2.195-1.268a.5.5 0 0 1 .683.183l1.268 2.196a.5.5 0 0 1-.563.733l-.79-.212zm7.777 2.084a4 4 0 0 0-5.284-4.775 1 1 0 0 1-.712-1.87 6 6 0 0 1 7.927 7.162l.742.2a.5.5 0 0 1 .12.915l-2.196 1.268a.5.5 0 0 1-.683-.183l-1.267-2.196a.5.5 0 0 1 .562-.733l.79.212z"/></symbol><symbol viewBox="0 0 16 16" id="scale" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.99 9a.792.792 0 0 0-.078-.231L13 7l-.912 1.769a.791.791 0 0 0-.077.231h1.978zm-10 0a.792.792 0 0 0-.078-.231L3 7l-.912 1.769A.791.791 0 0 0 2.011 9h1.978zM2 0h12a1 1 0 0 1 0 2H2a1 1 0 1 1 0-2zm3 14h6a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2zM8 4a1 1 0 0 1 1 1v9H7V5a1 1 0 0 1 1-1zm-4.53-.714l2.265 4.735c.68 1.42.006 3.091-1.504 3.73A3.161 3.161 0 0 1 3 12c-1.657 0-3-1.263-3-2.821 0-.4.09-.794.264-1.158L2.53 3.286a.53.53 0 0 1 .94 0zm10 0l2.265 4.735c.68 1.42.006 3.091-1.504 3.73A3.161 3.161 0 0 1 13 12c-1.657 0-3-1.263-3-2.821 0-.4.09-.794.264-1.158l2.266-4.735a.53.53 0 0 1 .94 0z"/></symbol><symbol viewBox="0 0 16 16" id="screen-full" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M14 14v-2a1 1 0 0 1 2 0v3a.997.997 0 0 1-1 1h-3a1 1 0 0 1 0-2h2zM2 14v-2a1 1 0 0 0-2 0v3a1 1 0 0 0 1 1h3a1 1 0 0 0 0-2H2zM15.707.293A.997.997 0 0 1 16 1v3a1 1 0 0 1-2 0V2h-2a1 1 0 0 1 0-2h3c.276 0 .526.112.707.293zM2 2v2a1 1 0 1 1-2 0V1a.997.997 0 0 1 1-1h3a1 1 0 1 1 0 2H2zm4 4h4a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="screen-normal" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3 3V1a1 1 0 1 1 2 0v3a.997.997 0 0 1-1 1H1a1 1 0 1 1 0-2h2zm10 0h2a1 1 0 0 1 0 2h-3a.997.997 0 0 1-1-1V1a1 1 0 0 1 2 0v2zM3 13H1a1 1 0 0 1 0-2h3a.997.997 0 0 1 1 1v3a1 1 0 0 1-2 0v-2zm10 0v2a1 1 0 0 1-2 0v-3a.997.997 0 0 1 1-1h3a1 1 0 0 1 0 2h-2zM6.5 7h3a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5z"/></symbol><symbol viewBox="0 0 12 16" id="scroll_down" xmlns="http://www.w3.org/2000/svg"><path class="fbfirst-triangle" d="M1.048 14.155a.508.508 0 0 0-.32.105c-.091.07-.136.154-.136.25v.71c0 .095.045.178.135.249.09.07.197.105.321.105h10.043a.51.51 0 0 0 .321-.105c.09-.07.136-.154.136-.25v-.71c0-.095-.045-.178-.136-.249a.508.508 0 0 0-.32-.105"/><path class="fbsecond-triangle" d="M.687 8.027c-.09-.087-.122-.16-.093-.22.028-.06.104-.09.228-.09h10.5c.123 0 .2.03.228.09.029.06-.002.133-.093.22L6.393 12.91a.458.458 0 0 1-.136.089h-.37a.626.626 0 0 1-.136-.09"/><path class="fbthird-triangle" d="M.687 1.027C.597.94.565.867.594.807c.028-.06.104-.09.228-.09h10.5c.123 0 .2.03.228.09.029.06-.002.133-.093.22L6.393 5.91a.458.458 0 0 1-.136.09h-.37a.626.626 0 0 1-.136-.09"/></symbol><symbol viewBox="0 0 12 16" id="scroll_up" xmlns="http://www.w3.org/2000/svg"><path d="M1.048 1.845a.508.508 0 0 1-.32-.105c-.091-.07-.136-.154-.136-.25V.78c0-.095.045-.178.135-.249a.508.508 0 0 1 .321-.105h10.043a.51.51 0 0 1 .321.105c.09.07.136.154.136.25v.71c0 .095-.045.178-.136.249a.508.508 0 0 1-.32.105M.687 7.973c-.09.087-.122.16-.093.22.028.06.104.09.228.09h10.5c.123 0 .2-.03.228-.09.029-.06-.002-.133-.093-.22L6.393 3.09A.458.458 0 0 0 6.257 3h-.37a.626.626 0 0 0-.136.09M.687 14.973c-.09.087-.122.16-.093.22.028.06.104.09.228.09h10.5c.123 0 .2-.03.228-.09.029-.06-.002-.133-.093-.22L6.393 10.09a.458.458 0 0 0-.136-.09h-.37a.626.626 0 0 0-.136.09"/></symbol><symbol viewBox="0 0 16 16" id="search" xmlns="http://www.w3.org/2000/svg"><path d="M8.853 8.854a3.5 3.5 0 1 0-4.95-4.95 3.5 3.5 0 0 0 4.95 4.95zm.207 2.328a5.5 5.5 0 1 1 2.121-2.121l3.329 3.328a1.5 1.5 0 0 1-2.121 2.121L9.06 11.182z"/></symbol><symbol viewBox="0 0 16 16" id="settings" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2.415 5.803L1.317 4.084A.5.5 0 0 1 1.35 3.5l.805-.994a.5.5 0 0 1 .564-.153l1.878.704a5.975 5.975 0 0 1 1.65-.797L6.885.342A.5.5 0 0 1 7.36 0h1.28a.5.5 0 0 1 .474.342l.639 1.918a5.97 5.97 0 0 1 1.65.797l1.877-.704a.5.5 0 0 1 .565.153l.805.994a.5.5 0 0 1 .032.584l-1.097 1.719c.217.551.354 1.143.399 1.76l1.731 1.058a.5.5 0 0 1 .227.54l-.288 1.246a.5.5 0 0 1-.44.385l-2.008.19a6.026 6.026 0 0 1-1.142 1.431l.265 1.995a.5.5 0 0 1-.277.516l-1.15.56a.5.5 0 0 1-.576-.1l-1.424-1.452a6.047 6.047 0 0 1-1.804 0l-1.425 1.453a.5.5 0 0 1-.576.1l-1.15-.561a.5.5 0 0 1-.276-.516l.265-1.995a6.026 6.026 0 0 1-1.143-1.43l-2.008-.191a.5.5 0 0 1-.44-.385L.058 9.16a.5.5 0 0 1 .226-.539l1.732-1.058a5.968 5.968 0 0 1 .399-1.76zM8 11a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="shield" xmlns="http://www.w3.org/2000/svg"><path d="M4 0h8c1.657 0 3 1.373 3 3.067v7.346c0 1.065-.54 2.053-1.426 2.611l-4 2.52a2.944 2.944 0 0 1-3.148 0l-4-2.52A3.083 3.083 0 0 1 1 10.414V3.066C1 1.373 2.343 0 4 0zm0 2.045c-.552 0-1 .457-1 1.022v7.346c0 .355.18.685.475.87l4 2.52a.981.981 0 0 0 1.05 0l4-2.52c.295-.185.475-.515.475-.87V3.067c0-.565-.448-1.022-1-1.022H4zm0 1.533c0-.282.224-.511.5-.511h4V12.1a.52.52 0 0 1-.069.258.494.494 0 0 1-.684.183l-3.5-2.098a.513.513 0 0 1-.247-.44V3.577z"/></symbol><symbol viewBox="0 0 16 16" id="slight-frown" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-2.163-3.275a2.499 2.499 0 0 1 4.343.03.5.5 0 0 1-.871.49 1.5 1.5 0 0 0-2.607-.018.5.5 0 1 1-.865-.502zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="slight-smile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm-5.163 2.254a.5.5 0 1 1 .865-.502 1.499 1.499 0 0 0 2.607-.018.5.5 0 1 1 .871.49 2.499 2.499 0 0 1-4.343.03z"/></symbol><symbol viewBox="0 0 16 16" id="smile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM6.18 6.27a.5.5 0 0 1-.873.487.5.5 0 0 0-.872-.003.5.5 0 1 1-.87-.495 1.5 1.5 0 0 1 2.616.012zm6 0a.5.5 0 1 1-.873.487.5.5 0 0 0-.872-.003.5.5 0 1 1-.87-.495 1.5 1.5 0 0 1 2.616.012zM5 9a3 3 0 0 0 6 0H5z"/></symbol><symbol viewBox="0 0 16 16" id="smiley" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM5 9h6a3 3 0 0 1-6 0z"/></symbol><symbol viewBox="0 0 16 16" id="snippet" xmlns="http://www.w3.org/2000/svg"><path d="M10.67 9.31a3.001 3.001 0 0 1 2.062 5.546 3 3 0 0 1-3.771-4.559 1.007 1.007 0 0 1-.095-.137l-4.5-7.794a1 1 0 0 1 1.732-1l4.5 7.794c.028.05.052.1.071.15zm-3.283.35l-.289.5c-.028.05-.06.095-.095.137a3.001 3.001 0 0 1-3.77 4.56A3 3 0 0 1 5.294 9.31c.02-.051.043-.102.071-.15l.866-1.5 1.155 2zm2.31-4l-1.156-2 1.325-2.294a1 1 0 0 1 1.732 1L9.696 5.66zm-5.465 7.464a1 1 0 1 0 1-1.732 1 1 0 0 0-1 1.732zm7.5 0a1 1 0 1 0-1-1.732 1 1 0 0 0 1 1.732z"/></symbol><symbol viewBox="0 0 16 16" id="soft-unwrap" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6.5 11v-.598a.5.5 0 0 1 .765-.424l2.557 1.598a.5.5 0 0 1 0 .848l-2.557 1.598a.5.5 0 0 1-.765-.424V13H2a1 1 0 0 1 0-2h4.5zM2 3h12a1 1 0 0 1 0 2H2a1 1 0 1 1 0-2zm0 4h12a1 1 0 0 1 0 2H2a1 1 0 1 1 0-2zm10 4h2a1 1 0 0 1 0 2h-2a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="soft-wrap" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.5 13v.598a.5.5 0 0 1-.765.424l-2.557-1.598a.5.5 0 0 1 0-.848l2.557-1.598a.5.5 0 0 1 .765.424V11H12a1 1 0 0 0 0-2H2a1 1 0 1 1 0-2h10a3 3 0 0 1 0 6h-1.5zM2 3h12a1 1 0 0 1 0 2H2a1 1 0 1 1 0-2zm0 8h3a1 1 0 0 1 0 2H2a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="spam" xmlns="http://www.w3.org/2000/svg"><path d="M8.75.433l5.428 3.134a1.5 1.5 0 0 1 .75 1.299v6.268a1.5 1.5 0 0 1-.75 1.299L8.75 15.567a1.5 1.5 0 0 1-1.5 0l-5.428-3.134a1.5 1.5 0 0 1-.75-1.299V4.866a1.5 1.5 0 0 1 .75-1.299L7.25.433a1.5 1.5 0 0 1 1.5 0zM3.072 5.155v5.69L8 13.691l4.928-2.846v-5.69L8 2.309 3.072 5.155zM8 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V5a1 1 0 0 1 1-1zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 14 14" id="spinner" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><circle cx="7" cy="7" r="6" stroke="#000" stroke-opacity=".1" stroke-width="2"/><path fill="#000" fill-opacity=".1" fill-rule="nonzero" d="M7 0a7 7 0 0 1 7 7h-2a5 5 0 0 0-5-5V0z"/></g></symbol><symbol viewBox="0 0 16 16" id="staged" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 3h4a1 1 0 1 1 0 2H2a1 1 0 1 1 0-2zm9 6a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM2 7h4a1 1 0 1 1 0 2H2a1 1 0 1 1 0-2zm0 4h12a1 1 0 0 1 0 2H2a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="star" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.609 14.394l-3.465 1.473a1 1 0 0 1-1.39-.989l.276-4.024a1 1 0 0 0-.219-.694L.303 7.037A1 1 0 0 1 .83 5.443l3.715-.964a1 1 0 0 0 .609-.457L7.14.682a1 1 0 0 1 1.72 0l1.985 3.34a1 1 0 0 0 .609.457l3.715.964a1 1 0 0 1 .528 1.594L13.19 10.16a1 1 0 0 0-.219.694l.275 4.024a1 1 0 0 1-1.389.989l-3.465-1.473a1 1 0 0 0-.782 0z"/></symbol><symbol viewBox="0 0 16 16" id="star-o" xmlns="http://www.w3.org/2000/svg"><path d="M10.975 10.99a3 3 0 0 1 .655-2.083l1.54-1.916-2.219-.576a3 3 0 0 1-1.825-1.37L8 3.15 6.874 5.044a3 3 0 0 1-1.825 1.371l-2.218.576 1.54 1.916a3 3 0 0 1 .654 2.083l-.165 2.4 1.965-.836a3 3 0 0 1 2.348 0l1.965.836-.164-2.399zM7.61 14.394l-3.465 1.473a1 1 0 0 1-1.39-.989l.276-4.024a1 1 0 0 0-.219-.694L.303 7.037A1 1 0 0 1 .83 5.443l3.715-.964a1 1 0 0 0 .609-.457L7.14.682a1 1 0 0 1 1.72 0l1.985 3.34a1 1 0 0 0 .609.457l3.715.964a1 1 0 0 1 .528 1.594L13.19 10.16a1 1 0 0 0-.219.694l.275 4.024a1 1 0 0 1-1.389.989l-3.465-1.473a1 1 0 0 0-.782 0z"/></symbol><symbol viewBox="0 0 14 14" id="status_canceled" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M5.2 3.8l4.9 4.9c.2.2.2.5 0 .7l-.7.7c-.2.2-.5.2-.7 0L3.8 5.2c-.2-.2-.2-.5 0-.7l.7-.7c.2-.2.5-.2.7 0"/></g></symbol><symbol viewBox="0 0 22 22" id="status_canceled_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M8.171 5.971l7.7 7.7a.76.76 0 0 1 0 1.1l-1.1 1.1a.76.76 0 0 1-1.1 0l-7.7-7.7a.76.76 0 0 1 0-1.1l1.1-1.1a.76.76 0 0 1 1.1 0"/></symbol><symbol viewBox="0 0 16 16" id="status_closed" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.83a1 1 0 0 1 1.414 1.416l-3.535 3.535a1 1 0 0 1-1.415.001l-2.12-2.12a1 1 0 1 1 1.413-1.415zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 14 14" id="status_created" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><circle cx="7" cy="7" r="3.25"/></g></symbol><symbol viewBox="0 0 22 22" id="status_created_borderless" xmlns="http://www.w3.org/2000/svg"><circle cx="11" cy="11" r="5.107"/></symbol><symbol viewBox="0 0 14 14" id="status_failed" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 5.969L5.599 4.568a.29.29 0 0 0-.413.004l-.614.614a.294.294 0 0 0-.004.413L5.968 7l-1.4 1.401a.29.29 0 0 0 .004.413l.614.614c.113.114.3.117.413.004L7 8.032l1.401 1.4a.29.29 0 0 0 .413-.004l.614-.614a.294.294 0 0 0 .004-.413L8.032 7l1.4-1.401a.29.29 0 0 0-.004-.413l-.614-.614a.294.294 0 0 0-.413-.004L7 5.968z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_failed_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M11 9.38L8.798 7.178a.455.455 0 0 0-.65.006l-.964.965a.462.462 0 0 0-.006.65L9.38 11l-2.202 2.202a.455.455 0 0 0 .006.65l.965.964a.462.462 0 0 0 .65.006L11 12.62l2.202 2.202a.455.455 0 0 0 .65-.006l.964-.965a.462.462 0 0 0 .006-.65L12.62 11l2.202-2.202a.455.455 0 0 0-.006-.65l-.965-.964a.462.462 0 0 0-.65-.006L11 9.38z"/></symbol><symbol viewBox="0 0 14 14" id="status_manual" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M10.5 7.63V6.37l-.787-.13c-.044-.175-.132-.349-.263-.61l.481-.652-.918-.913-.657.478a2.346 2.346 0 0 0-.612-.26L7.656 3.5H6.388l-.132.783c-.219.043-.394.13-.612.26l-.657-.478-.918.913.437.652c-.131.218-.175.392-.262.61l-.744.086v1.261l.787.13c.044.218.132.392.263.61l-.438.651.92.913.655-.434c.175.086.394.173.613.26l.131.783h1.313l.131-.783c.219-.043.394-.13.613-.26l.656.478.918-.913-.48-.652c.13-.218.218-.435.262-.61l.656-.13zM7 8.283a1.285 1.285 0 0 1-1.313-1.305c0-.739.57-1.304 1.313-1.304.744 0 1.313.565 1.313 1.304 0 .74-.57 1.305-1.313 1.305z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_manual_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M16.5 11.99v-1.98l-1.238-.206c-.068-.273-.206-.546-.412-.956l.756-1.025-1.444-1.435-1.03.752a3.686 3.686 0 0 0-.963-.41L12.03 5.5h-1.994l-.206 1.23c-.343.068-.618.205-.962.41l-1.031-.752-1.444 1.435.687 1.025c-.206.341-.275.615-.412.956L5.5 9.941v1.981l1.237.205c.07.342.207.615.413.957l-.688 1.025 1.444 1.434 1.032-.683c.274.137.618.274.962.41l.206 1.23h2.063l.206-1.23c.344-.068.619-.205.963-.41l1.03.752 1.444-1.435-.756-1.025c.207-.341.344-.683.413-.956l1.031-.205zM11 13.017c-1.169 0-2.063-.889-2.063-2.05 0-1.162.894-2.05 2.063-2.05s2.063.888 2.063 2.05c0 1.161-.894 2.05-2.063 2.05z"/></symbol><symbol viewBox="0 0 14 14" id="status_notfound" xmlns="http://www.w3.org/2000/svg"><path d="M7 14A7 7 0 1 1 7 0a7 7 0 0 1 0 14z"/><path d="M7 13A6 6 0 1 0 7 1a6 6 0 0 0 0 12z" fill="#FFF"/><path d="M8.16 7.184c.519-.37.904-.857 1.07-1.477.384-1.427-.619-2.897-2.246-2.897-.732 0-1.327.26-1.766.692a2.163 2.163 0 0 0-.509.743.75.75 0 0 0 1.4.54.78.78 0 0 1 .16-.213c.168-.165.39-.262.715-.262.597 0 .936.496.798 1.007-.067.249-.235.462-.492.644-.231.165-.47.264-.601.3a.75.75 0 0 0-.556.724v1.421a.75.75 0 0 0 1.5 0v-.909a3.74 3.74 0 0 0 .526-.313z"/><ellipse cx="6.889" cy="10.634" rx="1" ry="1"/></symbol><symbol viewBox="0 0 22 22" id="status_notfound_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M12.822 11.29c.816-.581 1.421-1.348 1.683-2.322.603-2.243-.973-4.553-3.53-4.553-1.15 0-2.085.41-2.775 1.089-.42.413-.672.835-.8 1.167a1.179 1.179 0 0 0 2.2.847c.016-.043.1-.184.252-.334.264-.259.613-.412 1.123-.412.938 0 1.47.78 1.254 1.584-.105.39-.37.726-.773 1.012a3.25 3.25 0 0 1-.945.47 1.179 1.179 0 0 0-.874 1.138v2.234a1.179 1.179 0 1 0 2.358 0v-1.43a5.9 5.9 0 0 0 .827-.492z"/><ellipse cx="10.825" cy="16.711" rx="1.275" ry="1.322"/></symbol><symbol viewBox="0 0 14 14" id="status_open" xmlns="http://www.w3.org/2000/svg"><path d="M0 7c0-3.866 3.142-7 7-7 3.866 0 7 3.142 7 7 0 3.866-3.142 7-7 7-3.866 0-7-3.142-7-7z"/><path d="M1 7c0 3.309 2.69 6 6 6 3.309 0 6-2.69 6-6 0-3.309-2.69-6-6-6-3.309 0-6 2.69-6 6z" fill="#FFF"/><path d="M7 9.219a2.218 2.218 0 1 0 0-4.436A2.218 2.218 0 0 0 7 9.22zm0 1.12a3.338 3.338 0 1 1 0-6.676 3.338 3.338 0 0 1 0 6.676z"/></symbol><symbol viewBox="0 0 14 14" id="status_pending" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M4.7 5.3c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H5c-.2 0-.3-.1-.3-.3V5.3m3 0c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H8c-.2 0-.3-.1-.3-.3V5.3"/></g></symbol><symbol viewBox="0 0 22 22" id="status_pending_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M7.386 8.329c0-.315.157-.472.471-.472h1.414c.315 0 .472.157.472.472v5.342c0 .315-.157.472-.472.472H7.857c-.314 0-.471-.157-.471-.472V8.33m4.714 0c0-.315.157-.472.471-.472h1.415c.314 0 .471.157.471.472v5.342c0 .315-.157.472-.471.472H12.57c-.314 0-.471-.157-.471-.472V8.33"/></symbol><symbol viewBox="0 0 14 14" id="status_running" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 3c2.2 0 4 1.8 4 4s-1.8 4-4 4c-1.3 0-2.5-.7-3.3-1.7L7 7V3"/></g></symbol><symbol viewBox="0 0 22 22" id="status_running_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M11 4.714c3.457 0 6.286 2.829 6.286 6.286 0 3.457-2.829 6.286-6.286 6.286-2.043 0-3.929-1.1-5.186-2.672L11 11V4.714"/></symbol><symbol viewBox="0 0 14 14" id="status_skipped" xmlns="http://www.w3.org/2000/svg"><path d="M7 14A7 7 0 1 1 7 0a7 7 0 0 1 0 14z"/><path d="M7 13A6 6 0 1 0 7 1a6 6 0 0 0 0 12z" fill="#FFF"/><path d="M6.415 7.04L4.579 5.203a.295.295 0 0 1 .004-.416l.349-.349a.29.29 0 0 1 .416-.004l2.214 2.214a.289.289 0 0 1 .019.021l.132.133c.11.11.108.291 0 .398L5.341 9.573a.282.282 0 0 1-.398 0l-.331-.331a.285.285 0 0 1 0-.399L6.415 7.04zm2.54 0L7.119 5.203a.295.295 0 0 1 .004-.416l.349-.349a.29.29 0 0 1 .416-.004l2.214 2.214a.289.289 0 0 1 .019.021l.132.133c.11.11.108.291 0 .398L7.881 9.573a.282.282 0 0 1-.398 0l-.331-.331a.285.285 0 0 1 0-.399L8.955 7.04z"/></symbol><symbol viewBox="0 0 22 22" id="status_skipped_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M14.072 11.063l-2.82 2.82a.46.46 0 0 0-.001.652l.495.495a.457.457 0 0 0 .653-.001l3.7-3.7a.46.46 0 0 0 .001-.653l-.196-.196a.453.453 0 0 0-.03-.033l-3.479-3.479a.464.464 0 0 0-.654.007l-.548.548a.463.463 0 0 0-.007.654l2.886 2.886z"/><path d="M10.08 11.063l-2.819 2.82a.46.46 0 0 0-.002.652l.496.495a.457.457 0 0 0 .652-.001l3.7-3.7a.46.46 0 0 0 .002-.653l-.196-.196a.453.453 0 0 0-.03-.033l-3.48-3.479a.464.464 0 0 0-.653.007l-.548.548a.463.463 0 0 0-.007.654l2.886 2.886z"/></symbol><symbol viewBox="0 0 14 14" id="status_success" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_success_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M9.866 12.095l-1.95-1.95a.462.462 0 0 0-.647.01l-.964.964a.46.46 0 0 0-.01.646l3.013 3.014a.787.787 0 0 0 1.106.008l.425-.425 4.854-4.853a.462.462 0 0 0 .002-.659l-.964-.964a.468.468 0 0 0-.658.002l-4.207 4.207z"/></symbol><symbol viewBox="0 0 14 14" id="status_success_solid" xmlns="http://www.w3.org/2000/svg"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7zm6.278.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 14 14" id="status_warning" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6 3.5c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v4c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-4m0 6c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v1c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-1"/></g></symbol><symbol viewBox="0 0 22 22" id="status_warning_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M9.429 5.5c0-.471.314-.786.785-.786h1.572c.471 0 .785.315.785.786v6.286c0 .471-.314.785-.785.785h-1.572c-.471 0-.785-.314-.785-.785V5.5m0 9.429c0-.472.314-.786.785-.786h1.572c.471 0 .785.314.785.786V16.5c0 .471-.314.786-.785.786h-1.572c-.471 0-.785-.315-.785-.786v-1.571"/></symbol><symbol viewBox="0 0 16 16" id="stop" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 0h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2z"/></symbol><symbol viewBox="0 0 16 16" id="task-done" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 0 1 6.12 7.243l1.415 1.414zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="template" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3zm.8 2h2.4a.8.8 0 0 1 .8.8v1.4a.8.8 0 0 1-.8.8H3.8a.8.8 0 0 1-.8-.8V4.8a.8.8 0 0 1 .8-.8zm4.7 0h4a.5.5 0 1 1 0 1h-4a.5.5 0 0 1 0-1zm0 2h4a.5.5 0 1 1 0 1h-4a.5.5 0 0 1 0-1zm-5 3h9a.5.5 0 1 1 0 1h-9a.5.5 0 0 1 0-1zm0 2h9a.5.5 0 1 1 0 1h-9a.5.5 0 1 1 0-1z"/></symbol><symbol viewBox="0 0 16 16" id="terminal" xmlns="http://www.w3.org/2000/svg"><path d="M7 8a.997.997 0 0 1-.293.707l-1.414 1.414a1 1 0 1 1-1.414-1.414L4.586 8l-.707-.707a1 1 0 1 1 1.414-1.414l1.414 1.414A.997.997 0 0 1 7 8zM4 0h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm0 2a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H4zm5 7h2a1 1 0 0 1 0 2H9a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="thumb-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.33 11h5.282a2 2 0 0 0 1.963-2.38l-.563-2.905a3 3 0 0 0-.243-.732l-1.103-2.286A3 3 0 0 0 10.964 1H7a3 3 0 0 0-3 3v6.3a2 2 0 0 0 .436 1.247l3.11 3.9a.632.632 0 0 0 .941.053l.137-.137a1 1 0 0 0 .28-.87L8.329 11zM1 10h2V3H1a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1z"/></symbol><symbol viewBox="0 0 16 16" id="thumb-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.33 5h5.282a2 2 0 0 1 1.963 2.38l-.563 2.905a3 3 0 0 1-.243.732l-1.103 2.286A3 3 0 0 1 10.964 15H7a3 3 0 0 1-3-3V5.7a2 2 0 0 1 .436-1.247l3.11-3.9A.632.632 0 0 1 8.487.5l.137.137a1 1 0 0 1 .28.87L8.329 5zM1 6h2v7H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="thumbtack" xmlns="http://www.w3.org/2000/svg"><path d="M7.125 9h-2.19a.5.5 0 0 1-.417-.777L6 6V2L5.362.724A.5.5 0 0 1 5.809 0h4.382a.5.5 0 0 1 .447.724L10 2v4l1.482 2.223a.5.5 0 0 1-.416.777H8.875L8 16l-.875-7z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 16 16" id="timer" xmlns="http://www.w3.org/2000/svg"><path d="M12.022 3.27l.77-.77a1 1 0 0 1 1.415 1.414l-.728.729a7 7 0 1 1-1.456-1.372zM8 14A5 5 0 1 0 8 4a5 5 0 0 0 0 10zm0-9a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V6a1 1 0 0 1 1-1zM6 0h4a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="todo-add" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 4V2a1 1 0 0 1 2 0v2h2a1 1 0 0 1 0 2h-2v2a1 1 0 0 1-2 0V6H8a1 1 0 1 1 0-2h2zm2 7a1 1 0 0 1 2 0v2a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h2a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-2z"/></symbol><symbol viewBox="0 0 16 16" id="todo-done" xmlns="http://www.w3.org/2000/svg"><path d="M8.243 7.485l4.95-4.95a1 1 0 1 1 1.414 1.415L8.95 9.607a.997.997 0 0 1-1.414 0L4.707 6.778a1 1 0 0 1 1.414-1.414l2.122 2.121zM12 11a1 1 0 0 1 2 0v2a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h2a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-2z"/></symbol><symbol viewBox="0 0 16 16" id="token" xmlns="http://www.w3.org/2000/svg"><path d="M3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H3zm1 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="unapproval" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M11.95 8.536l1.06-1.061a1 1 0 0 1 1.415 1.414l-1.061 1.06 1.06 1.061a1 1 0 0 1-1.414 1.415l-1.06-1.061-1.06 1.06a1 1 0 1 1-1.415-1.414l1.06-1.06-1.06-1.06a1 1 0 0 1 1.414-1.415l1.06 1.06zm-3.768-.33c.006.503.201 1.006.586 1.39l.353.354-.353.353a2 2 0 1 0 2.828 2.829l.354-.354.047.048C11.964 14.363 11.527 15 6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8c.834 0 1.557.074 2.182.205zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="unassignee" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M11 5h4a1 1 0 0 1 0 2h-4a1 1 0 0 1 0-2zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="unlink" xmlns="http://www.w3.org/2000/svg"><path d="M11.295 8.845l-.659-1.664a1.78 1.78 0 0 0 .04-.04l1.415-1.414c.586-.586.654-1.468.152-1.97s-1.384-.434-1.97.152L8.859 5.323a1.781 1.781 0 0 0-.04.04l-1.664-.658c.141-.208.305-.408.491-.594l1.415-1.414c1.366-1.367 3.424-1.525 4.596-.354 1.171 1.172 1.013 3.23-.354 4.596L11.89 8.354c-.186.186-.386.35-.594.491zm-2.45 2.45a4.075 4.075 0 0 1-.491.594l-1.415 1.414c-1.366 1.367-3.424 1.525-4.596.354-1.171-1.172-1.013-3.23.354-4.596L4.11 7.646c.186-.186.386-.35.594-.491l.659 1.664a1.781 1.781 0 0 0-.04.04l-1.415 1.414c-.586.586-.654 1.468-.152 1.97s1.384.434 1.97-.152l1.414-1.414a1.78 1.78 0 0 0 .04-.04l1.664.658zm3.812-2.088h2a.5.5 0 0 1 .5.5v.05a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-.05a.5.5 0 0 1 .5-.5zm-.384 2.116l1.415 1.414a.5.5 0 0 1 0 .708l-.037.036a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 0-.707l.036-.037a.5.5 0 0 1 .707 0zm-2.823 1.09a.5.5 0 0 1 .5-.5h.052a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5H9.95a.5.5 0 0 1-.5-.5v-2zm-2.748-9.16a.5.5 0 0 1-.5.5h-.05a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5h.05a.5.5 0 0 1 .5.5v2zm-2.116.383a.5.5 0 0 1 0 .707l-.036.036a.5.5 0 0 1-.707 0L2.428 2.965a.5.5 0 0 1 0-.707l.037-.036a.5.5 0 0 1 .707 0l1.414 1.414zm-1.09 2.823h-2a.5.5 0 0 1-.5-.5v-.051a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v.05a.5.5 0 0 1-.5.5z"/></symbol><symbol viewBox="0 0 16 16" id="unstaged" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 3h12a1 1 0 0 1 0 2H2a1 1 0 1 1 0-2zm0 4h12a1 1 0 0 1 0 2H2a1 1 0 1 1 0-2zm0 4h12a1 1 0 0 1 0 2H2a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="user" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 8c-6.888 0-6.976-.78-6.976-2.52S2.144 8 8 8s6.976 2.692 6.976 4.48c0 1.788-.088 2.52-6.976 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="users" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.521 8.01C15.103 8.19 16 10.755 16 12.48c0 1.533-.056 2.29-3.808 2.475.609-.54.808-1.331.808-2.475 0-1.911-.804-3.503-2.479-4.47zm-1.67-1.228A3.987 3.987 0 0 0 9.976 4a3.987 3.987 0 0 0-1.125-2.782 3 3 0 1 1 0 5.563zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="volume-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 5h1v6H1a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1zm2 0l4.445-2.964A1 1 0 0 1 9 2.87v10.26a1 1 0 0 1-1.555.833L3 11V5zm10.283 7.89a.5.5 0 0 1-.66-.752A5.485 5.485 0 0 0 14.5 8c0-1.601-.687-3.09-1.865-4.128a.5.5 0 0 1 .661-.75A6.484 6.484 0 0 1 15.5 8a6.485 6.485 0 0 1-2.217 4.89zm-2.002-2.236a.5.5 0 1 1-.652-.758c.55-.472.871-1.157.871-1.896 0-.732-.315-1.411-.856-1.883a.5.5 0 0 1 .658-.753A3.492 3.492 0 0 1 12.5 8c0 1.033-.45 1.994-1.219 2.654z"/></symbol><symbol viewBox="0 0 16 16" id="warning" xmlns="http://www.w3.org/2000/svg"><path d="M15.572 10.506c.867 1.42.375 3.247-1.098 4.082a3.184 3.184 0 0 1-1.57.412h-9.81C1.387 15 0 13.665 0 12.018a2.9 2.9 0 0 1 .427-1.512L5.332 2.47C6.2 1.05 8.096.577 9.57 1.412c.453.257.831.622 1.098 1.059l4.905 8.035zM8.89 3.479a1.014 1.014 0 0 0-.366-.353 1.053 1.053 0 0 0-1.412.353l-4.905 8.035a.967.967 0 0 0-.143.504c0 .549.462.994 1.032.994h9.81c.184 0 .364-.048.523-.137a.974.974 0 0 0 .366-1.361L8.889 3.479zM8 5a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V6a1 1 0 0 1 1-1zm0 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="work" xmlns="http://www.w3.org/2000/svg"><path d="M12 3h1a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h1V2a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1zM6 2v1h4V2H6zM3 5a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H3zm1.5 1a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5zm7 0a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5z"/></symbol></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/cluster_popover.svg b/app/assets/images/illustrations/cluster_popover.svg deleted file mode 100644 index 202231373f1..00000000000 --- a/app/assets/images/illustrations/cluster_popover.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="142" height="104" viewBox="0 0 142 104"><g fill="none" fill-rule="evenodd"><g transform="translate(112 4)"><path fill="#FFF" d="M8 4a4 4 0 0 0-4 4v14a4 4 0 0 0 4 4h14a4 4 0 0 0 4-4V8a4 4 0 0 0-4-4H8z"/><path fill="#FDC4A8" fill-rule="nonzero" d="M8 4a4 4 0 0 0-4 4v14a4 4 0 0 0 4 4h14a4 4 0 0 0 4-4V8a4 4 0 0 0-4-4H8zm0-4h14a8 8 0 0 1 8 8v14a8 8 0 0 1-8 8H8a8 8 0 0 1-8-8V8a8 8 0 0 1 8-8z"/><rect width="10" height="10" x="10" y="10" fill="#FC6D26" rx="5"/></g><g transform="translate(5 74)"><rect width="30" height="30" fill="#FFF" rx="8"/><path fill="#E1DBF1" fill-rule="nonzero" d="M8 4a4 4 0 0 0-4 4v14a4 4 0 0 0 4 4h14a4 4 0 0 0 4-4V8a4 4 0 0 0-4-4H8zm0-4h14a8 8 0 0 1 8 8v14a8 8 0 0 1-8 8H8a8 8 0 0 1-8-8V8a8 8 0 0 1 8-8z"/><rect width="10" height="10" x="10" y="10" fill="#6B4FBB" rx="5"/></g><path fill="#FFF" d="M6 4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H6z"/><path fill="#FDC4A8" fill-rule="nonzero" d="M6 4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H6zm0-4h12a6 6 0 0 1 6 6v12a6 6 0 0 1-6 6H6a6 6 0 0 1-6-6V6a6 6 0 0 1 6-6z"/><rect width="8" height="8" x="8" y="8" fill="#FC6D26" rx="4"/><g transform="translate(112 77)"><rect width="24" height="24" fill="#FFF" rx="6"/><path fill="#E1DBF1" fill-rule="nonzero" d="M6 4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H6zm0-4h12a6 6 0 0 1 6 6v12a6 6 0 0 1-6 6H6a6 6 0 0 1-6-6V6a6 6 0 0 1 6-6z"/><rect width="8" height="8" x="8" y="8" fill="#6B4FBB" rx="4"/></g><g transform="translate(46 29)"><rect width="46" height="46" y="2" fill="#E1DBF1" rx="10"/><rect width="46" height="46" fill="#E1DBF1" rx="10"/><path fill="#C3B8E3" fill-rule="nonzero" d="M10 4a6 6 0 0 0-6 6v26a6 6 0 0 0 6 6h26a6 6 0 0 0 6-6V10a6 6 0 0 0-6-6H10zm0-4h26c5.523 0 10 4.477 10 10v26c0 5.523-4.477 10-10 10H10C4.477 46 0 41.523 0 36V10C0 4.477 4.477 0 10 0z"/><rect width="14" height="14" x="16" y="16" fill="#6B4FBB" rx="2"/></g><path fill="#E1DBF1" fill-rule="nonzero" d="M98.413 35.682a2 2 0 1 1-2.826-2.83l2.122-2.12a2 2 0 1 1 2.827 2.83l-2.123 2.12z"/><path fill="#C3B8E3" d="M104.78 29.32a2 2 0 0 1-2.826-2.829l2.122-2.12a2 2 0 0 1 2.827 2.83l-2.122 2.12z"/><path fill="#C3B8E3" fill-rule="nonzero" d="M42.413 89.682a2 2 0 1 1-2.826-2.83l2.122-2.12a2 2 0 1 1 2.827 2.83l-2.123 2.12z"/><path fill="#E1DBF1" d="M48.78 83.32a2 2 0 1 1-2.826-2.829l2.122-2.12a2 2 0 1 1 2.827 2.83l-2.122 2.12z"/><path fill="#E1DBF1" fill-rule="nonzero" d="M27.713 26.531a2 2 0 1 1 2.574-3.062l2.296 1.93a2 2 0 1 1-2.573 3.062l-2.297-1.93z"/><path fill="#C3B8E3" d="M34.604 32.321a2 2 0 1 1 2.573-3.062l2.297 1.93A2 2 0 0 1 36.9 34.25l-2.297-1.93z"/><path fill="#C3B8E3" fill-rule="nonzero" d="M93.74 74.553a2 2 0 0 1 2.52-3.106l2.33 1.891a2 2 0 1 1-2.521 3.106l-2.33-1.891z"/><path fill="#E1DBF1" d="M100.727 80.225a2 2 0 1 1 2.521-3.105l2.33 1.89a2 2 0 1 1-2.522 3.106l-2.33-1.89z"/></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/clusters_empty.svg b/app/assets/images/illustrations/clusters_empty.svg deleted file mode 100644 index 39627a1c314..00000000000 --- a/app/assets/images/illustrations/clusters_empty.svg +++ /dev/null @@ -1 +0,0 @@ -<svg height="128" viewBox="0 0 142 128" width="142" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path d="M94 62h20v4H94z" fill="#f0edf8"/><path d="M84.828 84l17.678 17.678-2.828 2.828L82 86.828z" fill="#fee1d3"/><path d="M42.828 24l17.678 17.678-2.828 2.828L40 26.828zM40 101.678L57.678 84l2.828 2.828-17.678 17.678z" fill="#f0edf8"/><path d="M82 41.678L99.678 24l2.828 2.828-17.678 17.678zM28 62h20v4H28zM3 52h24v24H3z" fill="#fee1d3"/><path d="M31 3h24v24H31z" fill="#f0edf8"/><path d="M87 3h24v24H87z" fill="#fef0e8"/><path d="M115 52h24v24h-24z" fill="#f0edf8"/><path d="M87 101h24v24H87z" fill="#fee1d3"/><path d="M31 101h24v24H31z" fill="#f0edf8"/><path d="M49 42h44v44H49z" fill="#c3b8e3"/><g fill-rule="nonzero"><path d="M5 53a1 1 0 0 0-1 1v20a1 1 0 0 0 1 1h20a1 1 0 0 0 1-1V54a1 1 0 0 0-1-1zm0-4h20a5 5 0 0 1 5 5v20a5 5 0 0 1-5 5H5a5 5 0 0 1-5-5V54a5 5 0 0 1 5-5z" fill="#fdc4a8"/><path d="M56 43a6 6 0 0 0-6 6v30a6 6 0 0 0 6 6h30a6 6 0 0 0 6-6V49a6 6 0 0 0-6-6zm0-4h30c5.523 0 10 4.477 10 10v30c0 5.523-4.477 10-10 10H56c-5.523 0-10-4.477-10-10V49c0-5.523 4.477-10 10-10z" fill="#6b4fbb"/><path d="M89 4a1 1 0 0 0-1 1v20a1 1 0 0 0 1 1h20a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1zm0-4h20a5 5 0 0 1 5 5v20a5 5 0 0 1-5 5H89a5 5 0 0 1-5-5V5a5 5 0 0 1 5-5z" fill="#fee1d3"/><path d="M89 102a1 1 0 0 0-1 1v20a1 1 0 0 0 1 1h20a1 1 0 0 0 1-1v-20a1 1 0 0 0-1-1zm0-4h20a5 5 0 0 1 5 5v20a5 5 0 0 1-5 5H89a5 5 0 0 1-5-5v-20a5 5 0 0 1 5-5z" fill="#fdc4a8"/><path d="M117 53a1 1 0 0 0-1 1v20a1 1 0 0 0 1 1h20a1 1 0 0 0 1-1V54a1 1 0 0 0-1-1zm0-4h20a5 5 0 0 1 5 5v20a5 5 0 0 1-5 5h-20a5 5 0 0 1-5-5V54a5 5 0 0 1 5-5zM33 102a1 1 0 0 0-1 1v20a1 1 0 0 0 1 1h20a1 1 0 0 0 1-1v-20a1 1 0 0 0-1-1zm0-4h20a5 5 0 0 1 5 5v20a5 5 0 0 1-5 5H33a5 5 0 0 1-5-5v-20a5 5 0 0 1 5-5zM33 4a1 1 0 0 0-1 1v20a1 1 0 0 0 1 1h20a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1zm0-4h20a5 5 0 0 1 5 5v20a5 5 0 0 1-5 5H33a5 5 0 0 1-5-5V5a5 5 0 0 1 5-5z" fill="#e1dbf1"/></g></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/convdev/convdev_no_data.svg b/app/assets/images/illustrations/convdev/convdev_no_data.svg deleted file mode 100644 index b90eddcccfa..00000000000 --- a/app/assets/images/illustrations/convdev/convdev_no_data.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="360" height="220" viewBox="0 0 360 220"><g fill="none" fill-rule="evenodd"><path fill="#000" fill-opacity=".02" d="M125 44V24.003C125 18.48 129.483 14 135.005 14h89.99C230.52 14 235 18.477 235 24.003V43h84.992C326.624 43 332 48.372 332 55.002v144.996c0 6.63-5.38 12.002-12.008 12.002h-85.984c-6.632 0-12.008-5.372-12.008-12.002V183h-78v17.002c0 6.626-5.38 11.998-12.008 11.998H46.008C39.376 212 34 206.624 34 200.002V55.998C34 49.372 39.38 44 46.008 44H125z"/><g transform="translate(214 36)"><rect width="110" height="168" x="2" y="2" fill="#FFF" rx="10"/><path fill="#EEE" fill-rule="nonzero" d="M4 12.006c0-2.208.896-4.27 2.457-5.77a2 2 0 0 0-2.773-2.883A11.974 11.974 0 0 0 0 12.006a2 2 0 1 0 4 0zM14.388 4h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm18 0h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm18 0h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm18 0h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm18 0h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm17.51.227a8.015 8.015 0 0 1 5.022 3.756 2 2 0 1 0 3.458-2.011A12.01 12.01 0 0 0 104.844.34a2 2 0 0 0-.946 3.887zM110 16.78v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm-.024 17.844a7.99 7.99 0 0 1-2.903 5.558 2 2 0 0 0 2.54 3.09 11.977 11.977 0 0 0 4.35-8.338 2.002 2.002 0 0 0-1.838-2.15 2.003 2.003 0 0 0-2.15 1.84zM98.826 168h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-18 0h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-18 0h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-18 0h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-18 0h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-17.334-.4a8.032 8.032 0 0 1-4.71-4.143 1.998 1.998 0 0 0-2.667-.938 1.997 1.997 0 0 0-.938 2.667 12.022 12.022 0 0 0 7.063 6.21 1.998 1.998 0 1 0 1.252-3.798zM4 154.434v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0z"/><path fill="#F0EDF8" fill-rule="nonzero" d="M57 111c-11.598 0-21-9.402-21-21s9.402-21 21-21 21 9.402 21 21-9.402 21-21 21zm0-4c9.39 0 17-7.61 17-17s-7.61-17-17-17-17 7.61-17 17 7.61 17 17 17z"/><path fill="#6B4FBB" d="M58 88v-6.997c0-1.11-.895-2.003-2-2.003-1.112 0-2 .897-2 2.003v8.994a1.999 1.999 0 0 0 2.503 1.94c.162.04.33.063.506.063h7.98a2 2 0 0 0 .001-4H58z"/><rect width="8" height="4" x="8" y="14" fill="#EEE" rx="2"/><path fill="#EEE" d="M21 16c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004A1.995 1.995 0 0 1 21 16zm13 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004A1.995 1.995 0 0 1 34 16zm13 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004A1.995 1.995 0 0 1 47 16zm13 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004A1.995 1.995 0 0 1 60 16zm13 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004A1.995 1.995 0 0 1 73 16zm13 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004A1.995 1.995 0 0 1 86 16zm13 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004A1.995 1.995 0 0 1 99 16z"/></g><g transform="translate(118 7)"><rect width="110" height="168" x="2" y="2" fill="#FFF" rx="10"/><path fill="#EEE" fill-rule="nonzero" d="M4 12.006c0-2.208.896-4.27 2.457-5.77a2 2 0 0 0-2.773-2.883A11.974 11.974 0 0 0 0 12.006a2 2 0 1 0 4 0zM14.388 4h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm18 0h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm18 0h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm18 0h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm18 0h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm17.51.227a8.015 8.015 0 0 1 5.022 3.756 2 2 0 1 0 3.458-2.011A12.01 12.01 0 0 0 104.844.34a2 2 0 0 0-.946 3.887zM110 16.78v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm-.024 17.844a7.99 7.99 0 0 1-2.903 5.558 2 2 0 0 0 2.54 3.09 11.977 11.977 0 0 0 4.35-8.338 2.002 2.002 0 0 0-1.838-2.15 2.003 2.003 0 0 0-2.15 1.84zM98.826 168h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-18 0h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-18 0h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-18 0h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-18 0h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-17.334-.4a8.032 8.032 0 0 1-4.71-4.143 1.998 1.998 0 0 0-2.667-.938 1.997 1.997 0 0 0-.938 2.667 12.022 12.022 0 0 0 7.063 6.21 1.998 1.998 0 1 0 1.252-3.798zM4 154.434v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0z"/><g fill-rule="nonzero"><path fill="#F0EDF8" d="M57 112c-12.15 0-22-9.85-22-22s9.85-22 22-22 22 9.85 22 22-9.85 22-22 22zm0-6c8.837 0 16-7.163 16-16s-7.163-16-16-16-16 7.163-16 16 7.163 16 16 16z"/><path fill="#6B4FBB" d="M41.692 105.8A21.93 21.93 0 0 0 57 112c12.15 0 22-9.85 22-22s-9.85-22-22-22v6c8.837 0 16 7.163 16 16s-7.163 16-16 16a15.935 15.935 0 0 1-11.133-4.508l-4.175 4.31z"/></g><path fill="#EEE" d="M8 16c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2H9.998A1.995 1.995 0 0 1 8 16zm13 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004A1.995 1.995 0 0 1 21 16zm13 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004A1.995 1.995 0 0 1 34 16zm13 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004A1.995 1.995 0 0 1 47 16zm13 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004A1.995 1.995 0 0 1 60 16zm13 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004A1.995 1.995 0 0 1 73 16zm13 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004A1.995 1.995 0 0 1 86 16zm13 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004A1.995 1.995 0 0 1 99 16z"/></g><g transform="translate(26 36)"><rect width="110" height="168" x="2" y="2" fill="#FFF" rx="10"/><path fill="#EEE" fill-rule="nonzero" d="M4 12.006v147.988A8 8 0 0 0 12.005 168h89.99a8.007 8.007 0 0 0 8.005-8.006V12.006A8 8 0 0 0 101.995 4h-89.99A8.007 8.007 0 0 0 4 12.006zm-4 0C0 5.376 5.377 0 12.005 0h89.99C108.628 0 114 5.37 114 12.006v147.988c0 6.63-5.377 12.006-12.005 12.006h-89.99C5.372 172 0 166.63 0 159.994V12.006z"/><g transform="translate(21 82)"><rect width="24" height="4" y="10" fill="#F0EDF8" rx="2"/><rect width="14" height="4" x="5" fill="#6B4FBB" rx="2"/></g><g transform="translate(69 82)"><rect width="24" height="4" y="10" fill="#F0EDF8" rx="2"/><rect width="14" height="4" x="5" fill="#6B4FBB" rx="2"/></g><g transform="translate(38 42)"><rect width="22" height="4" x="8" fill="#FEE1D3" rx="2"/><rect width="38" height="4" y="12" fill="#FB722E" rx="2"/></g><path fill="#EEE" d="M4 14h106v4H4z"/><path fill="#333" d="M35.724 138h9.696v-2.856h-2.856V122.76h-2.592c-1.08.648-2.136 1.08-3.792 1.392v2.184h2.856v8.808h-3.312V138zm17.736.288c-2.952 0-5.76-2.208-5.76-7.56 0-5.688 2.952-8.256 6.168-8.256 2.016 0 3.48.84 4.44 1.824l-1.848 2.112c-.528-.576-1.488-1.08-2.376-1.08-1.68 0-3.024 1.2-3.144 4.752.792-1.008 2.112-1.608 3.048-1.608 2.616 0 4.536 1.488 4.536 4.704 0 3.168-2.304 5.112-5.064 5.112zm-.072-2.64c1.056 0 1.92-.744 1.92-2.472 0-1.608-.84-2.208-1.992-2.208-.792 0-1.68.432-2.304 1.512.312 2.4 1.32 3.168 2.376 3.168zM63.9 132c-2.256 0-3.888-1.848-3.888-4.992 0-3.12 1.632-4.944 3.888-4.944 2.256 0 3.912 1.824 3.912 4.944 0 3.144-1.656 4.992-3.912 4.992zm0-1.968c.792 0 1.44-.792 1.44-3.024s-.648-2.976-1.44-2.976c-.792 0-1.44.744-1.44 2.976s.648 3.024 1.44 3.024zm.528 8.256l8.448-16.224h2.04l-8.448 16.224h-2.04zm11.016 0c-2.256 0-3.888-1.848-3.888-4.992 0-3.12 1.632-4.944 3.888-4.944 2.256 0 3.912 1.824 3.912 4.944 0 3.144-1.656 4.992-3.912 4.992zm0-1.968c.792 0 1.44-.792 1.44-3.024s-.648-2.976-1.44-2.976c-.792 0-1.44.744-1.44 2.976s.648 3.024 1.44 3.024z"/></g></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/convdev/convdev_no_index.svg b/app/assets/images/illustrations/convdev/convdev_no_index.svg deleted file mode 100644 index 4aaf505e0b8..00000000000 --- a/app/assets/images/illustrations/convdev/convdev_no_index.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="360" height="200" viewBox="0 0 360 200"><g fill="none" fill-rule="evenodd" transform="translate(3 11)"><rect width="110" height="168" x="6" y="8" fill="#000" fill-opacity=".02" rx="10"/><g transform="translate(0 2)"><rect width="110" height="168" fill="#FFF" rx="10"/><path fill="#EEE" fill-rule="nonzero" d="M2 10.006v147.988A8 8 0 0 0 10.005 166h89.99a8.007 8.007 0 0 0 8.005-8.006V10.006A8 8 0 0 0 99.995 2h-89.99A8.007 8.007 0 0 0 2 10.006zm-4 0C-2 3.376 3.377-2 10.005-2h89.99C106.628-2 112 3.37 112 10.006v147.988c0 6.63-5.377 12.006-12.005 12.006h-89.99C3.372 170-2 164.63-2 157.994V10.006z"/><g transform="translate(19 80)"><rect width="24" height="4" y="10" fill="#F0EDF8" rx="2"/><rect width="14" height="4" x="5" fill="#6B4FBB" rx="2"/></g><g transform="translate(67 80)"><rect width="24" height="4" y="10" fill="#F0EDF8" rx="2"/><rect width="14" height="4" x="5" fill="#6B4FBB" rx="2"/></g><g transform="translate(36 40)"><rect width="22" height="4" x="8" fill="#FEE1D3" rx="2"/><rect width="38" height="4" y="12" fill="#FB722E" rx="2"/></g><path fill="#EEE" d="M2 12h106v4H2z"/><path fill="#333" d="M38.048 127.792c.792 0 1.68-.432 2.28-1.512-.312-2.4-1.296-3.168-2.376-3.168-1.032 0-1.92.744-1.92 2.472 0 1.608.864 2.208 2.016 2.208zm-.552 8.496c-2.016 0-3.504-.864-4.464-1.824l1.872-2.112c.504.576 1.464 1.08 2.352 1.08 1.704 0 3.024-1.2 3.144-4.752-.792 1.008-2.112 1.608-3.048 1.608-2.592 0-4.536-1.488-4.536-4.704 0-3.168 2.304-5.112 5.064-5.112 2.952 0 5.784 2.208 5.784 7.56 0 5.688-2.976 8.256-6.168 8.256zm13.488 0c-3.048 0-5.304-1.704-5.304-4.176 0-1.848 1.152-2.976 2.592-3.744v-.096c-1.176-.888-2.04-1.992-2.04-3.6 0-2.592 2.04-4.2 4.872-4.2 2.784 0 4.632 1.656 4.632 4.176 0 1.464-.936 2.64-1.992 3.336v.096c1.464.792 2.64 1.968 2.64 3.984 0 2.4-2.16 4.224-5.4 4.224zm.96-9.168c.6-.696.936-1.44.936-2.232 0-1.176-.696-1.968-1.848-1.968-.936 0-1.704.576-1.704 1.752 0 1.248 1.056 1.848 2.616 2.448zm-.888 6.72c1.176 0 2.04-.624 2.04-1.896 0-1.344-1.296-1.848-3.216-2.664-.672.624-1.176 1.488-1.176 2.424 0 1.344 1.08 2.136 2.352 2.136zm10.8-3.84c-2.256 0-3.888-1.848-3.888-4.992 0-3.12 1.632-4.944 3.888-4.944 2.256 0 3.912 1.824 3.912 4.944 0 3.144-1.656 4.992-3.912 4.992zm0-1.968c.792 0 1.44-.792 1.44-3.024s-.648-2.976-1.44-2.976c-.792 0-1.44.744-1.44 2.976s.648 3.024 1.44 3.024zm.528 8.256l8.448-16.224h2.04l-8.448 16.224h-2.04zm11.016 0c-2.256 0-3.888-1.848-3.888-4.992 0-3.12 1.632-4.944 3.888-4.944 2.256 0 3.912 1.824 3.912 4.944 0 3.144-1.656 4.992-3.912 4.992zm0-1.968c.792 0 1.44-.792 1.44-3.024s-.648-2.976-1.44-2.976c-.792 0-1.44.744-1.44 2.976s.648 3.024 1.44 3.024z"/></g><g transform="translate(122)"><rect width="110" height="168" x="2" y="2" fill="#FFF" rx="10"/><path fill="#EEE" fill-rule="nonzero" d="M4 12.006c0-2.208.896-4.27 2.457-5.77a2 2 0 0 0-2.773-2.883A11.974 11.974 0 0 0 0 12.006a2 2 0 1 0 4 0zM14.388 4h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm18 0h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm18 0h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm18 0h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm18 0h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm17.51.227a8.015 8.015 0 0 1 5.022 3.756 2 2 0 1 0 3.458-2.011A12.01 12.01 0 0 0 104.844.34a2 2 0 0 0-.946 3.887zM110 16.78v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm-.024 17.844a7.99 7.99 0 0 1-2.903 5.558 2 2 0 0 0 2.54 3.09 11.977 11.977 0 0 0 4.35-8.338 2.002 2.002 0 0 0-1.838-2.15 2.003 2.003 0 0 0-2.15 1.84zM98.826 168h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-18 0h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-18 0h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-18 0h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-18 0h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-17.334-.4a8.032 8.032 0 0 1-4.71-4.143 1.998 1.998 0 0 0-2.667-.938 1.997 1.997 0 0 0-.938 2.667 12.022 12.022 0 0 0 7.063 6.21 1.998 1.998 0 1 0 1.252-3.798zM4 154.434v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0z"/><g transform="translate(21 82)"><rect width="24" height="4" y="10" fill="#F0EDF8" rx="2"/><rect width="14" height="4" x="5" fill="#C3B8E3" rx="2"/></g><g transform="translate(69 82)"><rect width="24" height="4" y="10" fill="#F0EDF8" rx="2"/><rect width="14" height="4" x="5" fill="#C3B8E3" rx="2"/></g><path fill="#FEE1D3" d="M44 44a2 2 0 0 1 1.998-2h2.004a2 2 0 0 1 0 4h-2.004A1.994 1.994 0 0 1 44 44zm10 0a2 2 0 0 1 1.998-2h2.004a2 2 0 0 1 0 4h-2.004A1.994 1.994 0 0 1 54 44zm10 0a2 2 0 0 1 1.998-2h2.004a2 2 0 0 1 0 4h-2.004A1.994 1.994 0 0 1 64 44zM34 56a2 2 0 0 1 1.998-2h2.004a2 2 0 0 1 0 4h-2.004A1.994 1.994 0 0 1 34 56zm10 0a2 2 0 0 1 1.998-2h2.004a2 2 0 0 1 0 4h-2.004A1.994 1.994 0 0 1 44 56zm10 0a2 2 0 0 1 1.998-2h2.004a2 2 0 0 1 0 4h-2.004A1.994 1.994 0 0 1 54 56zm10 0a2 2 0 0 1 1.998-2h2.004a2 2 0 0 1 0 4h-2.004A1.994 1.994 0 0 1 64 56zm10 0a2 2 0 0 1 1.998-2h2.004a2 2 0 0 1 0 4h-2.004A1.994 1.994 0 0 1 74 56z"/><rect width="8" height="4" x="8" y="14" fill="#EEE" rx="2"/><rect width="8" height="4" x="21" y="14" fill="#EEE" rx="2"/><rect width="8" height="4" x="34" y="14" fill="#EEE" rx="2"/><rect width="8" height="4" x="47" y="14" fill="#EEE" rx="2"/><rect width="8" height="4" x="60" y="14" fill="#EEE" rx="2"/><rect width="8" height="4" x="73" y="14" fill="#EEE" rx="2"/><rect width="8" height="4" x="86" y="14" fill="#EEE" rx="2"/><rect width="8" height="4" x="99" y="14" fill="#EEE" rx="2"/><path fill="#EEE" d="M46.716 138.288c-3.264 0-5.448-2.784-5.448-7.968s2.184-7.848 5.448-7.848c3.264 0 5.448 2.664 5.448 7.848 0 5.184-2.184 7.968-5.448 7.968zm0-2.736c1.2 0 2.112-1.08 2.112-5.232 0-4.176-.912-5.112-2.112-5.112-1.176 0-2.112.936-2.112 5.112 0 4.152.936 5.232 2.112 5.232zM57.564 132c-2.256 0-3.888-1.848-3.888-4.992 0-3.12 1.632-4.944 3.888-4.944 2.256 0 3.912 1.824 3.912 4.944 0 3.144-1.656 4.992-3.912 4.992zm0-1.968c.792 0 1.44-.792 1.44-3.024s-.648-2.976-1.44-2.976c-.792 0-1.44.744-1.44 2.976s.648 3.024 1.44 3.024zm.528 8.256l8.448-16.224h2.04l-8.448 16.224h-2.04zm11.016 0c-2.256 0-3.888-1.848-3.888-4.992 0-3.12 1.632-4.944 3.888-4.944 2.256 0 3.912 1.824 3.912 4.944 0 3.144-1.656 4.992-3.912 4.992zm0-1.968c.792 0 1.44-.792 1.44-3.024s-.648-2.976-1.44-2.976c-.792 0-1.44.744-1.44 2.976s.648 3.024 1.44 3.024z"/></g><g transform="translate(243)"><rect width="110" height="168" x="2" y="2" fill="#FFF" rx="10"/><path fill="#EEE" fill-rule="nonzero" d="M4 12.006c0-2.208.896-4.27 2.457-5.77a2 2 0 0 0-2.773-2.883A11.974 11.974 0 0 0 0 12.006a2 2 0 1 0 4 0zM14.388 4h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm18 0h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm18 0h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm18 0h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm18 0h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm17.51.227a8.015 8.015 0 0 1 5.022 3.756 2 2 0 1 0 3.458-2.011A12.01 12.01 0 0 0 104.844.34a2 2 0 0 0-.946 3.887zM110 16.78v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm-.024 17.844a7.99 7.99 0 0 1-2.903 5.558 2 2 0 0 0 2.54 3.09 11.977 11.977 0 0 0 4.35-8.338 2.002 2.002 0 0 0-1.838-2.15 2.003 2.003 0 0 0-2.15 1.84zM98.826 168h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-18 0h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-18 0h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-18 0h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-18 0h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-17.334-.4a8.032 8.032 0 0 1-4.71-4.143 1.998 1.998 0 0 0-2.667-.938 1.997 1.997 0 0 0-.938 2.667 12.022 12.022 0 0 0 7.063 6.21 1.998 1.998 0 1 0 1.252-3.798zM4 154.434v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0z"/><path fill="#FEE1D3" d="M44 44a2 2 0 0 1 1.998-2h2.004a2 2 0 0 1 0 4h-2.004A1.994 1.994 0 0 1 44 44zm10 0a2 2 0 0 1 1.998-2h2.004a2 2 0 0 1 0 4h-2.004A1.994 1.994 0 0 1 54 44zm10 0a2 2 0 0 1 1.998-2h2.004a2 2 0 0 1 0 4h-2.004A1.994 1.994 0 0 1 64 44zM34 56a2 2 0 0 1 1.998-2h2.004a2 2 0 0 1 0 4h-2.004A1.994 1.994 0 0 1 34 56zm10 0a2 2 0 0 1 1.998-2h2.004a2 2 0 0 1 0 4h-2.004A1.994 1.994 0 0 1 44 56zm10 0a2 2 0 0 1 1.998-2h2.004a2 2 0 0 1 0 4h-2.004A1.994 1.994 0 0 1 54 56zm10 0a2 2 0 0 1 1.998-2h2.004a2 2 0 0 1 0 4h-2.004A1.994 1.994 0 0 1 64 56zm10 0a2 2 0 0 1 1.998-2h2.004a2 2 0 0 1 0 4h-2.004A1.994 1.994 0 0 1 74 56z"/><g transform="translate(21 82)"><rect width="24" height="4" y="10" fill="#F0EDF8" rx="2"/><rect width="14" height="4" x="5" fill="#C3B8E3" rx="2"/></g><g transform="translate(69 82)"><rect width="24" height="4" y="10" fill="#F0EDF8" rx="2"/><rect width="14" height="4" x="5" fill="#C3B8E3" rx="2"/></g><rect width="8" height="4" x="8" y="14" fill="#EEE" rx="2"/><rect width="8" height="4" x="21" y="14" fill="#EEE" rx="2"/><rect width="8" height="4" x="34" y="14" fill="#EEE" rx="2"/><rect width="8" height="4" x="47" y="14" fill="#EEE" rx="2"/><rect width="8" height="4" x="60" y="14" fill="#EEE" rx="2"/><rect width="8" height="4" x="73" y="14" fill="#EEE" rx="2"/><rect width="8" height="4" x="86" y="14" fill="#EEE" rx="2"/><rect width="8" height="4" x="99" y="14" fill="#EEE" rx="2"/><path fill="#EEE" d="M46.716 138.288c-3.264 0-5.448-2.784-5.448-7.968s2.184-7.848 5.448-7.848c3.264 0 5.448 2.664 5.448 7.848 0 5.184-2.184 7.968-5.448 7.968zm0-2.736c1.2 0 2.112-1.08 2.112-5.232 0-4.176-.912-5.112-2.112-5.112-1.176 0-2.112.936-2.112 5.112 0 4.152.936 5.232 2.112 5.232zM57.564 132c-2.256 0-3.888-1.848-3.888-4.992 0-3.12 1.632-4.944 3.888-4.944 2.256 0 3.912 1.824 3.912 4.944 0 3.144-1.656 4.992-3.912 4.992zm0-1.968c.792 0 1.44-.792 1.44-3.024s-.648-2.976-1.44-2.976c-.792 0-1.44.744-1.44 2.976s.648 3.024 1.44 3.024zm.528 8.256l8.448-16.224h2.04l-8.448 16.224h-2.04zm11.016 0c-2.256 0-3.888-1.848-3.888-4.992 0-3.12 1.632-4.944 3.888-4.944 2.256 0 3.912 1.824 3.912 4.944 0 3.144-1.656 4.992-3.912 4.992zm0-1.968c.792 0 1.44-.792 1.44-3.024s-.648-2.976-1.44-2.976c-.792 0-1.44.744-1.44 2.976s.648 3.024 1.44 3.024z"/></g></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/convdev/convdev_overview.svg b/app/assets/images/illustrations/convdev/convdev_overview.svg deleted file mode 100644 index a06d70812ca..00000000000 --- a/app/assets/images/illustrations/convdev/convdev_overview.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="208" height="127" viewBox="0 0 208 127" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="a" width="58" height="98" y="17" rx="6"/><rect id="b" width="58" height="98" x="3.5" y="17" rx="6"/><rect id="c" width="58" height="98.394" rx="6"/></defs><g fill="none" fill-rule="evenodd" transform="translate(1)"><path fill="#000" fill-opacity=".06" fill-rule="nonzero" d="M16 11.06c0-1.39.56-2.69 1.534-3.635.398-.386.41-1.025.027-1.426a.993.993 0 0 0-1.413-.028A7.075 7.075 0 0 0 14 11.062c0 .556.448 1.007 1 1.007s1-.452 1-1.01zm6.432-5.043h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0H185a4.95 4.95 0 0 1 3.254 1.215.995.995 0 0 0 1.41-.108c.36-.423.312-1.06-.107-1.422A6.944 6.944 0 0 0 185 4h-.568c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zM190 11.932v4.84c0 .557.448 1.008 1 1.008s1-.45 1-1.008v-4.84c0-.556-.448-1.008-1-1.008s-1 .452-1 1.008zm0 10.89v4.84c0 .556.448 1.008 1 1.008s1-.452 1-1.01v-4.838c0-.557-.448-1.01-1-1.01s-1 .453-1 1.01zm0 10.89v4.84c0 .555.448 1.007 1 1.007s1-.453 1-1.01v-4.84c0-.556-.448-1.007-1-1.007s-1 .45-1 1.008zm0 10.888v4.84c0 .557.448 1.008 1 1.008s1-.45 1-1.008V44.6c0-.557-.448-1.008-1-1.008s-1 .45-1 1.008zm0 10.89v4.84c0 .556.448 1.007 1 1.007s1-.45 1-1.008v-4.84c0-.557-.448-1.01-1-1.01s-1 .453-1 1.01zm0 10.89v4.838c0 .557.448 1.01 1 1.01s1-.453 1-1.01v-4.84c0-.556-.448-1.008-1-1.008s-1 .452-1 1.01zm0 10.888v4.84c0 .556.448 1.008 1 1.008s1-.452 1-1.008v-4.84c0-.557-.448-1.008-1-1.008s-1 .45-1 1.008zm0 10.89v4.84c0 .556.448 1.007 1 1.007s1-.45 1-1.008v-4.84c0-.557-.448-1.008-1-1.008s-1 .45-1 1.007zm0 10.888v4.84c0 .557.448 1.008 1 1.008s1-.45 1-1.008v-4.84c0-.556-.448-1.008-1-1.008s-1 .452-1 1.008zm-.24 21.446a5.06 5.06 0 0 1-2.572 2.985 1.01 1.01 0 0 0-.46 1.348c.24.5.84.708 1.336.464a7.06 7.06 0 0 0 3.598-4.178c.17-.53-.12-1.098-.644-1.27a1 1 0 0 0-1.26.65zm-8.063 3.49h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.577-.116a5.009 5.009 0 0 1-3.19-2.3.994.994 0 0 0-1.373-.333c-.472.29-.62.91-.332 1.386.99 1.632 2.6 2.8 4.465 3.215a1 1 0 0 0 1.192-.768 1.005 1.005 0 0 0-.762-1.2zM16 105.292v-4.84c0-.556-.448-1.008-1-1.008s-1 .452-1 1.01v4.838c0 .557.448 1.01 1 1.01s1-.453 1-1.01zm0-10.89v-4.84c0-.555-.448-1.007-1-1.007s-1 .452-1 1.008v4.84c0 .557.448 1.008 1 1.008s1-.45 1-1.007zm0-10.888v-4.84c0-.557-.448-1.008-1-1.008s-1 .45-1 1.008v4.84c0 .557.448 1.008 1 1.008s1-.45 1-1.008zm0-10.89v-4.84c0-.556-.448-1.007-1-1.007s-1 .45-1 1.008v4.84c0 .556.448 1.008 1 1.008s1-.452 1-1.008zm0-10.89v-4.838c0-.557-.448-1.01-1-1.01s-1 .453-1 1.01v4.84c0 .556.448 1.008 1 1.008s1-.452 1-1.01zm0-11.888v-4.84c0-.556-.448-1.008-1-1.008s-1 .452-1 1.008v4.84c0 .557.448 1.008 1 1.008s1-.45 1-1.008zm0-9.89v-4.84c0-.556-.448-1.007-1-1.007s-1 .45-1 1.007v4.84c0 .557.448 1.008 1 1.008s1-.45 1-1.008zm0-10.888v-4.84c0-.557-.448-1.008-1-1.008s-1 .45-1 1.008v4.84c0 .556.448 1.008 1 1.008s1-.452 1-1.008zm0-10.89v-4.84c0-.556-.448-1.008-1-1.008s-1 .452-1 1.01v4.838c0 .557.448 1.01 1 1.01s1-.453 1-1.01z"/><g transform="translate(74)"><rect width="58" height="98" y="20" fill="#000" fill-opacity=".02" rx="6"/><use fill="#FFF" xlink:href="#a"/><rect width="56" height="96" x="1" y="18" stroke="#EEE" stroke-width="2" rx="6"/><g transform="translate(16 45.185)"><path fill="#333" d="M.59 33.815h5.655V32.15H4.58v-7.225H3.066c-.63.378-1.246.63-2.212.812v1.274H2.52v5.14H.59v1.665zm10.093.168c-1.778 0-3.094-.994-3.094-2.436 0-1.078.67-1.736 1.51-2.184v-.056c-.685-.518-1.19-1.162-1.19-2.1 0-1.512 1.19-2.45 2.843-2.45 1.624 0 2.702.966 2.702 2.436 0 .854-.546 1.54-1.162 1.946v.055c.854.462 1.54 1.148 1.54 2.324 0 1.4-1.26 2.463-3.15 2.463zm.56-5.348c.35-.406.546-.84.546-1.302 0-.686-.407-1.148-1.08-1.148-.545 0-.993.336-.993 1.022 0 .728.616 1.078 1.526 1.428zm-.518 3.92c.686 0 1.19-.364 1.19-1.106 0-.785-.756-1.08-1.876-1.555-.393.364-.687.868-.687 1.414 0 .783.63 1.245 1.372 1.245zm6.3-2.24c-1.316 0-2.268-1.078-2.268-2.912 0-1.82.952-2.884 2.268-2.884 1.316 0 2.282 1.063 2.282 2.883 0 1.834-.966 2.912-2.282 2.912zm0-1.148c.462 0 .84-.462.84-1.764s-.378-1.736-.84-1.736c-.462 0-.84.434-.84 1.736s.378 1.764.84 1.764zm.308 4.816l4.928-9.464h1.19l-4.927 9.463h-1.19zm6.426 0c-1.317 0-2.27-1.078-2.27-2.912 0-1.82.953-2.883 2.27-2.883 1.315 0 2.28 1.064 2.28 2.884 0 1.835-.965 2.913-2.28 2.913zm0-1.148c.46 0 .84-.462.84-1.764 0-1.3-.38-1.735-.84-1.735-.463 0-.84.434-.84 1.736 0 1.303.377 1.765.84 1.765z"/><rect width="13" height="2" x="6" y=".815" fill="#FB722E" rx="1"/><path fill="#F0EDF8" d="M3 47.815c0-.552.455-1 .992-1h18.016c.548 0 .992.444.992 1 0 .553-.455 1-.992 1H3.992a.994.994 0 0 1-.992-1zm0 6c0-.552.455-1 .992-1h18.016c.548 0 .992.444.992 1 0 .553-.455 1-.992 1H3.992a.994.994 0 0 1-.992-1z"/><rect width="20" height="2" x="3" y="6.815" fill="#FEE1D3" rx="1"/></g><g transform="translate(10.81)"><circle cx="18.19" cy="18" r="18" fill="#FFF"/><path fill="#F0EDF8" fill-rule="nonzero" d="M18.19 34c8.837 0 16-7.163 16-16s-7.163-16-16-16-16 7.163-16 16 7.163 16 16 16zm0 2c-9.94 0-18-8.06-18-18s8.06-18 18-18 18 8.06 18 18-8.06 18-18 18z"/><g transform="translate(10 11)"><path fill="#C3B8E3" fill-rule="nonzero" d="M2.19 13.32L5.397 11h7.783a.998.998 0 0 0 1.01-1V3c0-.55-.45-1-1.01-1H3.2a.998.998 0 0 0-1.01 1v10.32zM6.045 13l-3.422 2.476C1.28 16.45.19 15.892.19 14.23V3c0-1.657 1.337-3 3.01-3h9.98a3.004 3.004 0 0 1 3.01 3v7c0 1.657-1.337 3-3.01 3H6.045z"/><rect width="4" height="2" x="5.19" y="4" fill="#6B4FBB" rx="1"/><rect width="6" height="2" x="5.19" y="7" fill="#6B4FBB" rx="1"/></g></g></g><g transform="translate(144.5)"><rect width="58" height="98" x=".5" y="20" fill="#000" fill-opacity=".02" rx="6"/><use fill="#FFF" xlink:href="#b"/><rect width="56" height="96" x="4.5" y="18" stroke="#EEE" stroke-width="2" rx="6"/><g transform="translate(19 46.185)"><path fill="#333" d="M4.01 33.746c1.793 0 3.305-.938 3.305-2.59 0-1.148-.742-1.876-1.764-2.17v-.056c.953-.406 1.485-1.05 1.485-1.974 0-1.554-1.232-2.436-3.066-2.436-1.093 0-1.99.434-2.8 1.134l1.035 1.26c.56-.49 1.036-.784 1.666-.784.7 0 1.093.364 1.093.98 0 .714-.504 1.19-2.1 1.19v1.456c1.932 0 2.394.49 2.394 1.274 0 .672-.574 1.05-1.442 1.05-.756 0-1.414-.378-1.946-.896l-.953 1.302c.644.756 1.652 1.26 3.094 1.26zm4.51-.168h6.257v-1.736h-1.792c-.42 0-1.036.056-1.484.112 1.443-1.512 2.843-3.108 2.843-4.606 0-1.708-1.19-2.828-2.94-2.828-1.274 0-2.1.476-2.982 1.414l1.12 1.106c.45-.476.94-.91 1.583-.91.77 0 1.26.476 1.26 1.344 0 1.26-1.596 2.786-3.864 4.928v1.176zm9.505-3.5c-1.316 0-2.268-1.078-2.268-2.912 0-1.82.952-2.884 2.268-2.884 1.316 0 2.282 1.064 2.282 2.884 0 1.834-.966 2.912-2.282 2.912zm0-1.148c.462 0 .84-.462.84-1.764s-.378-1.736-.84-1.736c-.462 0-.84.434-.84 1.736s.378 1.764.84 1.764zm.308 4.816l4.928-9.464h1.19l-4.927 9.464h-1.19zm6.426 0c-1.317 0-2.27-1.078-2.27-2.912 0-1.82.953-2.884 2.27-2.884 1.315 0 2.28 1.064 2.28 2.884 0 1.834-.965 2.912-2.28 2.912zm0-1.148c.46 0 .84-.462.84-1.764s-.38-1.736-.84-1.736c-.463 0-.84.434-.84 1.736s.377 1.764.84 1.764z"/><rect width="13" height="2.008" x="7.5" fill="#FB722E" rx="1.004"/><path fill="#F0EDF8" d="M3.5 47.19c0-.556.455-1.005 1.006-1.005h17.988c.556 0 1.006.445 1.006 1.004 0 .553-.455 1.003-1.006 1.003H4.506A1.003 1.003 0 0 1 3.5 47.188zm0 6.023c0-.555.455-1.004 1.006-1.004h17.988c.556 0 1.006.444 1.006 1.003 0 .554-.455 1.004-1.006 1.004H4.506A1.003 1.003 0 0 1 3.5 53.212z"/><rect width="20" height="2.008" x="4" y="6.024" fill="#FEE1D3" rx="1.004"/></g><g transform="translate(14.413)"><circle cx="18.087" cy="18" r="18" fill="#FFF"/><path fill="#F0EDF8" fill-rule="nonzero" d="M18.087 34c8.836 0 16-7.163 16-16s-7.164-16-16-16c-8.837 0-16 7.163-16 16s7.163 16 16 16zm0 2c-9.942 0-18-8.06-18-18s8.058-18 18-18c9.94 0 18 8.06 18 18s-8.06 18-18 18z"/><path fill="#C3B8E3" fill-rule="nonzero" d="M18.087 24a6 6 0 1 0 0-12 6 6 0 0 0 0 12zm0 2c-4.42 0-8-3.582-8-8s3.58-8 8-8a8 8 0 0 1 0 16z"/><path fill="#6B4FBB" d="M19.087 17v-2c0-.556-.448-1-1-1-.557 0-1 .448-1 1v3a.997.997 0 0 0 .998 1h3c.557 0 1-.448 1-1 0-.556-.447-1-1-1h-2z"/></g></g><rect width="58" height="98" x="3" y="20" fill="#000" fill-opacity=".02" rx="6"/><g transform="translate(0 16.754)"><use fill="#FFF" xlink:href="#c"/><rect width="56" height="96.394" x="1" y="1" stroke="#EEE" stroke-width="2" rx="6"/><g transform="translate(16 29.618)"><path fill="#333" d="M3.137 27.84c.462 0 .98-.253 1.33-.883-.182-1.4-.756-1.848-1.386-1.848-.6 0-1.12.433-1.12 1.44 0 .94.505 1.29 1.177 1.29zm-.322 4.955A3.626 3.626 0 0 1 .21 31.73l1.093-1.23c.294.335.854.63 1.372.63.994 0 1.764-.7 1.834-2.773-.463.588-1.233.938-1.78.938-1.51 0-2.645-.868-2.645-2.744 0-1.847 1.344-2.98 2.954-2.98 1.72 0 3.373 1.287 3.373 4.41 0 3.317-1.736 4.815-3.598 4.815zm8.12 0c-1.722 0-3.36-1.288-3.36-4.41 0-3.318 1.722-4.816 3.598-4.816 1.176 0 2.03.49 2.59 1.063l-1.078 1.232c-.308-.336-.868-.63-1.386-.63-.98 0-1.765.7-1.835 2.772.462-.588 1.232-.938 1.778-.938 1.526 0 2.646.867 2.646 2.743 0 1.848-1.345 2.982-2.955 2.982zm-.042-1.54c.616 0 1.12-.434 1.12-1.442 0-.938-.49-1.288-1.162-1.288-.46 0-.98.252-1.343.882.182 1.4.77 1.848 1.386 1.848zm6.132-2.128c-1.316 0-2.268-1.078-2.268-2.912 0-1.82.952-2.884 2.268-2.884 1.316 0 2.282 1.065 2.282 2.885 0 1.834-.966 2.912-2.282 2.912zm0-1.148c.462 0 .84-.463.84-1.765 0-1.302-.378-1.736-.84-1.736-.462 0-.84.433-.84 1.735s.378 1.764.84 1.764zm.308 4.815l4.928-9.464h1.19l-4.927 9.465h-1.19zm6.426 0c-1.317 0-2.27-1.078-2.27-2.912 0-1.82.953-2.884 2.27-2.884 1.315 0 2.28 1.063 2.28 2.883 0 1.834-.965 2.912-2.28 2.912zm0-1.148c.46 0 .84-.462.84-1.764s-.38-1.736-.84-1.736c-.463 0-.84.434-.84 1.736s.377 1.764.84 1.764z"/><rect width="13" height="2.008" x="6.5" y=".314" fill="#FEE1D3" rx="1.004"/><path fill="#F0EDF8" d="M3 46.627c0-.552.455-1 .992-1h18.016c.548 0 .992.444.992 1 0 .553-.455 1-.992 1H3.992a.994.994 0 0 1-.992-1zm0 6c0-.552.455-1 .992-1h18.016c.548 0 .992.444.992 1 0 .553-.455 1-.992 1H3.992a.994.994 0 0 1-.992-1z"/><rect width="20" height="2" x="3" y="5.627" fill="#FB722E" rx="1"/></g></g><g transform="translate(10.41)"><circle cx="18.589" cy="18" r="18" fill="#FFF"/><path fill="#F0EDF8" fill-rule="nonzero" d="M18.59 34c8.836 0 16-7.163 16-16s-7.164-16-16-16c-8.837 0-16 7.163-16 16s7.163 16 16 16zm0 2c-9.942 0-18-8.06-18-18s8.058-18 18-18c9.94 0 18 8.06 18 18s-8.06 18-18 18z"/><path fill="#C3B8E3" d="M17.05 19.262h3.367l.248-2.808H17.3l-.25 2.808zm-.177 2.008l-.144 1.627c-.06.662-.646 1.2-1.3 1.2h.25c-.658 0-1.144-.534-1.085-1.2l.144-1.627H13.59a1.001 1.001 0 0 1-1.003-1.004c0-.555.455-1.004 1.002-1.004h1.325l.248-2.808h-1.15a1 1 0 0 1-1.004-1.004 1.01 1.01 0 0 1 1.004-1.004h1.33l.106-1.2c.058-.66.644-1.198 1.298-1.198h-.25c.66 0 1.145.533 1.086 1.2l-.106 1.198h3.365l.107-1.2c.058-.66.644-1.198 1.298-1.198h-.25c.66 0 1.145.533 1.086 1.2l-.106 1.198h1.03c.554 0 1.003.446 1.003 1.004 0 .555-.455 1.004-1 1.004H22.8l-.25 2.808h1.037a1 1 0 0 1 1.002 1.004c0 .554-.456 1.004-1.003 1.004h-1.214l-.144 1.627c-.06.662-.646 1.2-1.3 1.2h.25c-.658 0-1.144-.534-1.085-1.2l.144-1.627H16.87z"/><path fill="#6B4FBB" d="M17.05 19.262l-.177 2.008H14.74l.177-2.008h2.134zm-1.707-4.816h2.135l-.178 2.008h-2.135l.178-2.008zm5.5 0h2.135l-.178 2.008h-2.135l.178-2.008zm1.708 4.816l-.177 2.008H20.24l.177-2.008h2.134z"/></g></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/convdev/i2p_step_1.svg b/app/assets/images/illustrations/convdev/i2p_step_1.svg deleted file mode 100644 index 67467b1513d..00000000000 --- a/app/assets/images/illustrations/convdev/i2p_step_1.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76"><path d="M45.688 18.854c-4.869-1.989-10.488-1.975-15.29-.001a20.014 20.014 0 0 0-6.493 4.268 19.798 19.798 0 0 0-4.346 6.381 19.135 19.135 0 0 0-1.525 7.537c0 2.066.33 4.118.983 6.104a20.142 20.142 0 0 0 1.83 3.937 5.983 5.983 0 0 0-2.086 4.538c0 3.309 2.691 6 6 6s6-2.691 6-6-2.691-6-6-6c-.779 0-1.522.154-2.205.425a18.13 18.13 0 0 1-1.642-3.533 17.467 17.467 0 0 1-.881-5.472c0-2.351.459-4.623 1.391-6.814a17.721 17.721 0 0 1 3.88-5.675 18.057 18.057 0 0 1 5.85-3.845c4.329-1.778 9.392-1.79 13.78.002a18.077 18.077 0 0 1 5.843 3.84c3.39 3.34 5.257 7.776 5.257 12.493a17.463 17.463 0 0 1-.878 5.481 17.451 17.451 0 0 1-2.569 4.923c-2.134 2.866-3.818 4.698-5.174 6.173-2.424 2.643-3.98 4.599-4.383 8.384H32.215a1 1 0 1 0 0 2h11.739a1 1 0 0 0 .999-.947c.19-3.645 1.345-5.263 3.934-8.09 1.385-1.506 3.107-3.381 5.304-6.331a19.422 19.422 0 0 0 2.864-5.489c.651-1.98.98-4.04.979-6.109 0-5.256-2.078-10.198-5.856-13.92a20.079 20.079 0 0 0-6.49-4.265M28.761 51.612c0 2.206-1.794 4-4 4s-4-1.794-4-4 1.794-4 4-4 4 1.794 4 4M40 74h-4a1 1 0 1 0 0 2h4a1 1 0 1 0 0-2M42 70h-8a1 1 0 1 0 0 2h8a1 1 0 1 0 0-2M38 10a1 1 0 0 0 1-1V1a1 1 0 1 0-2 0v8a1 1 0 0 0 1 1M20.828 15.828a.999.999 0 0 0 .707-1.707l-5.656-5.656a.999.999 0 1 0-1.414 1.414l5.656 5.656a.997.997 0 0 0 .707.293M10 33H2a1 1 0 1 0 0 2h8a1 1 0 1 0 0-2M60.12 8.465l-5.656 5.656a.999.999 0 1 0 1.414 1.414l5.656-5.656a.999.999 0 1 0-1.414-1.414M74 33h-8a1 1 0 1 0 0 2h8a1 1 0 1 0 0-2M43 66H33a1 1 0 1 0 0 2h10a1 1 0 1 0 0-2"/></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/convdev/i2p_step_10.svg b/app/assets/images/illustrations/convdev/i2p_step_10.svg deleted file mode 100644 index 588ecd81414..00000000000 --- a/app/assets/images/illustrations/convdev/i2p_step_10.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76"><path d="M5 43a1 1 0 1 0 2 0v-4h4a1 1 0 1 0 0-2H7v-4a1 1 0 1 0-2 0v4H1a1 1 0 1 0 0 2h4v4M75 37h-4v-4a1 1 0 1 0-2 0v4h-4a1 1 0 1 0 0 2h4v4a1 1 0 1 0 2 0v-4h4a1 1 0 1 0 0-2M21 38a1 1 0 0 0 .47.848l8 5a.999.999 0 0 0 1.061-1.696L23.887 38l6.644-4.152a1 1 0 1 0-1.061-1.695l-8 5A.998.998 0 0 0 21 38M55 38a1 1 0 0 0-.47-.848l-8-5a.999.999 0 1 0-1.061 1.695L52.113 38l-6.644 4.152a1 1 0 1 0 1.061 1.696l8-5A1 1 0 0 0 55 38M41.803 26.05a1 1 0 0 0-1.256.65l-7 22a1.001 1.001 0 0 0 .953 1.303 1 1 0 0 0 .953-.697l7-22a1.001 1.001 0 0 0-.65-1.256M62 7c3.859 0 7 3.141 7 7v11a1 1 0 1 0 2 0V14c0-4.963-4.04-9-9-9H45.91c-.479-2.833-2.943-5-5.91-5-3.309 0-6 2.691-6 6s2.691 6 6 6c2.967 0 5.431-2.167 5.91-5H62m-22 3c-2.206 0-4-1.794-4-4s1.794-4 4-4 4 1.794 4 4-1.794 4-4 4M6 26a1 1 0 0 0 1-1V14c0-3.859 3.141-7 7-7h11.09l-3.293 3.293a.999.999 0 1 0 1.414 1.414l5-5a.999.999 0 0 0 0-1.414l-5-5a.999.999 0 1 0-1.414 1.414L25.09 5H14c-4.963 0-9 4.04-9 9v11a1 1 0 0 0 1 1M36 64c-2.967 0-5.431 2.167-5.91 5H14c-3.859 0-7-3.141-7-7V51a1 1 0 1 0-2 0v11c0 4.963 4.04 9 9 9h16.09c.478 2.833 2.942 5 5.91 5 3.309 0 6-2.691 6-6s-2.691-6-6-6m0 10c-2.206 0-4-1.794-4-4s1.794-4 4-4 4 1.794 4 4-1.794 4-4 4M70 50a1 1 0 0 0-1 1v11c0 3.859-3.141 7-7 7H50.91l3.293-3.293a.999.999 0 1 0-1.414-1.414l-5 5a.999.999 0 0 0 0 1.414l5 5a.997.997 0 0 0 1.414 0 .999.999 0 0 0 0-1.414L50.91 71H62c4.963 0 9-4.04 9-9V51a1 1 0 0 0-1-1"/></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/convdev/i2p_step_2.svg b/app/assets/images/illustrations/convdev/i2p_step_2.svg deleted file mode 100644 index 4280024c23c..00000000000 --- a/app/assets/images/illustrations/convdev/i2p_step_2.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76"><path d="M42.26 40.44a.989.989 0 0 0 1.109-.877l2.625-22.444a.997.997 0 0 0-.993-1.117h-14a1 1 0 0 0-.994 1.108l3.454 31.575a6.981 6.981 0 0 0-2.46 5.317c0 3.859 3.141 7 7 7s7-3.141 7-7-3.141-7-7-7c-.94 0-1.835.189-2.655.527l-3.23-29.527h11.761L41.383 39.33a1 1 0 0 0 .877 1.11m.741 13.562c0 2.757-2.243 5-5 5s-5-2.243-5-5 2.243-5 5-5 5 2.243 5 5"/><path d="M73.236 23.749a1 1 0 0 0-1.854.75A35.788 35.788 0 0 1 74 38c0 19.851-16.149 36-36 36S2 57.851 2 38 18.149 2 38 2c7.6 0 14.83 2.332 20.965 6.74A5.955 5.955 0 0 0 58 12c0 1.603.624 3.109 1.758 4.242A5.956 5.956 0 0 0 64 18a5.956 5.956 0 0 0 4.242-1.758C69.376 15.109 70 13.603 70 12s-.624-3.109-1.758-4.242A5.956 5.956 0 0 0 64 6a5.943 5.943 0 0 0-3.668 1.259C53.812 2.512 46.104 0 38 0 17.047 0 0 17.047 0 38s17.047 38 38 38 38-17.047 38-38c0-4.93-.93-9.725-2.764-14.251zM64 8c1.068 0 2.072.416 2.828 1.172S68 10.932 68 12s-.416 2.072-1.172 2.828c-1.512 1.512-4.145 1.512-5.656 0C60.416 14.072 60 13.068 60 12s.416-2.072 1.172-2.828S62.932 8 64 8z"/></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/convdev/i2p_step_3.svg b/app/assets/images/illustrations/convdev/i2p_step_3.svg deleted file mode 100644 index 7690f91b420..00000000000 --- a/app/assets/images/illustrations/convdev/i2p_step_3.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76"><path d="M12 8c0-3.309-2.691-6-6-6S0 4.691 0 8c0 2.967 2.167 5.431 5 5.91v8.181c-2.833.478-5 2.942-5 5.909s2.167 5.431 5 5.91v8.181c-2.833.478-5 2.942-5 5.909s2.167 5.431 5 5.91v8.181c-2.833.478-5 2.942-5 5.909 0 3.309 2.691 6 6 6s6-2.691 6-6c0-2.967-2.167-5.431-5-5.91v-8.18c2.833-.478 5-2.942 5-5.91s-2.167-5.431-5-5.91v-8.18c2.833-.478 5-2.942 5-5.91s-2.167-5.431-5-5.91v-8.18c2.833-.479 5-2.943 5-5.91M2 8c0-2.206 1.794-4 4-4s4 1.794 4 4-1.794 4-4 4-4-1.794-4-4m8 60c0 2.206-1.794 4-4 4s-4-1.794-4-4 1.794-4 4-4 4 1.794 4 4m0-20c0 2.206-1.794 4-4 4s-4-1.794-4-4 1.794-4 4-4 4 1.794 4 4m0-20c0 2.206-1.794 4-4 4s-4-1.794-4-4 1.794-4 4-4 4 1.794 4 4M21 6h54a1 1 0 1 0 0-2H21a1 1 0 1 0 0 2M21 12h35a1 1 0 1 0 0-2H21a1 1 0 1 0 0 2M75 24H21a1 1 0 1 0 0 2h54a1 1 0 1 0 0-2M21 32h34a1 1 0 1 0 0-2H21a1 1 0 1 0 0 2M75 44H21a1 1 0 1 0 0 2h54a1 1 0 1 0 0-2M21 52h34a1 1 0 1 0 0-2H21a1 1 0 1 0 0 2M75 64H21a1 1 0 1 0 0 2h54a1 1 0 1 0 0-2M55 70H21a1 1 0 1 0 0 2h34a1 1 0 1 0 0-2"/></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/convdev/i2p_step_4.svg b/app/assets/images/illustrations/convdev/i2p_step_4.svg deleted file mode 100644 index ba21b9e2c3a..00000000000 --- a/app/assets/images/illustrations/convdev/i2p_step_4.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76"><path d="M67.7 10h-6.751C60.442 4.402 55.728 0 50 0c-6.06 0-11 4.935-11 11s4.935 11 11 11c5.728 0 10.442-4.402 10.949-10H67.7c1.269 0 2.3.987 2.3 2.2v57.6c0 1.213-1.031 2.2-2.3 2.2H8.3C7.031 74 6 73.013 6 71.8V14.2C6 12.987 7.031 12 8.3 12h15.15a1 1 0 1 0 0-2H8.3C5.929 10 4 11.884 4 14.2v57.6C4 74.116 5.929 76 8.3 76h59.4c2.371 0 4.3-1.884 4.3-4.2V14.2c0-2.316-1.929-4.2-4.3-4.2M50 20c-4.963 0-9-4.04-9-9s4.04-9 9-9 9 4.04 9 9-4.04 9-9 9"/><path d="M21.293 29.29a.999.999 0 0 0 0 1.414l12.975 12.975-12.975 12.974a.999.999 0 1 0 1.414 1.414l13.682-13.682a.999.999 0 0 0 0-1.414L22.707 29.29a.999.999 0 0 0-1.414 0M54 59a1 1 0 1 0 0-2H42a1 1 0 1 0 0 2h12"/></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/convdev/i2p_step_5.svg b/app/assets/images/illustrations/convdev/i2p_step_5.svg deleted file mode 100644 index 3c8f8422a97..00000000000 --- a/app/assets/images/illustrations/convdev/i2p_step_5.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76"><path d="M48.949 37C48.442 31.402 43.728 27 38 27s-10.442 4.402-10.949 10h-13.05a1 1 0 1 0 0 2h13.05c.507 5.598 5.221 10 10.949 10s10.442-4.402 10.949-10h12.24a1 1 0 1 0 0-2h-12.24M38 47c-4.963 0-9-4.04-9-9s4.04-9 9-9 9 4.04 9 9-4.04 9-9 9"/><path d="M73.236 23.749a1 1 0 0 0-1.854.75A35.788 35.788 0 0 1 74 38c0 19.851-16.149 36-36 36S2 57.851 2 38 18.149 2 38 2c7.6 0 14.83 2.332 20.965 6.74A5.955 5.955 0 0 0 58 12c0 1.603.624 3.109 1.758 4.242A5.956 5.956 0 0 0 64 18a5.956 5.956 0 0 0 4.242-1.758C69.376 15.109 70 13.603 70 12s-.624-3.109-1.758-4.242A5.956 5.956 0 0 0 64 6a5.943 5.943 0 0 0-3.668 1.259C53.812 2.512 46.104 0 38 0 17.047 0 0 17.047 0 38s17.047 38 38 38 38-17.047 38-38c0-4.93-.93-9.725-2.764-14.251zM64 8c1.068 0 2.072.416 2.828 1.172S68 10.932 68 12s-.416 2.072-1.172 2.828c-1.512 1.512-4.145 1.512-5.656 0C60.416 14.072 60 13.068 60 12s.416-2.072 1.172-2.828S62.932 8 64 8z"/></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/convdev/i2p_step_6.svg b/app/assets/images/illustrations/convdev/i2p_step_6.svg deleted file mode 100644 index 933860798ad..00000000000 --- a/app/assets/images/illustrations/convdev/i2p_step_6.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76"><path d="M14.267 7.32l-4.896 5.277-1.702-1.533a.999.999 0 1 0-1.338 1.486l2.434 2.192c.064.058.139.091.212.13.035.018.065.048.101.062a.99.99 0 0 0 .752-.016c.044-.019.077-.058.118-.084.076-.047.155-.086.219-.154l5.566-6a1 1 0 0 0-1.466-1.36M31 9h44a1 1 0 1 0 0-2H31a1 1 0 1 0 0 2M31 15h24a1 1 0 1 0 0-2H31a1 1 0 1 0 0 2"/><path d="M11 0C4.93 0 0 4.935 0 11s4.935 11 11 11 11-4.935 11-11S17.065 0 11 0m0 20c-4.963 0-9-4.04-9-9s4.04-9 9-9 9 4.04 9 9-4.04 9-9 9M14.267 34.32l-4.896 5.277-1.702-1.533a1 1 0 1 0-1.338 1.486l2.434 2.192c.064.058.139.091.212.13.035.018.065.048.101.062a.99.99 0 0 0 .752-.016c.044-.019.077-.058.118-.084.076-.047.155-.086.219-.154l5.566-6a1 1 0 0 0-1.466-1.36M75 34H31a1 1 0 1 0 0 2h44a1 1 0 1 0 0-2M31 42h24a1 1 0 1 0 0-2H31a1 1 0 1 0 0 2"/><path d="M11 27C4.93 27 0 31.935 0 38s4.935 11 11 11 11-4.935 11-11-4.935-11-11-11m0 20c-4.963 0-9-4.04-9-9s4.04-9 9-9 9 4.04 9 9-4.04 9-9 9M14.267 61.32l-4.896 5.277-1.702-1.533a1 1 0 1 0-1.338 1.486l2.434 2.192c.064.058.139.091.212.13.035.018.065.048.101.062a.99.99 0 0 0 .752-.016c.044-.019.077-.058.118-.084.076-.047.155-.086.219-.154l5.566-6a1 1 0 0 0-1.466-1.36"/><path d="M11 54C4.93 54 0 58.935 0 65s4.935 11 11 11 11-4.935 11-11-4.935-11-11-11m0 20c-4.963 0-9-4.04-9-9s4.04-9 9-9 9 4.04 9 9-4.04 9-9 9M75 61H31a1 1 0 1 0 0 2h44a1 1 0 1 0 0-2M55 67H31a1 1 0 1 0 0 2h24a1 1 0 1 0 0-2"/></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/convdev/i2p_step_7.svg b/app/assets/images/illustrations/convdev/i2p_step_7.svg deleted file mode 100644 index d97c8f7c2d4..00000000000 --- a/app/assets/images/illustrations/convdev/i2p_step_7.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76"><path d="M73.236 23.749a1 1 0 1 0-1.854.75A35.788 35.788 0 0 1 74 38c0 19.851-16.149 36-36 36S2 57.851 2 38 18.149 2 38 2c7.6 0 14.83 2.332 20.965 6.74A5.955 5.955 0 0 0 58 12c0 1.603.624 3.109 1.758 4.242A5.956 5.956 0 0 0 64 18a5.956 5.956 0 0 0 4.242-1.758C69.376 15.109 70 13.603 70 12s-.624-3.109-1.758-4.242A5.956 5.956 0 0 0 64 6a5.943 5.943 0 0 0-3.668 1.259C53.812 2.512 46.104 0 38 0 17.047 0 0 17.047 0 38s17.047 38 38 38 38-17.047 38-38c0-4.93-.93-9.725-2.764-14.251zM64 8c1.068 0 2.072.416 2.828 1.172S68 10.932 68 12s-.416 2.072-1.172 2.828c-1.512 1.512-4.145 1.512-5.656 0C60.416 14.072 60 13.068 60 12s.416-2.072 1.172-2.828S62.932 8 64 8z"/><path d="M27.19 32.17a.997.997 0 0 0-1.366-.364L13.17 39.132a1 1 0 0 0 0 1.73l12.654 7.326a1 1 0 0 0 1.002-1.73l-11.159-6.461 11.159-6.461a.998.998 0 0 0 .364-1.366M48.808 47.827a1 1 0 0 0 1.366.364l12.654-7.326a1 1 0 0 0 0-1.73l-12.654-7.326a1 1 0 0 0-1.002 1.73L60.331 40l-11.159 6.461a.998.998 0 0 0-.364 1.366M42.71 23.06L31.398 56.29a1 1 0 0 0 1.892.645l11.312-33.23a1 1 0 0 0-1.892-.645"/></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/convdev/i2p_step_8.svg b/app/assets/images/illustrations/convdev/i2p_step_8.svg deleted file mode 100644 index 919bbeff319..00000000000 --- a/app/assets/images/illustrations/convdev/i2p_step_8.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76"><path d="M62.44 54.765l-9.912-11.09c.315-3.881.481-7.241.508-10.271-.029-13.871-3.789-23.05-13.413-32.746-.855-.859-2.411-.828-3.294.059-7.594 7.65-11.139 13.934-12.575 22.3a6.94 6.94 0 0 0-4.699 2.039c-1.321 1.321-2.05 3.079-2.05 4.949s.729 3.628 2.051 4.949c1.321 1.322 3.079 2.051 4.949 2.051s3.628-.729 4.949-2.051a6.951 6.951 0 0 0 2.051-4.949 6.955 6.955 0 0 0-2.051-4.949c-.9-.9-2-1.517-3.205-1.824 1.373-7.859 4.764-13.818 11.999-21.11.128-.13.356-.158.456-.059 9.207 9.274 12.805 18.06 12.832 31.33-.026 3.079-.202 6.527-.536 10.54a.997.997 0 0 0 .25.749l10.166 11.379c.062.076.109.23.093.32l-4.547 17.407c-.004.015-.009.036-.079.106a.403.403 0 0 1-.2.106l-3.577.002c-.144-.009-.265-.077-.309-.153l-5.425-10.328a1.002 1.002 0 0 0-.886-.535H30.024c-.371 0-.713.206-.886.535l-5.407 10.303-.069.072a.366.366 0 0 1-.199.105l-3.588.001c-.179-.009-.304-.123-.33-.227l-4.531-17.338a.525.525 0 0 1 .049-.34L25.26 44.682a1 1 0 0 0-1.492-1.332L13.539 54.803c-.448.554-.63 1.312-.474 2.084l4.544 17.396c.253.963 1.146 1.669 2.218 1.719h3.636c.581 0 1.187-.261 1.615-.693.114-.114.286-.286.406-.528l5.144-9.793h14.754l5.16 9.822c.396.697 1.124 1.143 2.01 1.192l3.712-.003a2.396 2.396 0 0 0 1.544-.694c.313-.316.504-.646.598-1.022l4.557-17.451a2.502 2.502 0 0 0-.518-2.066M29.01 30.001c0 1.335-.521 2.591-1.465 3.535s-2.2 1.465-3.535 1.465-2.591-.521-3.535-1.465-1.465-2.2-1.465-3.535.521-2.591 1.465-3.535 2.2-1.465 3.535-1.465 2.591.521 3.535 1.465 1.465 2.2 1.465 3.535"/></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/convdev/i2p_step_9.svg b/app/assets/images/illustrations/convdev/i2p_step_9.svg deleted file mode 100644 index 2d1b10d430d..00000000000 --- a/app/assets/images/illustrations/convdev/i2p_step_9.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76"><path d="M68 67c-1.725 0-3.36.541-4.723 1.545A12.998 12.998 0 0 0 52 62c-2.734 0-5.359.853-7.555 2.43L42.159 49h1.228l3.829 7.645c.339.598.962.979 1.724 1.022l2.812-.003a2.07 2.07 0 0 0 1.316-.595c.264-.266.433-.559.514-.882l3.433-13.145a2.138 2.138 0 0 0-.449-1.763l-7.385-8.268c.231-2.875.354-5.376.374-7.641C49.532 14.863 46.684 7.908 39.393.564c-.737-.742-2.072-.715-2.829.044-5.617 5.659-8.309 10.336-9.446 16.463a5.95 5.95 0 0 0-3.36 1.686C22.624 19.891 22 21.397 22 23s.624 3.109 1.758 4.242C24.891 28.376 26.397 29 28 29s3.109-.624 4.242-1.758C33.376 26.109 34 24.603 34 23s-.624-3.109-1.758-4.242a5.952 5.952 0 0 0-3.098-1.648c1.095-5.538 3.637-9.855 8.83-15.14 6.874 6.924 9.561 13.485 9.581 23.392-.021 2.316-.151 4.903-.402 7.91a.999.999 0 0 0 .25.749l7.663 8.572-3.391 13.07-2.695.036-4.081-8.15a1.001 1.001 0 0 0-.895-.553h-12.01c-.379 0-.725.214-.895.553l-4.04 8.114-2.707.015-3.427-13.07 7.671-8.588a1 1 0 0 0-1.492-1.332l-7.7 8.623c-.383.47-.54 1.116-.406 1.787l3.419 13.08c.216.829.98 1.438 1.907 1.48h2.735c.508 0 1.016-.218 1.391-.595.091-.09.242-.241.358-.475l3.804-7.597h1.228l-2.286 15.43a12.914 12.914 0 0 0-7.555-2.43c-4.685 0-8.979 2.53-11.277 6.545a7.943 7.943 0 0 0-4.723-1.545c-4.411 0-8 3.589-8 8a1 1 0 0 0 1 1h74a1 1 0 0 0 1-1c0-4.411-3.589-8-8-8m-36-44a3.973 3.973 0 0 1-1.172 2.828c-1.512 1.512-4.145 1.512-5.656 0-.756-.756-1.172-1.76-1.172-2.828s.416-2.072 1.172-2.828 1.76-1.172 2.828-1.172 2.072.416 2.828 1.172 1.172 1.76 1.172 2.828m-29.917 51a6.01 6.01 0 0 1 5.917-5c1.638 0 3.17.652 4.313 1.836a.998.998 0 0 0 1.634-.289 11.011 11.011 0 0 1 10.05-6.547c2.836 0 5.532 1.085 7.593 3.055a1.001 1.001 0 0 0 1.681-.576l2.588-17.479h4.275l2.589 17.479a.999.999 0 1 0 1.681.576 10.945 10.945 0 0 1 7.593-3.055c4.343 0 8.288 2.57 10.05 6.547a.998.998 0 0 0 1.634.289 5.948 5.948 0 0 1 4.313-1.836 6.01 6.01 0 0 1 5.917 5H2.076"/></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/epics.svg b/app/assets/images/illustrations/epics.svg deleted file mode 100644 index 1a37e6bba5f..00000000000 --- a/app/assets/images/illustrations/epics.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="430" height="300" viewBox="0 0 430 300"><g fill="none" fill-rule="evenodd"><g transform="translate(75 53)"><rect width="284" height="208" y="5" fill="#F9F9F9" rx="10"/><rect width="284" height="208" fill="#FFF" rx="10"/><path fill="#EEE" fill-rule="nonzero" d="M10 4a6 6 0 0 0-6 6v188a6 6 0 0 0 6 6h264a6 6 0 0 0 6-6V10a6 6 0 0 0-6-6H10zm0-4h264c5.523 0 10 4.477 10 10v188c0 5.523-4.477 10-10 10H10c-5.523 0-10-4.477-10-10V10C0 4.477 4.477 0 10 0z"/><path fill="#EEE" fill-rule="nonzero" d="M25.168 153.995c3.837-.215 7.173.028 10.119.691a3 3 0 1 0 1.318-5.853c-3.509-.79-7.4-1.074-11.773-.828a3 3 0 1 0 .336 5.99zm19.043 4.66c2.401 1.704 4.388 3.61 7.569 7.083a3 3 0 0 0 4.424-4.054c-3.448-3.763-5.686-5.911-8.522-7.923a3 3 0 1 0-3.471 4.894zm15.575 15.173c3.181 2.675 6.52 4.665 10.397 6.039a3 3 0 0 0 2.004-5.655c-3.162-1.121-5.884-2.743-8.54-4.976a3 3 0 1 0-3.861 4.592zm22.133 8.148c1.02.037 2.067.045 3.143.023a72.664 72.664 0 0 0 8.346-.638 3 3 0 1 0-.812-5.945c-2.442.334-4.996.53-7.658.585a48.55 48.55 0 0 1-2.796-.021 3 3 0 0 0-.223 5.996zm22.778-3.286c3.9-1.37 7.427-3.15 10.54-5.305a3 3 0 0 0-3.415-4.933c-2.665 1.845-5.712 3.382-9.114 4.578a3 3 0 0 0 1.989 5.66zm19.156-13.62a33.752 33.752 0 0 0 5.276-10.817 3 3 0 1 0-5.773-1.633 27.753 27.753 0 0 1-4.341 8.9 3 3 0 1 0 4.838 3.55zm6.577-22.657c-.187-3.817-.926-7.71-2.204-11.596a3 3 0 0 0-5.7 1.874c1.113 3.384 1.75 6.745 1.91 10.016a3 3 0 1 0 5.994-.294zm-7.097-22.26c-1.897-3.2-4.152-6.325-6.748-9.344a3 3 0 0 0-4.55 3.913c2.372 2.756 4.421 5.597 6.136 8.49a3 3 0 0 0 5.162-3.06zm-11.546-17.793c-.938-3.025-1.402-6.42-1.365-9.976a3 3 0 0 0-6-.063c-.043 4.163.506 8.177 1.634 11.816a3 3 0 1 0 5.731-1.777zm.053-20.107c.905-3.341 2.22-6.538 3.904-9.448a3 3 0 0 0-5.194-3.004c-1.948 3.368-3.463 7.048-4.501 10.884a3 3 0 1 0 5.791 1.568zm10.134-17.305c2.475-2.28 5.265-4.09 8.335-5.374a3 3 0 1 0-2.314-5.536c-3.725 1.558-7.105 3.75-10.086 6.497a3 3 0 1 0 4.065 4.413zm18.177-7.586c3.202-.18 6.599.092 10.18.843a3 3 0 0 0 1.23-5.872c-4.086-.857-8.009-1.172-11.747-.962a3 3 0 1 0 .337 5.99zm20.047 3.95c3.068 1.268 6.232 2.842 9.487 4.728a3 3 0 0 0 3.009-5.191c-3.48-2.017-6.883-3.71-10.204-5.083a3 3 0 1 0-2.292 5.545zm19.578 9.955c3.711 1.586 7.376 2.77 10.997 3.565a3 3 0 0 0 1.286-5.86c-3.248-.713-6.555-1.782-9.925-3.222a3 3 0 1 0-2.358 5.517zm22.591 4.789c3.94-.04 7.808-.553 11.61-1.513a3 3 0 1 0-1.468-5.817 43.358 43.358 0 0 1-10.203 1.33 3 3 0 0 0 .061 6zm22.52-5.558c3.335-1.637 6.607-3.613 9.845-5.916a3 3 0 1 0-3.477-4.89c-2.984 2.122-5.98 3.931-9.011 5.42a3 3 0 1 0 2.643 5.386zm18.678-13.054a3 3 0 0 1-4.02-4.454 130.547 130.547 0 0 0 5.31-5.088 3 3 0 1 1 4.265 4.22 136.507 136.507 0 0 1-5.555 5.322zm-48.722 25.641a3 3 0 1 1 4.314-4.17c3.056 3.16 5.075 6.744 6.172 10.754a3 3 0 0 1-5.787 1.584c-.834-3.047-2.35-5.739-4.699-8.168zm5.347 18.049a3 3 0 1 1 5.978.52c-.282 3.232-.805 6.273-1.832 11.206a3 3 0 0 1-5.874-1.222c.981-4.717 1.473-7.572 1.728-10.504zm-3.777 21.555a3 3 0 0 1 5.953.747c-.5 3.988-.397 7.09.399 9.67a3 3 0 1 1-5.733 1.769c-1.087-3.52-1.217-7.426-.62-12.186zm7.393 22.444a3 3 0 0 1 4.461-4.013c2.703 3.005 5.224 5.296 7.594 6.947a3 3 0 0 1-3.429 4.924c-2.775-1.932-5.632-4.53-8.626-7.858zm20.352 12.28a3 3 0 1 1 .334-5.99c2.77.154 5.453-.554 9.224-2.254a3 3 0 0 1 2.466 5.47c-4.57 2.06-8.103 2.993-12.024 2.775zm21.784-7.058a3 3 0 0 1-1.815-5.719c4.227-1.342 8.24-1.61 12.496-.572a3 3 0 0 1-1.421 5.83c-3.116-.76-6.025-.566-9.26.46zM106.53 56.038a3 3 0 1 1-3.45 4.909c-1.074-.755-6.723-6.044-8.083-7.204a68.019 68.019 0 0 0-.332-.281 3 3 0 1 1 3.865-4.59l.362.306c1.643 1.402 6.971 6.391 7.638 6.86zM88.536 42.422a3 3 0 0 1-2.285 5.548c-3.14-1.293-5.78-1.34-8.105-.05a3 3 0 0 1-2.91-5.247c4.087-2.266 8.597-2.187 13.3-.25zM66.698 48.73a3 3 0 0 1 2.029 5.647c-4.432 1.592-8.786.835-13.166-1.88a3 3 0 1 1 3.16-5.1c2.93 1.816 5.425 2.25 7.977 1.333zm-15.636-8.038a3 3 0 0 1-4.352 4.13c-.911-.96-1.85-1.98-3.061-3.32-.295-.325-2.437-2.703-3.07-3.4-.47-.518-.9-.988-1.313-1.436a3 3 0 0 1 4.41-4.068c.425.46.866.942 1.346 1.47.642.709 2.79 3.092 3.076 3.41a180.865 180.865 0 0 0 2.964 3.214z"/><path fill="#E1DBF1" d="M254.66 72.196l2-3.464a2 2 0 1 0-3.464-2l-2 3.464-3.464-2a2 2 0 0 0-2 3.464l3.464 2-2 3.464a2 2 0 0 0 3.464 2l2-3.464 3.464 2a2 2 0 1 0 2-3.464l-3.464-2zm-151.904 78.732l2.829-2.828a2 2 0 0 0-2.829-2.829l-2.828 2.829-2.828-2.829a2 2 0 0 0-2.829 2.829l2.829 2.828-2.829 2.829a2 2 0 1 0 2.829 2.828l2.828-2.828 2.828 2.828a2 2 0 1 0 2.829-2.828l-2.829-2.829z"/><path fill="#6B4FBB" d="M210.66 173.66l3.464-2a2 2 0 1 0-2-3.464l-3.464 2-2-3.464a2 2 0 0 0-3.464 2l2 3.464-3.464 2a2 2 0 1 0 2 3.464l3.464-2 2 3.464a2 2 0 1 0 3.464-2l-2-3.464z"/><path fill="#FDC4A8" fill-rule="nonzero" d="M27 181a8 8 0 1 1 0-16 8 8 0 0 1 0 16zm0-4a4 4 0 1 0 0-8 4 4 0 0 0 0 8z"/><path fill="#C3B8E3" fill-rule="nonzero" d="M138 85a7 7 0 1 1 0-14 7 7 0 0 1 0 14zm0-4a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/><path fill="#6B4FBB" fill-rule="nonzero" d="M200 57a7 7 0 1 1 0-14 7 7 0 0 1 0 14zm0-4a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/><path fill="#FC6D26" fill-rule="nonzero" d="M222.647 121.647v5h5v-5h-5zm0-4h5a4 4 0 0 1 4 4v5a4 4 0 0 1-4 4h-5a4 4 0 0 1-4-4v-5a4 4 0 0 1 4-4z"/><path fill="#FEE1D3" fill-rule="nonzero" d="M103.647 28.647v5h5v-5h-5zm0-4h5a4 4 0 0 1 4 4v5a4 4 0 0 1-4 4h-5a4 4 0 0 1-4-4v-5a4 4 0 0 1 4-4z"/><path fill="#FC6D26" fill-rule="nonzero" d="M85 103.488L81.841 108h6.318L85 103.488zm6.436 2.218A4 4 0 0 1 88.159 112H81.84a4 4 0 0 1-3.277-6.294l3.16-4.512a4 4 0 0 1 6.553 0l3.159 4.512z"/></g><path fill="#F9F9F9" d="M334.376 99.43A48.805 48.805 0 0 0 366 111c27.062 0 49-21.938 49-49s-21.938-49-49-49-49 21.938-49 49c0 9.454 2.677 18.283 7.315 25.77l-3.05 11.306a2.5 2.5 0 0 0 3.064 3.065l10.047-2.71z"/><path fill="#FFF" d="M339.376 94.43A48.805 48.805 0 0 0 371 106c27.062 0 49-21.938 49-49S398.062 8 371 8s-49 21.938-49 49c0 9.454 2.677 18.283 7.315 25.77l-3.05 11.306a2.5 2.5 0 0 0 3.064 3.065l10.047-2.71z"/><path fill="#EEE" fill-rule="nonzero" d="M329.85 99.072a4.5 4.5 0 0 1-5.516-5.517l2.827-10.48C322.501 75.258 320 66.31 320 57c0-28.167 22.833-51 51-51s51 22.833 51 51-22.833 51-51 51c-11.859 0-23.096-4.064-32.102-11.37l-9.048 2.442zm10.817-6.169C349.091 100.027 359.737 104 371 104c25.957 0 47-21.043 47-47s-21.043-47-47-47-47 21.043-47 47c0 8.859 2.453 17.351 7.016 24.716l.456.737-3.277 12.144c.072.527.347.685.613.613l11.059-2.984.8.677z"/><g transform="translate(354 34)"><path fill="#E1DBF1" fill-rule="nonzero" d="M13 4a1 1 0 0 0-1 1v1a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1h-8zm0-4h8a5 5 0 0 1 5 5v1a5 5 0 0 1-5 5h-8a5 5 0 0 1-5-5V5a5 5 0 0 1 5-5z"/><path fill="#6B4FBB" fill-rule="nonzero" d="M5 11a1 1 0 0 0 0 2h24a1 1 0 0 0 0-2H5zm0-4h24a5 5 0 0 1 0 10H5A5 5 0 0 1 5 7z"/><rect width="12" height="4" x="11" y="31" fill="#C3B8E3" rx="2"/><rect width="12" height="4" x="11" y="19" fill="#C3B8E3" rx="2"/><rect width="12" height="4" x="11" y="37" fill="#E1DBF1" rx="2"/><rect width="12" height="4" x="11" y="43" fill="#C3B8E3" rx="2"/><rect width="12" height="4" x="11" y="25" fill="#E1DBF1" rx="2"/></g><path fill="#F9F9F9" d="M344.238 225.072A38.83 38.83 0 0 1 368 217c21.54 0 39 17.46 39 39s-17.46 39-39 39-39-17.46-39-39a38.84 38.84 0 0 1 4.001-17.227l-3.737-13.85a2.5 2.5 0 0 1 3.065-3.064l11.91 3.213z"/><path fill="#FFF" d="M348.238 221.072A38.83 38.83 0 0 1 372 213c21.54 0 39 17.46 39 39s-17.46 39-39 39-39-17.46-39-39a38.84 38.84 0 0 1 4.001-17.227l-3.737-13.85a2.5 2.5 0 0 1 3.065-3.064l11.91 3.213z"/><path fill="#EEE" fill-rule="nonzero" d="M336.85 215.928a4.5 4.5 0 0 0-5.516 5.517l3.543 13.13A40.848 40.848 0 0 0 331 252c0 22.644 18.356 41 41 41s41-18.356 41-41-18.356-41-41-41a40.82 40.82 0 0 0-24.182 7.887l-10.968-2.96zm12.608 6.73A36.824 36.824 0 0 1 372 215c20.435 0 37 16.565 37 37s-16.565 37-37 37-37-16.565-37-37c0-5.747 1.31-11.304 3.795-16.343l.334-.677-3.934-14.577a.5.5 0 0 1 .613-.613l12.865 3.471.785-.604z"/><path fill="#FEE1D3" fill-rule="nonzero" d="M356.097 255.962a7 7 0 0 0 8.81 10.88l1.093-.885v1.454a7 7 0 1 0 14 0v-1.454l1.092.885a7 7 0 1 0 8.81-10.88l-1.185-.96 1.455-.337a7 7 0 1 0-3.15-13.64l-1.4.323.623-1.278a7 7 0 0 0-12.583-6.137l-.662 1.356-.662-1.356a7 7 0 0 0-12.583 6.137l.623 1.278-1.4-.324a7 7 0 1 0-3.15 13.641l1.455.336-1.186.96zm5.464-.913a11.914 11.914 0 0 1-.444-1.95l-.19-1.362-4.2-.97a3 3 0 0 1 1.35-5.845l4.178.964.768-1.145c.373-.557.793-1.082 1.254-1.57l.95-1.006-1.877-3.849a3 3 0 0 1 5.393-2.63l1.892 3.879 1.363-.113a12.188 12.188 0 0 1 2.004 0l1.363.113 1.892-3.879a3 3 0 0 1 5.393 2.63l-1.877 3.849.95 1.006c.461.488.88 1.013 1.254 1.57l.768 1.145 4.178-.964a3 3 0 1 1 1.35 5.846l-4.2.97-.19 1.36a11.914 11.914 0 0 1-.444 1.95l-.413 1.302 3.36 2.72a3 3 0 1 1-3.776 4.663l-3.32-2.688-1.196.706a11.94 11.94 0 0 1-1.808.873l-1.286.492v4.295a3 3 0 1 1-6 0v-4.295l-1.286-.492a11.94 11.94 0 0 1-1.808-.873l-1.196-.706-3.32 2.688a3 3 0 1 1-3.776-4.663l3.36-2.72-.413-1.301z"/><path fill="#FC6D26" fill-rule="nonzero" d="M373 245.411a6 6 0 1 0 0 12 6 6 0 0 0 0-12zm0 4a2 2 0 1 1 0 4 2 2 0 0 1 0-4z"/><g><path fill="#F9F9F9" d="M94.624 162.43A48.805 48.805 0 0 1 63 174c-27.062 0-49-21.938-49-49s21.938-49 49-49 49 21.938 49 49c0 9.454-2.677 18.283-7.315 25.77l3.05 11.306a2.5 2.5 0 0 1-3.064 3.065l-10.047-2.71z"/><path fill="#FFF" stroke="#EEE" stroke-width="4" d="M89.624 157.43A48.805 48.805 0 0 1 58 169c-27.062 0-49-21.938-49-49s21.938-49 49-49 49 21.938 49 49c0 9.454-2.677 18.283-7.315 25.77l3.05 11.306a2.5 2.5 0 0 1-3.064 3.065l-10.047-2.71z"/><path fill="#EEE" fill-rule="nonzero" d="M99.15 162.072a4.5 4.5 0 0 0 5.516-5.517l-2.827-10.48C106.499 138.258 109 129.31 109 120c0-28.167-22.833-51-51-51S7 91.833 7 120s22.833 51 51 51c11.859 0 23.096-4.064 32.102-11.37l9.048 2.442zm-10.817-6.169C79.909 163.027 69.263 167 58 167c-25.957 0-47-21.043-47-47s21.043-47 47-47 47 21.043 47 47c0 8.859-2.453 17.351-7.016 24.716l-.456.737 3.277 12.144c-.072.527-.347.685-.613.613l-11.059-2.984-.8.677z"/><g fill-rule="nonzero"><path fill="#FEE1D3" d="M55.47 94.47l-16.148 6.688a4 4 0 0 0-2.164 2.164l-6.689 16.147a4 4 0 0 0 0 3.062l6.689 16.147a4 4 0 0 0 2.164 2.164l16.147 6.689a4 4 0 0 0 3.062 0l16.147-6.689a4 4 0 0 0 2.164-2.164l6.689-16.147a4 4 0 0 0 0-3.062l-6.689-16.147a4 4 0 0 0-2.164-2.164L58.53 94.469a4 4 0 0 0-3.062 0zM57 98.164l16.147 6.688L79.835 121l-6.688 16.147L57 143.835l-16.147-6.688L34.165 121l6.688-16.147L57 98.165zM57 107a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm12 4a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm4 12a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm-4 11a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm-12 6a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm-12-6a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm-4-11a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm4-11a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm12 20c6.075 0 11-4.925 11-11s-4.925-11-11-11-11 4.925-11 11 4.925 11 11 11zm0-4a7 7 0 1 1 0-14 7 7 0 0 1 0 14z"/><path fill="#FC6D26" d="M57 126.5a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zm0-3a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5z"/></g></g></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/gitlab_logo.svg b/app/assets/images/illustrations/gitlab_logo.svg deleted file mode 100644 index 8dbd75a340e..00000000000 --- a/app/assets/images/illustrations/gitlab_logo.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="492.509" height="453.68" viewBox="0 0 492.50943 453.67966"><g fill="none" fill-rule="evenodd"><path d="M491.589 259.398l-27.559-84.814L409.413 6.486c-2.81-8.648-15.045-8.648-17.856 0l-54.619 168.098H155.572L100.952 6.486c-2.81-8.648-15.046-8.648-17.856 0L28.478 174.584.921 259.398a18.775 18.775 0 0 0 6.82 20.992l238.513 173.29L484.77 280.39a18.777 18.777 0 0 0 6.82-20.992" fill="#fc6d26"/><path d="M246.255 453.68l90.684-279.096H155.57z" fill="#e24329"/><path d="M246.255 453.68L155.57 174.583H28.479z" fill="#fc6d26"/><path d="M28.479 174.584L.92 259.4a18.773 18.773 0 0 0 6.821 20.99l238.514 173.29z" fill="#fca326"/><path d="M28.479 174.584H155.57L100.952 6.487c-2.81-8.65-15.047-8.65-17.856 0z" fill="#e24329"/><path d="M246.255 453.68l90.684-279.096H464.03z" fill="#fc6d26"/><path d="M464.03 174.584l27.56 84.815a18.773 18.773 0 0 1-6.822 20.99L246.255 453.68z" fill="#fca326"/><path d="M464.03 174.584H336.94L391.557 6.487c2.811-8.65 15.047-8.65 17.856 0z" fill="#e24329"/></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/image_comment_light_cursor.svg b/app/assets/images/illustrations/image_comment_light_cursor.svg deleted file mode 100644 index ac712ea0c96..00000000000 --- a/app/assets/images/illustrations/image_comment_light_cursor.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 38 38"><g fill="none" fill-rule="evenodd"><circle cx="19" cy="19" r="18" fill="#FFF"/><path fill="#1F78D1" fill-rule="nonzero" d="M19 38C8.507 38 0 29.493 0 19S8.507 0 19 0s19 8.507 19 19-8.507 19-19 19zm0-2c9.389 0 17-7.611 17-17S28.389 2 19 2 2 9.611 2 19s7.611 17 17 17zm-6.293-8.293c-.63.63-1.707.184-1.707-.707V15a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3h-7.586l-3.707 3.707zM13 24.586l2.293-2.293A1 1 0 0 1 16 22h8a1 1 0 0 0 1-1v-6a1 1 0 0 0-1-1H14a1 1 0 0 0-1 1v9.586z"/></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/image_comment_light_cursor@2x.svg b/app/assets/images/illustrations/image_comment_light_cursor@2x.svg deleted file mode 100644 index 02943acd9d7..00000000000 --- a/app/assets/images/illustrations/image_comment_light_cursor@2x.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 38 38"><g fill="none" fill-rule="evenodd"><circle cx="19" cy="19" r="18" fill="#FFF"/><path fill="#1F78D1" fill-rule="nonzero" d="M19 38C8.507 38 0 29.493 0 19S8.507 0 19 0s19 8.507 19 19-8.507 19-19 19zm0-2c9.389 0 17-7.611 17-17S28.389 2 19 2 2 9.611 2 19s7.611 17 17 17zm-6.293-8.293c-.63.63-1.707.184-1.707-.707V15a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3h-7.586l-3.707 3.707zM13 24.586l2.293-2.293A1 1 0 0 1 16 22h8a1 1 0 0 0 1-1v-6a1 1 0 0 0-1-1H14a1 1 0 0 0-1 1v9.586z"/></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/issues.svg b/app/assets/images/illustrations/issues.svg deleted file mode 100644 index c8e0504732d..00000000000 --- a/app/assets/images/illustrations/issues.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="790 253 425 254" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="a" width="25" height="8.942" x="25" y="88.423" rx="2"/><mask id="h" width="25" height="8.942" x="0" y="0" fill="#fff"><use xlink:href="#a"/></mask><path id="b" d="M16 29.801h43v61.603H16z"/><mask id="i" width="43" height="61.603" x="0" y="0" fill="#fff"><use xlink:href="#b"/></mask><path id="c" d="M57 60.603l13.187 9.358c.449.32.876 1.015.955 1.568l3.575 24.863c.157 1.086-.253 1.257-.912.384L66 86.436l-9-6.955"/><mask id="j" width="17.75" height="36.731" x="0" y="0" fill="#fff"><use xlink:href="#c"/></mask><path id="d" d="M.25 60.603l13.186 9.358c.45.32.876 1.015.956 1.568l3.575 24.863c.156 1.086-.253 1.257-.912.384l-7.806-10.34-9-6.955"/><mask id="k" width="17.75" height="36.731" x="0" y="0" fill="#fff"><use xlink:href="#d"/></mask><path id="e" d="M16 29.801L35.786 1.456c.947-1.357 2.48-1.36 3.428 0L59 29.8"/><mask id="l" width="43" height="29.364" x="0" y="0" fill="#fff"><use xlink:href="#e"/></mask><rect id="f" width="26.265" height="35.509" x="6.367" rx="13.133"/><mask id="m" width="26.265" height="35.509" x="0" y="0" fill="#fff"><use xlink:href="#f"/></mask><rect id="g" width="16.837" height="22.386" x="4.082" rx="8.418"/><mask id="n" width="16.837" height="22.386" x="0" y="0" fill="#fff"><use xlink:href="#g"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(792 255)"><path d="M225.437 59.587c-.059.59-.132 1.27-.22 2.03a178.367 178.367 0 0 1-.965 7.07 1.5 1.5 0 1 0 2.963.465c.4-2.553.726-4.975.982-7.19a137.446 137.446 0 0 0 .297-2.832 1.5 1.5 0 1 0-2.989-.26c-.01.123-.033.365-.068.717zm-5.563 28.354a1.5 1.5 0 0 0 2.853.929c.975-2.997 1.849-6.283 2.628-9.797a1.5 1.5 0 1 0-2.928-.65c-.76 3.426-1.61 6.62-2.553 9.518zm-9.947 15.225a1.5 1.5 0 1 0 1.001 2.828c2.98-1.055 5.542-3.68 7.78-7.627a1.5 1.5 0 0 0-2.61-1.48c-1.915 3.378-3.995 5.508-6.171 6.279zm-19.488 4.417a1.5 1.5 0 1 0 1.164 2.765c3.12-1.314 6.272-2.324 9.258-2.981a1.5 1.5 0 1 0-.645-2.93c-3.167.697-6.491 1.763-9.777 3.146zm-17.208 11.043a1.5 1.5 0 0 0 2.066 2.175c2.282-2.169 4.866-4.162 7.676-5.946a1.5 1.5 0 0 0-1.608-2.533c-2.97 1.885-5.707 3.998-8.134 6.304zm-10.777 17.623a1.5 1.5 0 1 0 2.91.732c.768-3.054 2.041-5.977 3.78-8.748a1.5 1.5 0 0 0-2.54-1.595c-1.903 3.032-3.302 6.244-4.15 9.611zm-.265 20.444a1.5 1.5 0 1 0 2.977-.375c-.367-2.91-.58-6.137-.645-9.817a1.5 1.5 0 0 0-3 .053c.067 3.783.287 7.116.668 10.139zm6.219 19.472a1.5 1.5 0 0 0 2.652-1.403c-1.674-3.162-2.903-5.995-3.848-8.943a1.5 1.5 0 1 0-2.857.916c1.003 3.127 2.302 6.12 4.053 9.43zm7.566 12.77a595.837 595.837 0 0 1 2.73 4.475 1.5 1.5 0 0 0 2.569-1.551 626.463 626.463 0 0 0-2.744-4.495c.08.13-1.954-3.173-2.486-4.04a1.5 1.5 0 1 0-2.558 1.567c.534.87 2.571 4.178 2.489 4.045zm8.856 22.447a1.5 1.5 0 0 0 3-.039 32.214 32.214 0 0 0-1.837-10.326 1.5 1.5 0 0 0-2.828.999 29.212 29.212 0 0 1 1.665 9.366zm-5.483 18.028a1.5 1.5 0 0 0 2.497 1.662 36.203 36.203 0 0 0 4.488-9.416 1.5 1.5 0 0 0-2.868-.882 33.197 33.197 0 0 1-4.117 8.636z" fill="#FDE5D8"/><g transform="rotate(60 126.799 371.622)"><path stroke="#FDE5D8" stroke-width="3" d="M19 154l10-52.66m16 0L55 154" stroke-linecap="round"/><rect width="3" height="38.75" x="35" y="99.353" fill="#FDE5D8" rx="1.5"/><use fill="#FFF" stroke="#FDE5D8" stroke-width="6" mask="url(#h)" xlink:href="#a"/><use stroke="#FDE5D8" stroke-width="6" mask="url(#i)" xlink:href="#b"/><use stroke="#FDE5D8" stroke-width="6" mask="url(#j)" xlink:href="#c"/><use stroke="#FDE5D8" stroke-width="6" mask="url(#k)" transform="matrix(-1 0 0 1 18.25 0)" xlink:href="#d"/><use stroke="#FDE5D8" stroke-width="6" mask="url(#l)" xlink:href="#e"/><ellipse cx="28.5" cy="82.958" fill="#FC8A51" rx="1.5" ry="1.49"/><ellipse cx="34.5" cy="82.958" fill="#FC8A51" rx="1.5" ry="1.49"/><ellipse cx="40.5" cy="82.958" fill="#FC8A51" rx="1.5" ry="1.49"/><ellipse cx="46.5" cy="82.958" fill="#FC8A51" rx="1.5" ry="1.49"/><ellipse cx="37.5" cy="55.138" stroke="#FDE5D8" stroke-width="3" rx="10.5" ry="10.433"/><ellipse cx="37.5" cy="55.138" stroke="#FDE5D8" stroke-width="3" rx="5.5" ry="5.465"/></g><path fill="#EEE" d="M96.043 37.21c-.152 1.688.081 3.816.997 6.147a1.016 1.016 0 0 0 1.89-.74c-.791-2.014-.99-3.832-.865-5.226.01-.114.02-.186.024-.211a1.015 1.015 0 1 0-2.002-.333 5.06 5.06 0 0 0-.044.363zm11.487 15.683c.491.24 1.098.063 1.355-.394.257-.456.068-1.02-.424-1.26-1.866-.907-3.458-1.914-4.794-3.007a1.058 1.058 0 0 0-1.417.085.888.888 0 0 0 .091 1.317c1.458 1.192 3.183 2.283 5.19 3.26zm13.131 6.06a1.032 1.032 0 0 0 1.293-.7 1.06 1.06 0 0 0-.686-1.32 376.355 376.355 0 0 1-5.915-1.882 1.031 1.031 0 0 0-1.303.681 1.06 1.06 0 0 0 .668 1.33c1.729.569 2.905.94 5.943 1.891zm11.934 3.928c.45.246 1.022.098 1.28-.33a.872.872 0 0 0-.346-1.221c-1.494-.819-3.192-1.545-5.267-2.275-.486-.17-1.025.067-1.204.53-.18.464.07.978.555 1.149 1.984.697 3.59 1.384 4.982 2.147zm9.382 10.502c.205.494.81.742 1.349.554.54-.188.81-.74.605-1.234-.85-2.048-1.853-3.796-3.037-5.305-.337-.429-.99-.527-1.459-.218-.469.308-.575.906-.238 1.335 1.074 1.368 1.992 2.97 2.78 4.868zm2.632 13.642c.018.553.568.99 1.228.975.66-.016 1.18-.477 1.163-1.03-.073-2.204-.27-4.206-.622-6.12-.101-.547-.712-.923-1.365-.838-.652.084-1.1.597-.999 1.144.336 1.825.525 3.745.595 5.869z"/><path fill="#E5E5E5" d="M144.142 95.73a244.285 244.285 0 0 0-.142 5.254c-.007.553.396 1.008.902 1.016.506.008.923-.433.93-.985.02-1.467.056-2.681.142-5.211l.026-.767c.018-.552-.377-1.016-.882-1.036-.506-.02-.931.41-.95.963l-.026.766zm.797 19.471c.12.545.673.892 1.236.777.562-.116.921-.651.802-1.196-.417-1.9-.71-3.84-.897-5.864-.052-.554-.558-.964-1.131-.914-.573.05-.996.54-.945 1.094.195 2.102.5 4.121.935 6.103zm5.056 12.324c.296.454.953.61 1.467.348.514-.261.69-.841.395-1.295a40.725 40.725 0 0 1-2.79-4.991c-.227-.485-.855-.715-1.403-.515-.548.2-.81.755-.582 1.239a42.56 42.56 0 0 0 2.913 5.214zm4.814 7.701a33.475 33.475 0 0 0 3.543 3.531 1.021 1.021 0 0 0 1.393-.066.908.908 0 0 0-.07-1.326 31.562 31.562 0 0 1-3.34-3.328 59.092 59.092 0 0 1-.576-.682 1.02 1.02 0 0 0-1.386-.152.909.909 0 0 0-.16 1.32c.196.234.394.469.596.703zm15.825 11.677c.48.242 1.052.017 1.276-.501.224-.52.016-1.136-.464-1.378a49.756 49.756 0 0 1-4.986-2.872c-.453-.298-1.044-.144-1.32.345-.276.488-.133 1.126.32 1.424a51.568 51.568 0 0 0 5.174 2.982z"/><path fill="#EEE" d="M184.733 151.97c.553.141 1.108-.226 1.239-.82.131-.595-.21-1.192-.763-1.333a72.17 72.17 0 0 1-5.863-1.763c-.54-.188-1.12.13-1.296.712-.175.581.121 1.205.662 1.393a74.018 74.018 0 0 0 6.021 1.81zm13.2 2.028c.554.04 1.03-.445 1.065-1.083.035-.639-.386-1.188-.939-1.228a71.842 71.842 0 0 1-5.92-.676c-.55-.086-1.055.358-1.13.991-.074.634.31 1.217.86 1.303a73.28 73.28 0 0 0 6.065.693zm14.188-1.392c.55-.055.94-.457.871-.9-.068-.441-.569-.755-1.118-.7-1.917.192-3.893.32-5.91.382-.554.017-.985.392-.963.837.021.445.487.792 1.04.774a88.939 88.939 0 0 0 6.08-.393zm14.245-2.657c.53-.22.776-.816.55-1.332a1.053 1.053 0 0 0-1.367-.535 44.421 44.421 0 0 1-5.777 1.923 1.012 1.012 0 0 0-.736 1.243c.15.542.721.863 1.277.717a46.532 46.532 0 0 0 6.054-2.016zm11.483-9.532c.292-.435.148-1.006-.32-1.277-.47-.27-1.087-.138-1.379.297-.957 1.424-2.225 2.734-3.784 3.92a.88.88 0 0 0-.138 1.304c.35.396.98.453 1.408.128 1.723-1.31 3.136-2.771 4.213-4.372zm7.824-9.73a.965.965 0 0 0 .09-1.358.958.958 0 0 0-1.355-.09 44.935 44.935 0 0 0-4.17 4.163.965.965 0 0 0 .089 1.359.957.957 0 0 0 1.354-.089 43.05 43.05 0 0 1 3.991-3.985zm11.808-7.817c.476-.257.657-.858.405-1.342a.967.967 0 0 0-1.319-.412 67.097 67.097 0 0 0-5.123 3.059c-.451.298-.58.913-.287 1.373.294.46.898.59 1.35.292a65.257 65.257 0 0 1 4.974-2.97zm12.795-5.948c.55-.169.851-.724.672-1.241-.179-.518-.77-.8-1.32-.632a92.308 92.308 0 0 0-5.975 2.054c-.536.205-.794.78-.576 1.283.218.504.83.746 1.366.541a90.115 90.115 0 0 1 5.833-2.005z"/><circle cx="145" cy="90" r="5" fill="#FFF" stroke="#EEE" stroke-width="2"/><circle cx="238" cy="138" r="5" fill="#FFF" stroke="#EEE" stroke-width="2"/><path stroke="#B5A7DD" stroke-width="3" d="M20.06 56s-17.47 33-12 53c5.47 20 17 32 38 44s32.44-5 60.94 6 29 43 29 43" stroke-linecap="round" stroke-dasharray="8 10"/><g stroke="#EEE" stroke-width="3" transform="translate(108 173)"><path fill="#FFF" d="M154 77c0-42.526-34.474-77-77-77S0 34.474 0 77" stroke-linecap="round"/><circle cx="108" cy="41" r="16"/><circle cx="42.5" cy="30.5" r="8.5"/><circle cx="22" cy="58" r="5"/></g><g fill="#FC8A51" transform="rotate(15 101.633 923.121)"><path d="M.398 11.298h2.388c0-4.234 3.385-7.666 7.56-7.666V1.21C4.853 1.21.399 5.727.399 11.298z"/><ellipse cx="10.745" cy="2.018" rx="1.99" ry="2.018"/></g><g fill="#FC8A51" transform="scale(-1 1) rotate(-15 -102.031 920.099)"><path d="M.398 11.298h2.388c0-4.234 3.385-7.666 7.56-7.666V1.21C4.853 1.21.399 5.727.399 11.298z"/><ellipse cx="10.745" cy="2.018" rx="1.99" ry="2.018"/></g><g transform="rotate(15 71.738 842.306)"><g fill="#FC8A51" transform="translate(29.449 11.298)"><rect width="7.959" height="2" x=".796" y="8.877" rx="1"/><rect width="7.959" height="2" x=".796" y="16.14" transform="rotate(15 4.776 17.14)" rx="1"/><rect width="7.959" height="2" x=".915" y="1.807" transform="rotate(-15 4.895 2.807)" rx="1"/></g><g fill="#FC8A51" transform="matrix(-1 0 0 1 9.551 11.298)"><rect width="7.959" height="2" x=".796" y="8.877" rx="1"/><rect width="7.959" height="2" x=".796" y="16.14" transform="rotate(15 4.776 17.14)" rx="1"/><rect width="7.959" height="2" x=".915" y="1.807" transform="rotate(-15 4.895 2.807)" rx="1"/></g><use stroke="#FC8A51" stroke-width="6" mask="url(#m)" xlink:href="#f"/><path fill="#FC8A51" d="M7.163 12.912h23.878v3H7.163z"/></g><g fill="#EEE" transform="scale(-1 1) rotate(15 -60.75 -335.206)"><path d="M.255 7.123h1.53a4.84 4.84 0 0 1 4.848-4.834V.763C3.11.763.255 3.611.255 7.123z"/><ellipse cx="6.888" cy="1.272" rx="1.276" ry="1.272"/></g><g fill="#EEE" transform="rotate(-15 60.494 -337.144)"><path d="M.255 7.123h1.53a4.84 4.84 0 0 1 4.848-4.834V.763C3.11.763.255 3.611.255 7.123z"/><ellipse cx="6.888" cy="1.272" rx="1.276" ry="1.272"/></g><g transform="scale(-1 1) rotate(15 -79.491 -386.955)"><g fill="#EEE" transform="translate(18.878 7.123)"><rect width="5.102" height="2" x=".51" y="5.596" rx="1"/><rect width="5.102" height="2" x=".51" y="10.175" transform="rotate(15 3.061 11.175)" rx="1"/><rect width="5.102" height="2" x=".587" y="1.139" transform="rotate(-15 3.138 2.14)" rx="1"/></g><g fill="#EEE" transform="matrix(-1 0 0 1 6.122 7.123)"><rect width="5.102" height="2" x=".51" y="5.596" rx="1"/><rect width="5.102" height="2" x=".51" y="10.175" transform="rotate(15 3.061 11.175)" rx="1"/><rect width="5.102" height="2" x=".587" y="1.139" transform="rotate(-15 3.138 2.14)" rx="1"/></g><use stroke="#EEE" stroke-width="4" mask="url(#n)" xlink:href="#g"/><path fill="#EEE" d="M4.592 8.14h15.306v2H4.592z"/></g><g fill="#FFF" transform="translate(0 103)"><circle cx="8.5" cy="8.5" r="8.5" stroke="#B5A7DD" stroke-width="4"/><circle cx="171.5" cy="20.5" r="6.5"/></g><g transform="translate(39 142)"><ellipse cx="12.5" cy="12.5" fill="#FFF" stroke="#6B4FBB" stroke-width="4" rx="12.5" ry="12.5"/><path fill="#FC8A51" d="M10.732 13.475l-1.766-1.767a1.5 1.5 0 1 0-2.122 2.122l2.826 2.826h.001v.001c.59.59 1.535.587 2.119.003l6.37-6.37a1.504 1.504 0 0 0-.003-2.118 1.494 1.494 0 0 0-2.118-.004l-5.307 5.307z"/></g><circle cx="171.5" cy="122.5" r="6.5" fill="#FFF" stroke="#FC8A51" stroke-width="3"/><circle cx="22" cy="52" r="6" fill="#FFF" stroke="#B5A7DD" stroke-width="3"/><path fill="#FFF" stroke="#B5A7DD" stroke-width="3.6" d="M188.151 141.596c8.704-7.746 11.013-20.925 4.862-31.578-7.02-12.16-22.405-16.422-34.362-9.518-11.958 6.904-15.96 22.358-8.939 34.518 6.236 10.8 19.068 15.37 30.238 11.42l10.899 18.879a4.765 4.765 0 0 0 6.508 1.748 4.768 4.768 0 0 0 1.74-6.51l-10.946-18.959zm-8.434-4.609c7.857-4.536 10.487-14.692 5.873-22.683-4.613-7.991-14.723-10.791-22.58-6.255-7.858 4.537-10.488 14.693-5.875 22.684 4.614 7.99 14.724 10.791 22.582 6.254z"/></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/job_not_triggered.svg b/app/assets/images/illustrations/job_not_triggered.svg deleted file mode 100644 index e13c1cb0a7d..00000000000 --- a/app/assets/images/illustrations/job_not_triggered.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 310 141" xmlns:xlink="http://www.w3.org/1999/xlink"><g fill="none" fill-rule="evenodd"><g fill-rule="nonzero"><path fill="#e5e5e5" d="M48 69c0-1.105.887-2 1.998-2h4c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4A1.992 1.992 0 0 1 48 69m14 0c0-1.105.887-2 1.998-2h4c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4A1.992 1.992 0 0 1 62 69"/><g fill="#31af64"><path d="M19 88C8.507 88 0 79.493 0 69s8.507-19 19-19 19 8.507 19 19-8.507 19-19 19m0-4c8.284 0 15-6.716 15-15 0-8.284-6.716-15-15-15-8.284 0-15 6.716-15 15 0 8.284 6.716 15 15 15"/><path d="M17.07 71.02l-2.829-2.828a1.995 1.995 0 0 0-2.828 0 1.997 1.997 0 0 0 0 2.83l4.243 4.243a1.993 1.993 0 0 0 2.823.005l7.79-7.79a1.998 1.998 0 0 0-.007-2.822 1.99 1.99 0 0 0-2.822-.006l-6.37 6.37v-.001"/></g></g><g transform="translate(187)"><rect width="116" height="134" y="7" fill="#f9f9f9" rx="10"/><rect width="116" height="134" x="5" y="2" fill="#fff" rx="10"/><path fill="#eee" fill-rule="nonzero" d="M15 4a8 8 0 0 0-8 8v114a8 8 0 0 0 8 8h96a8 8 0 0 0 8-8V12a8 8 0 0 0-8-8H15m0-4h96c6.627 0 12 5.373 12 12v114c0 6.627-5.373 12-12 12H15c-6.627 0-12-5.373-12-12V12C3 5.373 8.373 0 15 0"/><g transform="translate(23 25)"><g fill="#e1dbf1"><rect width="16" height="4" rx="2"/><rect width="16" height="4" x="32" y="12" rx="2"/></g><rect width="16" height="4" x="44" fill="#eee" rx="2"/><rect width="16" height="4" x="12" y="24" fill="#e1dbf1" rx="2"/><rect width="16" height="4" x="64" y="36" fill="#fef0e8" rx="2"/><rect width="8" height="4" x="20" fill="#fee1d3" rx="2" id="a"/><rect width="8" height="4" x="32" y="36" fill="#fc6d26" rx="2"/><rect width="8" height="4" x="52" y="12" fill="#fef0e8" rx="2"/><rect width="8" height="4" x="64" fill="#fef0e8" rx="2" id="b"/><rect width="12" height="4" x="16" y="48" fill="#e1dbf1" rx="2"/><rect width="8" height="4" x="44" y="36" fill="#fc6d26" rx="2"/><g fill="#e1dbf1"><rect width="4" height="4" x="56" y="36" rx="2"/><rect width="4" height="4" x="64" y="60" rx="2"/></g><rect width="4" height="4" x="72" y="60" fill="#fc6d26" rx="2"/><rect width="8" height="4" x="32" fill="#fc6d26" rx="2" id="c"/><g fill="#eee"><rect width="28" height="4" y="36" rx="2"/><rect width="28" height="4" x="44" y="48" rx="2"/></g><rect width="28" height="4" x="32" y="60" fill="#efedf8" rx="2"/><rect width="28" height="4" y="12" fill="#6b4fbb" rx="2"/><rect width="28" height="4" x="32" y="24" fill="#c3b8e3" rx="2"/><rect width="8" height="4" y="24" fill="#fef0e8" rx="2"/><rect width="8" height="4" x="32" y="48" fill="#6b4fbb" rx="2"/><rect width="12" height="4" y="48" fill="#fc6d26" rx="2"/><g fill="#fef0e8"><rect width="12" height="4" y="60" rx="2"/><rect width="12" height="4" x="16" y="60" rx="2"/></g></g><g transform="translate(23 97)"><rect width="16" height="4" fill="#efedf8" rx="2"/><rect width="16" height="4" x="18" y="12" fill="#fc6d26" rx="2"/><rect width="16" height="4" x="44" fill="#6b4fbb" rx="2"/><use xlink:href="#a"/><rect width="8" height="4" x="38" y="12" fill="#fef0e8" rx="2"/><use xlink:href="#b"/><use xlink:href="#c"/><rect width="14" height="4" y="12" fill="#eee" rx="2"/></g></g><g fill-rule="nonzero"><path fill="#eee" d="M109 101a2 2 0 1 1 0-4c2.524 0 5-.346 7.379-1.02a2 2 0 0 1 1.091 3.849 31.007 31.007 0 0 1-8.47 1.172m18.09-5.825a31.174 31.174 0 0 0 6.187-5.899 2 2 0 1 0-3.131-2.489 27.133 27.133 0 0 1-5.393 5.142 2.001 2.001 0 0 0 2.337 3.247m11.297-15.288a30.923 30.923 0 0 0 1.576-8.407 2 2 0 1 0-3.996-.188 26.875 26.875 0 0 1-1.372 7.32 2 2 0 1 0 3.791 1.275m.283-18.89a30.855 30.855 0 0 0-3.593-7.763 2 2 0 1 0-3.362 2.166 26.905 26.905 0 0 1 3.128 6.757 2 2 0 0 0 3.828-1.16M127.875 45.41a30.973 30.973 0 0 0-7.435-4.228 2 2 0 0 0-1.477 3.717 26.936 26.936 0 0 1 6.474 3.682 2 2 0 0 0 2.438-3.172m-17.834-6.391a31.09 31.09 0 0 0-8.5.886 2 2 0 0 0 .959 3.883 27.06 27.06 0 0 1 7.408-.771 2 2 0 1 0 .132-3.998m-18.272 5.207a31.139 31.139 0 0 0-6.383 5.688 2 2 0 1 0 3.045 2.593 27.152 27.152 0 0 1 5.564-4.957 2 2 0 1 0-2.226-3.324M79.96 59.121a30.864 30.864 0 0 0-1.862 8.349 2 2 0 1 0 3.987.323c.203-2.506.75-4.946 1.62-7.268a2 2 0 1 0-3.746-1.404m-.923 18.873a30.827 30.827 0 0 0 3.327 7.881 2.001 2.001 0 0 0 3.435-2.051 26.785 26.785 0 0 1-2.895-6.859 2 2 0 0 0-3.865 1.029M89.301 93.94a31.008 31.008 0 0 0 7.286 4.476 2 2 0 1 0 1.603-3.665 26.983 26.983 0 0 1-6.346-3.899 2 2 0 0 0-2.543 3.087m17.61 6.991a2 2 0 0 1 .265-3.991c.601.04 1.205.06 1.812.06a1.999 1.999 0 1 1-.001 3.999c-.695 0-1.387-.023-2.076-.069"/><path fill="#fc0" d="M117.78 63.798c.241.268.288.563.14.884l-10.848 23.24c-.174.334-.455.502-.843.502-.054 0-.148-.014-.282-.04a.855.855 0 0 1-.512-.382.761.761 0 0 1-.09-.603l3.957-16.232-8.156 2.03a1.08 1.08 0 0 1-.241.02.93.93 0 0 1-.623-.222c-.24-.2-.328-.462-.26-.783l4.04-16.574a.858.858 0 0 1 .321-.462.917.917 0 0 1 .563-.18h6.59c.254 0 .468.083.642.25a.797.797 0 0 1 .261.593.818.818 0 0 1-.1.362l-3.435 9.301 7.955-1.969c.107-.027.187-.04.241-.04.254 0 .482.1.683.301"/><path fill="#e5e5e5" d="M148 69c0-1.105.887-2 1.998-2h4c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4A1.992 1.992 0 0 1 148 69m14 0c0-1.105.887-2 1.998-2h4c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4A1.992 1.992 0 0 1 162 69"/></g></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/labels.svg b/app/assets/images/illustrations/labels.svg deleted file mode 100644 index 3a2d521323b..00000000000 --- a/app/assets/images/illustrations/labels.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="787 240 386 274" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><circle id="a" cx="37" cy="107" r="8"/><mask id="e" width="16" height="16" x="0" y="0" fill="#fff"><use xlink:href="#a"/></mask><circle id="b" cx="37" cy="75" r="8"/><mask id="f" width="16" height="16" x="0" y="0" fill="#fff"><use xlink:href="#b"/></mask><circle id="c" cx="42" cy="93" r="8"/><mask id="g" width="16" height="16" x="0" y="0" fill="#fff"><use xlink:href="#c"/></mask><circle id="d" cx="43" cy="75" r="8"/><mask id="h" width="16" height="16" x="0" y="0" fill="#fff"><use xlink:href="#d"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(791 244)"><g transform="rotate(30 49.554 229.722)"><rect width="74" height="124" x="8.6" y="95.9" fill="#FAFAFA" rx="8"/><rect width="74" height="124" y="87" fill="#FFF" stroke="#EEE" stroke-width="4" stroke-linecap="round" rx="8"/><circle cx="26.5" cy="178.5" r="3.5" fill="#FC8A51"/><circle cx="47.5" cy="178.5" r="3.5" fill="#FC8A51"/><rect width="50" height="4" x="12" y="127" fill="#E5E5E5" rx="2"/><rect width="38" height="4" x="18" y="139" fill="#E5E5E5" rx="2"/><use stroke="#E5E5E5" stroke-width="8" mask="url(#e)" stroke-linecap="round" xlink:href="#a"/><path stroke="#EEE" stroke-width="4" d="M37.3 107S10.5 18.3 81 .6" stroke-linecap="round"/><path fill="#FDE5D8" d="M31 189c0 3.3 2.7 6 6 6s6-2.7 6-6"/></g><g transform="translate(105 47)"><rect width="74" height="124" y="64" fill="#FAFAFA" rx="8"/><rect width="74" height="124" y="55" fill="#FFF" stroke="#EEE" stroke-width="4" stroke-linecap="round" rx="8"/><rect width="50" height="4" x="12" y="95" fill="#E5E5E5" rx="2"/><rect width="38" height="4" x="18" y="107" fill="#E5E5E5" rx="2"/><use stroke="#E5E5E5" stroke-width="8" mask="url(#f)" stroke-linecap="round" xlink:href="#b"/><path fill="#B5A7DD" d="M56 149.7c-.6-1-.2-2 .7-2.7l1.8-1c1-.6 2-.2 2.7.7.5 1 .2 2.2-.7 2.8l-1.8 1c-1 .5-2 .2-2.7-.8zm-37.8 0c.5-1 .2-2-.7-2.7l-1.8-1c-1-.6-2-.2-2.7.7-.6 1-.2 2.2.7 2.8l1.8 1c1 .5 2 .2 2.7-.8zM33 151h9v4h-9v-4z"/><path fill="#6B4FBB" d="M59 153c0-5.5-4.6-10-10-10-5.7 0-10 4.5-10 10s4.3 10 10 10c5.4 0 10-4.5 10-10zm-16 0c0-3.3 2.6-6 6-6 3.2 0 6 2.7 6 6s-2.8 6-6 6c-3.4 0-6-2.7-6-6zm-8 0c0-5.5-4.6-10-10-10-5.7 0-10 4.5-10 10s4.3 10 10 10c5.4 0 10-4.5 10-10zm-16 0c0-3.3 2.6-6 6-6 3.2 0 6 2.7 6 6s-2.8 6-6 6c-3.4 0-6-2.7-6-6z"/><path stroke="#EEE" stroke-width="4" d="M37 75S30 0 80 0" stroke-linecap="round"/></g><g transform="rotate(15 -82.507 752.644)"><rect width="74" height="124" x="14.6" y="81.8" fill="#FAFAFA" rx="8"/><rect width="74" height="124" x="5" y="73" fill="#FFF" stroke="#EEE" stroke-width="4" stroke-linecap="round" rx="8"/><path fill="#FDE5D8" d="M41 147c0-1 1-2 2-2s2 1 2 2v3c0 1-1 2-2 2s-2-1-2-2v-3zm16.8 6.2c.8-.7 2-.6 2.8.3.7.8.5 2-.3 2.8L58 158c-1 .8-2.2.7-3 0-.6-1-.4-2.3.4-3l2.4-1.8zm-32 3c-1-.6-1-2-.4-2.7.7-1 2-1 2.8-.3l2.4 1.8c.8.7 1 2 .3 3-.8.7-2 1-3 0l-2.3-1.7z"/><rect width="2" height="7" x="39" y="168" fill="#FC8A51" rx="1"/><rect width="2" height="7" x="45" y="168" fill="#FC8A51" rx="1"/><circle cx="40" cy="169" r="2" fill="#FC8A51"/><circle cx="46" cy="169" r="2" fill="#FC8A51"/><rect width="22" height="18" x="32" y="158" stroke="#FC8A51" stroke-width="4" rx="8"/><rect width="34" height="5" x="26" y="174" fill="#FC8A51" rx="2.5"/><rect width="50" height="4" x="17" y="113" fill="#E5E5E5" rx="2"/><rect width="38" height="4" x="23" y="125" fill="#E5E5E5" rx="2"/><use stroke="#E5E5E5" stroke-width="8" mask="url(#g)" stroke-linecap="round" xlink:href="#c"/><path stroke="#EEE" stroke-width="4" d="M42 93S50 0 0 0" stroke-linecap="round"/></g><g transform="rotate(-15 276.18 -697.744)"><rect width="74" height="124" x="18.7" y="65.6" fill="#FAFAFA" rx="8"/><rect width="74" height="124" x="6" y="55" fill="#FFF" stroke="#EEE" stroke-width="4" stroke-linecap="round" rx="8"/><g transform="translate(25 129)"><path stroke="#B5A7DD" stroke-width="4" d="M32 14c0-7.7-6.3-14-14-14S4 6.3 4 14" stroke-linecap="round"/><path stroke="#B5A7DD" stroke-width="2" d="M33 15v13c0 4.4-3.6 8-8 8" stroke-linecap="round"/><rect width="7" height="4" x="20" y="34" fill="#6B4FBB" rx="2"/><rect width="7" height="13" y="15" fill="#FFF" stroke="#6B4FBB" stroke-width="3" stroke-linejoin="round" rx="3.5"/><rect width="7" height="13" x="29" y="15" fill="#FFF" stroke="#6B4FBB" stroke-width="3" stroke-linejoin="round" transform="matrix(-1 0 0 1 65 0)" rx="3.5"/></g><rect width="50" height="4" x="18" y="95" fill="#E5E5E5" rx="2"/><rect width="38" height="4" x="24" y="107" fill="#E5E5E5" rx="2"/><use stroke="#E5E5E5" stroke-width="8" mask="url(#h)" stroke-linecap="round" xlink:href="#d"/><path stroke="#EEE" stroke-width="4" d="M43 75S50 0 0 0" stroke-linecap="round"/></g><circle cx="193" cy="47" r="12" fill="#FFF" stroke="#FDE5D8" stroke-width="4"/><circle cx="193" cy="47" r="5" fill="#FFF" stroke="#FDE5D8" stroke-width="4"/><g opacity=".2"><path fill="#FC8A51" d="M30.7 254.8l-2.6 1c-1 .5-1.7 0-1.7-1v-3l-1-2.7c-.4-1 .2-1.7 1.2-1.7h3l2.6-1c1.2-.4 2 .2 2 1.2l-.2 3 1 2.6c.5 1.2 0 2-1 2l-3-.2zm344-121l-2.6 1c-1 .5-1.7 0-1.7-1v-3l-1-2.7c-.4-1 .2-1.7 1.2-1.7h3l2.6-1c1.2-.4 2 .2 2 1.2l-.2 3 1 2.6c.5 1.2 0 2-1 2l-3-.2zM5.6 95H1.8c-1.3.2-2-.8-1.4-2l1.4-3.4-.2-3.8c0-1.3 1-2 2-1.4l3.6 1.4 3.7-.2c1.2 0 2 1 1.4 2L11 91.3V95c.2 1.2-.8 2-2 1.4L5.6 95z"/><path fill="#6B4FBB" d="M308.8 62l-2-2.3c-.7-.8-.5-1.7.6-2l2.8-1 2-2c1-.6 1.8-.4 2.2.7l.8 2.8 2 2c.8 1 .5 1.8-.5 2.2l-2.8.8-2.3 2c-.8.8-1.7.5-2-.5l-1-2.8zm9.2 164.6h-3c-1-.2-1.4-1-1-2l1.4-2.5v-3c.2-1 1-1.4 2-1l2.6 1.4h3c1 .2 1.5 1 1 2l-1.4 2.6v3c-.2 1-1 1.5-2 1l-2.5-1.4zM121.8 8l-2-2.3c-.7-.8-.5-1.7.6-2l2.8-1 2-2c1-.6 1.8-.4 2.2.7l.8 2.8 2 2c.8 1 .5 1.8-.5 2.2l-2.8.8-2.3 2c-.8.8-1.7.5-2-.5l-1-2.8z"/></g></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/logos/go_logo.svg b/app/assets/images/illustrations/logos/go_logo.svg deleted file mode 100644 index 7fd49118006..00000000000 --- a/app/assets/images/illustrations/logos/go_logo.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" viewBox="0 0 16 16"><g fill-rule="evenodd"><path d="M14 16.01h1V7.99C15 4.128 11.866.999 8 .999c-3.858 0-7 3.13-7 6.991v8.02h1V7.99c0-3.306 2.691-5.991 6-5.991 3.314 0 6 2.682 6 5.991v8.02M3.48 2.656a2 2 0 1 0-2.155 3.228c.102-.321.226-.631.371-.93a1.001 1.001 0 1 1 1.069-1.599 6.96 6.96 0 0 1 .717-.699m9.04-.002a2 2 0 1 1 2.155 3.23 6.835 6.835 0 0 0-.37-.931 1 1 0 1 0-1.068-1.599 6.96 6.96 0 0 0-.717-.699"/><path d="M5.726 8.04h1.557v.124c0 .283-.033.534-.1.752a1.583 1.583 0 0 1-.33.566c-.35.394-.795.591-1.335.591-.527 0-.979-.19-1.355-.571a1.893 1.893 0 0 1-.564-1.377c0-.547.191-1.01.574-1.391a1.902 1.902 0 0 1 1.396-.574c.295 0 .57.06.825.181.244.12.484.316.72.586l-.405.388c-.309-.412-.686-.618-1.13-.618-.399 0-.733.138-1 .413-.27.27-.405.609-.405 1.015 0 .42.151.766.452 1.037.282.252.587.378.915.378.28 0 .531-.094.754-.283.223-.19.347-.418.373-.683h-.94v-.535m2.884.061c0-.53.194-.986.583-1.367a1.919 1.919 0 0 1 1.396-.571c.537 0 .998.192 1.382.576.386.384.578.845.578 1.384 0 .542-.194 1-.581 1.379a1.944 1.944 0 0 1-1.408.569c-.487 0-.923-.168-1.311-.505-.426-.373-.64-.861-.64-1.465m.574.007c0 .417.14.759.42 1.028.278.269.6.403.964.403.395 0 .729-.137 1-.41.272-.277.408-.613.408-1.01 0-.402-.134-.739-.403-1.01a1.33 1.33 0 0 0-.991-.41c-.392 0-.723.137-.993.41a1.36 1.36 0 0 0-.405 1m-.184 3.918c.525.026.812.063.812.063.271.025.324-.096.116-.273 0 0-.775-.813-1.933-.813-1.159 0-1.923.813-1.923.813-.211.174-.153.3.12.273 0 0 .286-.037.81-.063v.477c0 .268.224.5.5.5.268 0 .5-.223.5-.498v-.252.25c0 .268.224.5.5.5.268 0 .5-.223.5-.498v-.478m-1-1.023c.552 0 1-.224 1-.5s-.448-.5-1-.5-1 .224-1 .5.448.5 1 .5"/></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/logos/mattermost_logo.svg b/app/assets/images/illustrations/logos/mattermost_logo.svg deleted file mode 100644 index b577c0599aa..00000000000 --- a/app/assets/images/illustrations/logos/mattermost_logo.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" version="1" viewBox="0 0 501 501"><path d="M236 .7C137.7 7.5 54 68.2 18.2 158.5c-32 81-19.6 172.8 33 242.5 39.8 53 97.2 87 164.3 97 16.5 2.7 48 3.2 63.5 1.2 48.7-6.3 92.2-24.6 129-54.2 13-10.5 33-31.2 42.2-43.7 26.4-35.5 42.8-75.8 49-120.3 1.6-12.3 1.6-48.7 0-61-4-28.3-12-54.8-24.2-79.5-12.8-26-26.5-45.3-46.8-65.8C417.8 64 400.2 49 398.4 49c-.6 0-.4 10.5.3 26l1.3 26 7 8.7c19 23.7 32.8 53.5 38.2 83 2.5 14 3 43 1 55.8-4.5 27.8-15.2 54-31 76.5-8.6 12.2-28 31.6-40.2 40.2-24 17-50 27.6-80 33-10 1.8-49 1.8-59 0-43-7.7-78.8-26-107.2-54.8-29.3-29.7-46.5-64-52.4-104.4-2-14-1.5-42 1-55C90 121.4 132 72 192 49.7c8-3 18.4-5.8 29.5-8.2 1.7-.4 34.4-38 35.3-40.6.3-1-10.2-1-20.8-.4z"/><path d="M322.2 24.6c-1.3.8-8.4 9.3-16 18.7-7.4 9.5-22.4 28-33.2 41.2-51 62.2-66 81.6-70.6 91-6 12-8.4 21-9 33-1.2 19.8 5 36 19 50C222 268 230 273 243 277.2c9 3 10.4 3.2 24 3.2 13.8 0 15 0 22.6-3 23.2-9 39-28.4 45-55.7 2-8.2 2-28.7.4-79.7l-2-72c-1-36.8-1.4-41.8-3-44-2-3-4.8-3.6-7.8-1.4z"/></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/manual_action.svg b/app/assets/images/illustrations/manual_action.svg deleted file mode 100644 index 85735855b46..00000000000 --- a/app/assets/images/illustrations/manual_action.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 398 151" xmlns:xlink="http://www.w3.org/1999/xlink"><g fill="none" fill-rule="evenodd"><path fill="#fef0e8" stroke="#fc6d26" stroke-width="4" d="M57.7 106.5h21.6a4.2 4.2 0 0 1 4.2 4.2v5.6a4.2 4.2 0 0 1-4.2 4.2H57.7a4.2 4.2 0 0 1-4.2-4.2v-5.6a4.2 4.2 0 0 1 4.2-4.2"/><g transform="translate(42 117)"><rect width="52" height="23" x=".5" y=".5" fill="#fff" stroke="#eee" stroke-width="4" rx="4.2"/><g fill="#fdc4a8"><rect width="11" height="2" x="8" y="8" rx="1"/><rect width="11" height="2" x="8" y="14" rx="1"/></g></g><g fill-rule="nonzero"><path fill="#e1dbf1" d="M96.31 132.32c1.048 0 1.648.007 4.319.042 11.523.153 18.377-.12 26.32-1.533 24.23-4.309 38.521-18.02 38.521-45.03 0-31.02 21.885-44.487 66.903-40.522l.351-3.985c-47.09-4.147-71.25 10.727-71.25 44.507 0 24.868-12.746 37.1-35.22 41.09-7.623 1.356-14.284 1.621-25.567 1.471a287.717 287.717 0 0 0-4.372-.042v4"/><path fill="#eee" d="M242 57.678c-6.29-1.373-11-6.976-11-13.678 0-6.702 4.71-12.304 11-13.678v4.136c-4.057 1.274-7 5.065-7 9.542 0 4.478 2.943 8.268 7 9.542v4.136"/></g><g transform="translate(242)"><rect width="116" height="134" y="7" fill="#f9f9f9" rx="10"/><rect width="116" height="134" x="5" y="2" fill="#fff" rx="10"/><path fill="#eee" fill-rule="nonzero" d="M15 4a8 8 0 0 0-8 8v114a8 8 0 0 0 8 8h96a8 8 0 0 0 8-8V12a8 8 0 0 0-8-8H15m0-4h96c6.627 0 12 5.373 12 12v114c0 6.627-5.373 12-12 12H15c-6.627 0-12-5.373-12-12V12C3 5.373 8.373 0 15 0"/><g transform="translate(23 25)"><g fill="#e1dbf1"><rect width="16" height="4" rx="2"/><rect width="16" height="4" x="32" y="12" rx="2"/></g><rect width="16" height="4" x="44" fill="#eee" rx="2"/><rect width="16" height="4" x="12" y="24" fill="#e1dbf1" rx="2"/><rect width="16" height="4" x="64" y="36" fill="#fef0e8" rx="2"/><rect width="8" height="4" x="20" fill="#fee1d3" rx="2" id="a"/><rect width="8" height="4" x="32" y="36" fill="#fc6d26" rx="2"/><rect width="8" height="4" x="52" y="12" fill="#fef0e8" rx="2"/><rect width="8" height="4" x="64" fill="#fef0e8" rx="2" id="b"/><rect width="12" height="4" x="16" y="48" fill="#e1dbf1" rx="2"/><rect width="8" height="4" x="44" y="36" fill="#fc6d26" rx="2"/><g fill="#e1dbf1"><rect width="4" height="4" x="56" y="36" rx="2"/><rect width="4" height="4" x="64" y="60" rx="2"/></g><rect width="4" height="4" x="72" y="60" fill="#fc6d26" rx="2"/><rect width="8" height="4" x="32" fill="#fc6d26" rx="2" id="c"/><g fill="#eee"><rect width="28" height="4" y="36" rx="2"/><rect width="28" height="4" x="44" y="48" rx="2"/></g><rect width="28" height="4" x="32" y="60" fill="#efedf8" rx="2"/><rect width="28" height="4" y="12" fill="#6b4fbb" rx="2"/><rect width="28" height="4" x="32" y="24" fill="#c3b8e3" rx="2"/><rect width="8" height="4" y="24" fill="#fef0e8" rx="2"/><rect width="8" height="4" x="32" y="48" fill="#6b4fbb" rx="2"/><rect width="12" height="4" y="48" fill="#fc6d26" rx="2"/><g fill="#fef0e8"><rect width="12" height="4" y="60" rx="2"/><rect width="12" height="4" x="16" y="60" rx="2"/></g><g transform="translate(0 72)"><rect width="16" height="4" fill="#efedf8" rx="2"/><rect width="16" height="4" x="18" y="12" fill="#fc6d26" rx="2"/><rect width="16" height="4" x="44" fill="#6b4fbb" rx="2"/><use xlink:href="#a"/><rect width="8" height="4" x="38" y="12" fill="#fef0e8" rx="2"/><use xlink:href="#b"/><use xlink:href="#c"/><rect width="14" height="4" y="12" fill="#eee" rx="2"/></g></g></g><g transform="translate(330 83)"><circle cx="33" cy="33" r="33" fill="#fff"/><g fill-rule="nonzero"><path fill="#eee" d="M33 68C13.67 68-2 52.33-2 33S13.67-2 33-2s35 15.67 35 35-15.67 35-35 35m0-4c17.12 0 31-13.879 31-31C64 15.88 50.121 2 33 2 15.88 2 2 15.879 2 33c0 17.12 13.879 31 31 31"/><path fill="#6b4fbb" stroke="#6b4fbb" stroke-width=".968" d="M42.383 34.655v-3.308l-2.112-.343c-.116-.456-.351-.913-.703-1.598l1.29-1.711-2.463-2.398-1.76 1.256a6.347 6.347 0 0 0-1.642-.684l-.233-2.055h-3.401l-.352 2.055c-.586.114-1.055.342-1.642.684l-1.76-1.255-2.463 2.397 1.173 1.711c-.352.57-.469 1.027-.704 1.598l-1.995.228v3.31l2.112.342c.116.57.351 1.027.703 1.598l-1.172 1.712 2.463 2.397 1.759-1.141c.469.227 1.056.456 1.642.684l.352 2.055h3.518l.352-2.055c.586-.114 1.055-.342 1.642-.684l1.76 1.255 2.463-2.397-1.29-1.712a6.03 6.03 0 0 0 .703-1.598l1.76-.344M33 36.367c-1.994 0-3.519-1.484-3.519-3.424 0-1.941 1.525-3.424 3.519-3.424 1.994 0 3.519 1.483 3.519 3.424 0 1.94-1.525 3.424-3.519 3.424" stroke-linecap="round" stroke-linejoin="bevel"/><path fill="#e1dbf1" d="M33 53.563c-11.598 0-21-9.206-21-20.563s9.402-20.563 21-20.563S54 21.643 54 33s-9.402 20.563-21 20.563m0-4.375c9.13 0 16.532-7.248 16.532-16.188 0-8.94-7.402-16.188-16.532-16.188-9.13 0-16.532 7.248-16.532 16.188 0 8.94 7.402 16.188 16.532 16.188"/></g></g><path fill="#fff" d="M164 114c14.912 0 27-12.09 27-27 0-14.912-12.09-27-27-27-14.912 0-27 12.09-27 27 0 14.912 12.09 27 27 27"/><g fill-rule="nonzero"><path fill="#eee" d="M164 118c-17.12 0-31-13.879-31-31 0-17.12 13.879-31 31-31 17.12 0 31 13.879 31 31 0 17.12-13.879 31-31 31m0-4c14.912 0 27-12.09 27-27 0-14.912-12.09-27-27-27-14.912 0-27 12.09-27 27 0 14.912 12.09 27 27 27"/><path fill="#fc0" d="M172.78 80.798c.241.268.288.563.14.884l-10.848 23.24c-.174.334-.455.502-.843.502-.054 0-.148-.014-.282-.04a.855.855 0 0 1-.512-.382.761.761 0 0 1-.09-.603l3.957-16.232-8.156 2.03a1.08 1.08 0 0 1-.241.02.93.93 0 0 1-.623-.222c-.24-.2-.328-.462-.26-.783l4.04-16.574a.858.858 0 0 1 .321-.462.917.917 0 0 1 .563-.18h6.59c.254 0 .468.083.642.25a.797.797 0 0 1 .261.593.818.818 0 0 1-.1.362l-3.435 9.301 7.955-1.969c.107-.027.187-.04.241-.04.254 0 .482.1.683.301"/></g><g><path fill="#eee" fill-rule="nonzero" d="M37.801 99.01l5.355 2.648c2.271 1.122 4.643-.252 4.809-2.778l.487-7.546a27.675 27.675 0 0 0 2.87-4.076c7.594-13.152 3.088-29.972-10.07-37.565-13.153-7.594-29.971-3.087-37.566 10.07-7.594 13.154-3.087 29.973 10.07 37.565a27.46 27.46 0 0 0 24.05 1.687m.952-3.992a2.002 2.002 0 0 0-1.698-.035 23.454 23.454 0 0 1-21.299-1.124c-11.24-6.488-15.09-20.86-8.602-32.1 6.49-11.239 20.862-15.09 32.1-8.601 11.239 6.489 15.09 20.862 8.6 32.1a23.519 23.519 0 0 1-2.849 3.939 1.995 1.995 0 0 0-.504 1.204l-.466 7.229-5.285-2.613"/><path fill="#fdc4a8" d="M21.137 70.471A7.495 7.495 0 0 0 27.5 74c2.684 0 5.04-1.41 6.363-3.529C36.377 71.869 38 74.267 38 77.674c0 5.799-2.739 9.587-10.5 9.587S17 83.473 17 77.674c0-3.407 1.622-5.804 4.137-7.203M27.5 72a5.5 5.5 0 1 1 0-11 5.5 5.5 0 1 1 0 11"/></g></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/merge_request_changes_empty.svg b/app/assets/images/illustrations/merge_request_changes_empty.svg deleted file mode 100644 index 40efeb2de57..00000000000 --- a/app/assets/images/illustrations/merge_request_changes_empty.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="374" height="268" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><circle id="a" cx="44" cy="44" r="44"/><circle id="b" cx="31" cy="31" r="31"/><circle id="c" cx="35" cy="35" r="35"/><rect id="d" width="230" height="176" rx="10"/><circle id="e" cx="31" cy="31" r="31"/></defs><g fill="none" fill-rule="evenodd"><g transform="translate(4 98)"><circle cx="53" cy="53" r="44" fill="#F9F9F9"/><g transform="translate(6 6)"><use fill="#FFF" xlink:href="#a"/><circle cx="44" cy="44" r="42" stroke="#EEE" stroke-width="4"/><path fill="#FEE1D3" fill-rule="nonzero" d="M34.394 55.736A4 4 0 0 1 36.706 55H56a6 6 0 0 0 6-6V35a6 6 0 0 0-6-6H34a6 6 0 0 0-6 6v25.265l6.394-4.53zM36.706 59l-7.972 5.647A3 3 0 0 1 24 62.199V35c0-5.523 4.477-10 10-10h22c5.523 0 10 4.477 10 10v14c0 5.523-4.477 10-10 10H36.706z"/><path fill="#FC6D26" d="M38 40a2 2 0 1 1 0 4 2 2 0 0 1 0-4zm7 0a2 2 0 1 1 0 4 2 2 0 0 1 0-4zm7 0a2 2 0 1 1 0 4 2 2 0 0 1 0-4z"/></g></g><g transform="translate(50 2)"><circle cx="39" cy="39" r="31" fill="#F9F9F9"/><g transform="translate(5 5)"><use fill="#FFF" xlink:href="#b"/><circle cx="31" cy="31" r="29" stroke="#EEE" stroke-width="4"/><rect width="20" height="4" x="21" y="29" fill="#6B4FBB" rx="2"/></g></g><path fill="#F9F9F9" d="M235.58 229H102c-6.627 0-12-5.373-12-12V65c0-6.627 5.373-12 12-12h206c6.627 0 12 5.373 12 12v18.399A34.842 34.842 0 0 1 337 79c19.33 0 35 15.67 35 35s-15.67 35-35 35a34.842 34.842 0 0 1-17-4.399V217c0 6.627-5.373 12-12 12h-11.58c.38 1.941.58 3.947.58 6 0 17.12-13.88 31-31 31s-31-13.88-31-31c0-2.053.2-4.059.58-6z"/><g transform="translate(87 50)"><g transform="translate(212 26)"><use fill="#FFF" xlink:href="#c"/><circle cx="35" cy="35" r="33" stroke="#EEE" stroke-width="4"/><g transform="translate(20 19)"><circle cx="15" cy="16" r="15" fill="#F4F1FA" stroke="#6B4FBB" stroke-width="3"/><path fill="#6B4FBB" d="M19.419 6.996h-.007L16.959 4l-2.454 2.997h-.006L12.045 4 9.59 6.998h-.003L7.132 4 4.676 7H2c2.605-4.204 7.23-7 12.502-7C19.771 0 24.394 2.793 27 6.994h-2.676L21.872 4l-2.453 2.996z"/><circle cx="9.5" cy="17.5" r="1.5" fill="#6B4FBB"/><circle cx="20.5" cy="17.5" r="1.5" fill="#6B4FBB"/></g></g><use fill="#FFF" xlink:href="#d"/><rect width="226" height="172" x="2" y="2" stroke="#EEE" stroke-width="4" rx="10"/><rect width="4" height="122" x="33" y="42" fill="#EEE" rx="2"/><g transform="translate(13 59)"><rect width="10" height="4" fill="#FEE1D3" rx="2"/><rect width="10" height="4" y="12" fill="#F0EDF8" rx="2"/><rect width="10" height="4" y="24" fill="#FEF0E9" rx="2"/><rect width="10" height="4" y="36" fill="#FEE1D3" rx="2"/><rect width="10" height="4" y="48" fill="#E1DBF1" rx="2"/><rect width="10" height="4" y="60" fill="#F0EDF8" rx="2"/><rect width="10" height="4" y="72" fill="#FEF0E9" rx="2"/><rect width="10" height="4" y="84" fill="#FEE1D3" rx="2"/></g><g transform="translate(55 59)"><rect width="14" height="4" fill="#6B4FBB" rx="2"/><rect width="14" height="4" x="20" fill="#F0EDF8" rx="2"/><rect width="14" height="4" x="40" fill="#FEF0E9" rx="2"/><rect width="14" height="4" y="12" fill="#F0EDF8" rx="2"/><rect width="14" height="4" x="40" y="24" fill="#FEF0E9" rx="2"/><rect width="14" height="4" y="48" fill="#E1DBF1" rx="2"/><rect width="14" height="4" x="40" y="36" fill="#FEF0E9" rx="2"/><rect width="7" height="4" x="20" y="12" fill="#FEE1D3" rx="2"/><rect width="7" height="4" x="27" y="36" fill="#6B4FBB" rx="2"/><rect width="7" height="4" x="20" y="48" fill="#FEE1D3" rx="2"/><rect width="7" height="4" y="24" fill="#FC6D26" rx="2"/><rect width="21" height="4" x="13" y="24" fill="#E1DBF1" rx="2"/><rect width="21" height="4" y="36" fill="#EEE" rx="2"/><rect width="7" height="4" x="33" y="12" fill="#6B4FBB" rx="2"/><g transform="translate(98)"><rect width="14" height="4" fill="#FEE1D3" rx="2"/><rect width="14" height="4" x="20" fill="#F0EDF8" rx="2"/><rect width="14" height="4" x="40" fill="#FC6D26" rx="2"/><rect width="14" height="4" y="12" fill="#FEF0E9" rx="2"/><rect width="14" height="4" x="40" y="24" fill="#E1DBF1" rx="2"/><rect width="14" height="4" y="48" fill="#F0EDF8" rx="2"/><rect width="14" height="4" x="40" y="36" fill="#FEE1D3" rx="2"/><rect width="7" height="4" x="20" y="12" fill="#FC6D26" rx="2"/><rect width="7" height="4" x="27" y="36" fill="#6B4FBB" rx="2"/><rect width="7" height="4" x="20" y="48" fill="#FC6D26" rx="2"/><rect width="7" height="4" y="24" fill="#6B4FBB" rx="2"/><rect width="21" height="4" x="13" y="24" fill="#FEE1D3" rx="2"/><rect width="21" height="4" y="36" fill="#FEF0E9" rx="2"/><rect width="7" height="4" x="33" y="12" fill="#6B4FBB" rx="2"/></g><g transform="translate(0 60)"><rect width="14" height="4" fill="#F0EDF8" rx="2"/><rect width="14" height="4" x="20" fill="#6B4FBB" rx="2"/><rect width="14" height="4" x="40" fill="#E1DBF1" rx="2"/><rect width="14" height="4" y="12" fill="#FEF0E9" rx="2"/><rect width="14" height="4" x="40" y="24" fill="#FEE1D3" rx="2"/><rect width="7" height="4" x="20" y="12" fill="#EEE" rx="2"/><rect width="7" height="4" y="24" fill="#6B4FBB" rx="2"/><rect width="21" height="4" x="13" y="24" fill="#FEF0E9" rx="2"/><rect width="7" height="4" x="33" y="12" fill="#FC6D26" rx="2"/></g><rect width="4" height="63" x="74" y="13" fill="#EEE" rx="2"/></g><rect width="230" height="4" y="27" fill="#EEE" rx="2"/></g><g transform="translate(233 201)"><use fill="#FFF" xlink:href="#e"/><circle cx="31" cy="31" r="29" stroke="#EEE" stroke-width="4"/><path fill="#FC6D26" d="M29 29v-6a2 2 0 1 1 4 0v6h6a2 2 0 1 1 0 4h-6v6a2 2 0 1 1-4 0v-6h-6a2 2 0 1 1 0-4h6z"/></g></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/merge_requests.svg b/app/assets/images/illustrations/merge_requests.svg deleted file mode 100644 index b9b8f0058e6..00000000000 --- a/app/assets/images/illustrations/merge_requests.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="755 221 385 225" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="a" width="278" height="179" rx="10"/><mask id="d" width="278" height="179" x="0" y="0" fill="#fff"><use xlink:href="#a"/></mask><path id="b" d="M13.6 49H57c5.5 0 10-4.5 10-10V10c0-5.5-4.5-10-10-10H10C4.5 0 0 4.5 0 10v42c0 5.5 3.2 7 7.2 3l6.4-6z"/><mask id="e" width="67" height="57.2" x="0" y="0" fill="#fff"><use xlink:href="#b"/></mask><path id="c" d="M13.6 49H57c5.5 0 10-4.5 10-10V10c0-5.5-4.5-10-10-10H10C4.5 0 0 4.5 0 10v42c0 5.5 3.2 7 7.2 3l6.4-6z"/><mask id="f" width="67" height="57.2" x="0" y="0" fill="#fff"><use xlink:href="#c"/></mask></defs><g fill="none" fill-rule="evenodd"><g fill="#F9F9F9" transform="translate(752 227)"><rect width="120" height="22" x="30" rx="11"/><rect width="132" height="22" y="44" rx="11"/><rect width="190" height="22" x="208" y="66" rx="11"/><rect width="158" height="22" x="129" y="197" rx="11"/><rect width="158" height="22" x="66" y="154" rx="11"/><rect width="350" height="22" x="31" y="110" rx="11"/><path d="M153 22H21h21.5c6 0 11 5 11 11s-5 11-11 11H21h132-36.5c-6 0-11-5-11-11s5-11 11-11H153zm252 66H288h36.5c6 0 11 5 11 11s-5 11-11 11H288h117-36.5c-6 0-11-5-11-11s5-11 11-11H405zm-244 44H44h36.5c6 0 11 5 11 11s-5 11-11 11H44h117-36.5c-6 0-11-5-11-11s5-11 11-11H161zm75 44H119h21.5c6 0 11 5 11 11s-5 11-11 11H119h117-51.5c-6 0-11-5-11-11s5-11 11-11H236z"/></g><g transform="translate(812 240)"><use fill="#FFF" stroke="#EEE" stroke-width="8" mask="url(#d)" xlink:href="#a"/><path fill="#EEE" d="M4 29h271v4H4z"/><g transform="translate(34 60)"><rect width="6" height="2" y="1" fill="#B5A7DD" rx="1"/><rect width="15" height="4" x="15" fill="#EEE" rx="2"/><rect width="15" height="4" x="72" fill="#EEE" rx="2"/><rect width="15" height="4" x="39" y="22" fill="#EEE" rx="2"/><rect width="15" height="4" x="53" y="11" fill="#FC6D26" rx="2"/><rect width="20" height="4" x="48" fill="#FC6D26" opacity=".5" rx="2"/><rect width="20" height="4" x="15" y="22" fill="#EEE" rx="2"/><rect width="20" height="4" x="29" y="11" fill="#EEE" rx="2"/><rect width="10" height="4" x="34" fill="#FC6D26" rx="2"/><rect width="10" height="4" x="15" y="11" fill="#EEE" rx="2"/><rect width="6" height="2" y="12" fill="#B5A7DD" rx="1"/><rect width="6" height="2" y="23" fill="#B5A7DD" rx="1"/></g><g transform="translate(34 93)"><rect width="6" height="2" y="1" fill="#B5A7DD" rx="1"/><rect width="15" height="4" x="15" fill="#FC6D26" rx="2"/><rect width="15" height="4" x="72" fill="#EEE" rx="2"/><rect width="15" height="4" x="39" y="22" fill="#FC6D26" opacity=".5" rx="2"/><rect width="15" height="4" x="53" y="11" fill="#EEE" rx="2"/><rect width="20" height="4" x="48" fill="#FC6D26" rx="2"/><rect width="20" height="4" x="15" y="22" fill="#FC6D26" rx="2"/><rect width="20" height="4" x="29" y="11" fill="#EEE" rx="2"/><rect width="10" height="4" x="34" fill="#FC6D26" opacity=".5" rx="2"/><rect width="10" height="4" x="15" y="11" fill="#EEE" rx="2"/><rect width="6" height="2" y="12" fill="#B5A7DD" rx="1"/><rect width="6" height="2" y="23" fill="#B5A7DD" rx="1"/></g><g transform="translate(34 126)"><rect width="6" height="2" y="1" fill="#B5A7DD" rx="1"/><rect width="15" height="4" x="15" fill="#EEE" rx="2"/><rect width="15" height="4" x="72" fill="#EEE" rx="2"/><rect width="15" height="4" x="39" y="22" fill="#EEE" rx="2"/><rect width="15" height="4" x="53" y="11" fill="#EEE" rx="2"/><rect width="20" height="4" x="48" fill="#FC6D26" rx="2"/><rect width="20" height="4" x="15" y="22" fill="#EEE" rx="2"/><rect width="20" height="4" x="29" y="11" fill="#EEE" rx="2"/><rect width="10" height="4" x="34" fill="#FC6D26" opacity=".5" rx="2"/><rect width="10" height="4" x="15" y="11" fill="#EEE" rx="2"/><rect width="6" height="2" y="12" fill="#B5A7DD" rx="1"/><rect width="6" height="2" y="23" fill="#B5A7DD" rx="1"/></g><g transform="translate(157 59)"><rect width="6" height="2" y="1" fill="#FDE5D8" rx="1"/><rect width="15" height="4" x="15" fill="#EEE" rx="2"/><rect width="15" height="4" x="72" fill="#EEE" rx="2"/><rect width="15" height="4" x="39" y="22" fill="#6B4FBB" opacity=".5" rx="2"/><rect width="15" height="4" x="53" y="11" fill="#6B4FBB" rx="2"/><rect width="20" height="4" x="48" fill="#6B4FBB" opacity=".5" rx="2"/><rect width="20" height="4" x="15" y="22" fill="#6B4FBB" rx="2"/><rect width="20" height="4" x="29" y="11" fill="#EEE" rx="2"/><rect width="10" height="4" x="34" fill="#6B4FBB" rx="2"/><rect width="10" height="4" x="15" y="11" fill="#EEE" rx="2"/><rect width="6" height="2" y="12" fill="#FDE5D8" rx="1"/><rect width="6" height="2" y="23" fill="#FDE5D8" rx="1"/><rect width="6" height="2" y="34" fill="#FDE5D8" rx="1"/><rect width="15" height="4" x="15" y="33" fill="#EEE" rx="2"/><rect width="15" height="4" x="58" y="22" fill="#EEE" rx="2"/><rect width="15" height="4" x="39" y="55" fill="#6B4FBB" opacity=".5" rx="2"/><rect width="15" height="4" x="29" y="44" fill="#6B4FBB" rx="2"/><rect width="20" height="4" x="48" y="33" fill="#6B4FBB" rx="2"/><rect width="20" height="4" x="15" y="55" fill="#EEE" rx="2"/><rect width="10" height="4" x="34" y="33" fill="#EEE" rx="2"/><rect width="10" height="4" x="15" y="44" fill="#EEE" rx="2"/><rect width="10" height="4" x="48" y="44" fill="#EEE" rx="2"/><rect width="10" height="4" x="62" y="44" fill="#EEE" rx="2"/><rect width="10" height="4" x="77" y="22" fill="#EEE" rx="2"/><rect width="6" height="2" y="45" fill="#FDE5D8" rx="1"/><rect width="6" height="2" y="56" fill="#FDE5D8" rx="1"/><rect width="6" height="2" y="67" fill="#FDE5D8" rx="1"/><rect width="15" height="4" x="15" y="66" fill="#6B4FBB" rx="2"/><rect width="15" height="4" x="39" y="88" fill="#EEE" rx="2"/><rect width="15" height="4" x="53" y="77" fill="#6B4FBB" opacity=".5" rx="2"/><rect width="20" height="4" x="15" y="88" fill="#EEE" rx="2"/><rect width="20" height="4" x="29" y="77" fill="#6B4FBB" rx="2"/><rect width="10" height="4" x="34" y="66" fill="#EEE" rx="2"/><rect width="10" height="4" x="72" y="77" fill="#EEE" rx="2"/><rect width="10" height="4" x="15" y="77" fill="#EEE" rx="2"/><rect width="6" height="2" y="78" fill="#FDE5D8" rx="1"/><rect width="6" height="2" y="89" fill="#FDE5D8" rx="1"/></g></g><g transform="translate(1057 221)"><use fill="#FFF" stroke="#FDE5D8" stroke-width="8" mask="url(#e)" xlink:href="#b"/><rect width="29" height="3" x="14" y="14" fill="#FDB692" rx="1.5"/><rect width="39" height="3" x="14" y="23" fill="#FDB692" rx="1.5"/><rect width="29" height="3" x="14" y="32" fill="#FDB692" rx="1.5"/></g><g transform="translate(1046 285)"><circle cx="16" cy="15" r="15" fill="#FFF7F4" stroke="#FC6D26" stroke-width="3"/><path stroke="#FC6D26" stroke-width="2" d="M0 14h1c5 0 9.2-2.7 11.4-6.7M14 1V0"/><path stroke="#FC6D26" stroke-width="2" d="M7.8 3c3 4.3 7.8 7 13.2 7 3.3 0 6.3-1 9-2.7"/><circle cx="10.5" cy="17.5" r="1.5" fill="#FC6D26"/><circle cx="21.5" cy="17.5" r="1.5" fill="#FC6D26"/></g><g transform="translate(825 370)"><circle cx="15" cy="16" r="15" fill="#F4F1FA" stroke="#6B4FBB" stroke-width="3"/><path fill="#6B4FBB" d="M25 7h2.7C25 2.8 20.4 0 15 0 9.6 0 5 2.8 2.3 7H5l2.5-3L10 7l2.5-3L15 7l2.5-3L20 7l2.5-3L25 7z"/><circle cx="9.5" cy="17.5" r="1.5" fill="#6B4FBB"/><circle cx="20.5" cy="17.5" r="1.5" fill="#6B4FBB"/></g><g transform="matrix(-1 0 0 1 840 306)"><use fill="#FFF" stroke="#E2DCF2" stroke-width="8" mask="url(#f)" xlink:href="#c"/><rect width="29" height="3" x="24" y="14" fill="#6B4FBB" opacity=".5" rx="1.5"/><rect width="19" height="3" x="34" y="23" fill="#6B4FBB" opacity=".5" rx="1.5"/><rect width="19" height="3" x="34" y="32" fill="#6B4FBB" opacity=".5" rx="1.5"/></g></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/monitoring/getting_started.svg b/app/assets/images/illustrations/monitoring/getting_started.svg deleted file mode 100644 index ff783bdd388..00000000000 --- a/app/assets/images/illustrations/monitoring/getting_started.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 406 305" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="a" width="159.8" height="127.81" x=".196" y="5" rx="10"/><rect id="b" width="160" height="128" x=".666" y=".41" rx="10"/><rect id="c" width="160.19" height="128.19" x=".339" y=".59" rx="10"/><mask id="d" width="159.8" height="127.81" x="0" y="0" fill="#fff"><use xlink:href="#a"/></mask><mask id="e" width="160" height="128" x="0" y="0" fill="#fff"><use xlink:href="#b"/></mask><mask id="f" width="160.19" height="128.19" x="0" y="0" fill="#fff"><use xlink:href="#c"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(12 3)"><rect width="160" height="128" x="122.08" y="146.08" fill="#f9f9f9" transform="rotate(5 202.071 210.085)" rx="10"/><g transform="rotate(15 -104.714 891.23)"><rect width="159.8" height="127.81" x="1.64" y="10.06" fill="#f9f9f9" rx="8"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#d)" xlink:href="#a"/><path fill="#d2caea" fill-rule="nonzero" d="M96.153 81.151a2.001 2.001 0 0 0 2.184-.496l35.956-38.34a2 2 0 1 0-2.918-2.736l-35.03 37.36-41.888-16.285a2 2 0 0 0-2.16.471l-26.368 27.16a2 2 0 1 0 2.87 2.786l25.444-26.21 41.911 16.294"/><g fill="#fff" transform="translate(24.368 36.951)"><circle cx="5.716" cy="5.104" r="5" stroke="#6b4fbb" stroke-width="4" transform="translate(65.917 34.945)"/><g stroke="#fb722e"><ellipse cx="4.632" cy="50.05" stroke-width="3.2" rx="4" ry="3.999"/><g stroke-width="4"><ellipse cx="29.632" cy="27.05" rx="4" ry="3.999"/><ellipse cx="107.63" cy="4.048" rx="4" ry="3.999"/></g></g></g></g><rect width="160.19" height="128.19" x="36.28" y="86.74" fill="#f9f9f9" transform="rotate(-5 116.372 150.825)" rx="10"/><g transform="rotate(5 -1514.687 1518.752)"><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#e)" xlink:href="#b"/><path fill="#6b4fbb" stroke="#6b4fbb" stroke-width="3.2" d="M84.67 28.41c18.225 0 33 15.07 33 33.651h-33V28.41" stroke-linecap="round" stroke-linejoin="round"/><path fill="#d2caea" fill-rule="nonzero" d="M78.67 66.41h30a2 2 0 0 1 2 2c0 18.778-15.222 34-34 34s-34-15.222-34-34 15.222-34 34-34a2 2 0 0 1 2 2v30m-32 2c0 16.569 13.431 30 30 30 15.896 0 28.905-12.364 29.934-28H76.67a2 2 0 0 1-2-2V38.476c-15.636 1.029-28 14.04-28 29.934"/></g><g transform="rotate(-5 1023.06 -299.524)"><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#f)" xlink:href="#c"/><path fill="#fef0ea" d="M42 47.391c0-.768.628-1.391 1.4-1.391h9.2c.773 0 1.4.626 1.4 1.391V97H42V47.391"/><path fill="#fb722e" d="M108 55.406c0-.777.628-1.406 1.4-1.406h9.2a1.4 1.4 0 0 1 1.4 1.406V97h-12V55.406"/><path fill="#6b4fbb" d="M64 35.404c0-.776.628-1.404 1.4-1.404h9.2a1.4 1.4 0 0 1 1.4 1.404v61.6H64v-61.6"/><path fill="#d2caea" d="M86 73.4a1.4 1.4 0 0 1 1.4-1.398h9.2c.773 0 1.4.618 1.4 1.398v23.602H86V73.4"/></g><g fill="#fee8dc"><path d="M3.592 93.86l-2.454-1.562c-.93-.592-.924-1.554 0-2.143l2.454-1.562 1.562-2.454c.592-.93 1.554-.925 2.143 0l1.562 2.454 2.454 1.562c.93.591.924 1.554 0 2.143L8.86 93.86l-1.562 2.454c-.591.93-1.554.924-2.143 0L3.592 93.86M309.489 52.07l-3.14-1.998c-1.12-.713-1.128-1.863 0-2.581l3.14-2 1.999-3.14c.713-1.12 1.863-1.127 2.58 0l2 3.14 3.14 2c1.12.713 1.128 1.863 0 2.58l-3.14 2-2 3.14c-.712 1.12-1.862 1.128-2.58 0l-1.999-3.14"/></g><path fill="#e1dcf1" d="M128.073 11.066l-1.99 3.126c-.718 1.129-1.88 1.131-2.6 0l-1.99-3.126-3.126-1.989c-1.128-.718-1.13-1.88 0-2.6l3.127-1.99 1.989-3.126c.718-1.128 1.88-1.13 2.6 0l1.99 3.126 3.126 1.99c1.128.718 1.13 1.88 0 2.6l-3.126 1.99"/><path fill="#d2caea" d="M378.07 243.068l-1.989 3.126c-.718 1.129-1.88 1.131-2.6 0l-1.99-3.126-3.126-1.989c-1.128-.718-1.13-1.88 0-2.6l3.127-1.99 1.989-3.126c.718-1.128 1.88-1.13 2.6 0l1.99 3.126 3.126 1.99c1.128.718 1.13 1.88 0 2.6l-3.126 1.99"/></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/monitoring/loading.svg b/app/assets/images/illustrations/monitoring/loading.svg deleted file mode 100644 index 1e196fc8ad1..00000000000 --- a/app/assets/images/illustrations/monitoring/loading.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 406 305" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="c" width="161" height="100" x="92" y="181" rx="10"/><rect id="d" width="151" height="32" x="20" rx="10"/><rect id="a" width="191" height="62" y="10" rx="10"/><circle id="b" cx="23" cy="41" r="9"/><circle id="k" cx="36.5" cy="36.5" r="36.5"/><circle id="e" cx="262.5" cy="169.5" r="15.5"/><circle id="g" cx="79.5" cy="169.5" r="15.5"/><circle id="j" cx="45" cy="41" r="9"/><circle id="f" cx="30.5" cy="30.5" r="30.5"/><circle id="h" cx="18" cy="34" r="3"/><ellipse id="i" cx="43.5" cy="43.5" rx="43.5" ry="43.5"/><mask id="t" width="191" height="62" x="0" y="0" fill="#fff"><use xlink:href="#a"/></mask><mask id="u" width="18" height="18" x="0" y="0" fill="#fff"><use xlink:href="#b"/></mask><mask id="r" width="161" height="100" x="0" y="0" fill="#fff"><use xlink:href="#c"/></mask><mask id="s" width="151" height="32" x="0" y="0" fill="#fff"><use xlink:href="#d"/></mask><mask id="p" width="31" height="31" x="0" y="0" fill="#fff"><use xlink:href="#e"/></mask><mask id="l" width="61" height="61" x="0" y="0" fill="#fff"><use xlink:href="#f"/></mask><mask id="q" width="31" height="31" x="0" y="0" fill="#fff"><use xlink:href="#g"/></mask><mask id="m" width="6" height="6" x="0" y="0" fill="#fff"><use xlink:href="#h"/></mask><mask id="o" width="87" height="87" x="0" y="0" fill="#fff"><use xlink:href="#i"/></mask><mask id="v" width="18" height="18" x="0" y="0" fill="#fff"><use xlink:href="#j"/></mask><mask id="n" width="73" height="73" x="0" y="0" fill="#fff"><use xlink:href="#k"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(28 2)"><g transform="translate(133 87)"><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#l)" xlink:href="#f"/><path stroke="#d2caea" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" d="M19 32l2-9 5 17 4-12 4 5 6-10 3 5"/><g fill="#fff" stroke="#fb722e"><use stroke-width="4" mask="url(#m)" xlink:href="#h"/><circle cx="44" cy="30" r="2" stroke-width="2"/></g></g><g transform="translate(188 29)"><circle cx="36.5" cy="41.5" r="36.5" fill="#f9f9f9"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#n)" xlink:href="#k"/><rect width="27" height="4" x="23" y="27" fill="#d2caea" rx="2"/><rect width="10.5" height="4" x="23" y="27" fill="#6b4fbb" rx="2"/><rect width="27" height="4" x="23" y="36" fill="#d2caea" rx="2"/><rect width="19" height="4" x="23" y="36" fill="#6b4fbb" rx="2"/><rect width="27" height="4" x="23" y="45" fill="#d2caea" rx="2"/><rect width="7" height="4" x="23" y="45" fill="#6b4fbb" rx="2"/></g><path fill="#eee" fill-rule="nonzero" d="M247 292v1c0 5.519-4.469 9.993-10.01 9.993H111c-5.177 0-9.436-3.927-9.954-8.96a9.96 9.96 0 0 0 4.705 1.883 6.008 6.008 0 0 0 5.248 3.077h125.99a6 6 0 0 0 5.526-3.637 10.027 10.027 0 0 0 4.48-3.359m1.947-8.962a10.001 10.001 0 0 1-9.95 8.958h-131.99a10 10 0 0 1-9.851-8.25 9.942 9.942 0 0 0 4.649 1.248 6 6 0 0 0 5.202 3h131.99a6.002 6.002 0 0 0 5.245-3.076 9.943 9.943 0 0 0 4.705-1.882"/><g transform="translate(79)"><ellipse cx="43.5" cy="47.5" fill="#f9f9f9" rx="43.5" ry="43.5"/><g fill="#fff"><g stroke="#eee"><use stroke-width="8" mask="url(#o)" xlink:href="#i"/><path stroke-width="4" d="M18.595 49C21.11 60.44 31.305 69 43.5 69 57.58 69 69 57.583 69 43.5c0-12.195-8.56-22.391-20-24.905v15.959c3 1.848 5 5.164 5 8.946C54 49.299 49.299 54 43.5 54c-3.782 0-7.098-2-8.946-5H18.595" stroke-linejoin="round"/></g><path stroke="#d2caea" stroke-width="4" d="M18 44a27.69 27.69 0 0 1-.005-.5c0-14.08 11.417-25.5 25.5-25.5.167 0 .334.002.5.005v15.01a10.365 10.365 0 0 0-.5-.012c-5.799 0-10.5 4.701-10.5 10.5 0 .168.004.334.012.5h-15.01" stroke-linejoin="round"/></g></g><g fill="#fff" stroke="#eee" stroke-width="8"><use mask="url(#p)" xlink:href="#e"/><use mask="url(#q)" xlink:href="#g"/><use mask="url(#r)" xlink:href="#c"/></g><g fill="#eee"><rect width="15" height="2" x="226" y="247" rx="1"/><rect width="15" height="2" x="226" y="242" rx="1"/><rect width="15" height="2" x="226" y="252" rx="1"/></g><rect width="10" height="52" x="118" y="196" fill="#d2caea" rx="2"/><rect width="10" height="47" x="154" y="196" fill="#6b4fbb" rx="2"/><rect width="10" height="37" x="190" y="196" fill="#d2caea" rx="2"/><g fill="#fee8dc"><rect width="10" height="52" x="132" y="185" rx="2"/><rect width="10" height="38" x="168" y="185" rx="2"/></g><rect width="10" height="58" x="204" y="185" fill="#fb722e" rx="2"/><g fill="#fff" stroke="#eee" stroke-width="8" transform="translate(76 128)"><use mask="url(#s)" xlink:href="#d"/><use mask="url(#t)" xlink:href="#a"/></g><g fill="#d2caea" transform="translate(76 128)"><rect width="16" height="4" x="156" y="35" rx="2"/><rect width="16" height="4" x="156" y="43" rx="2"/></g><g fill="#fff" stroke-width="8" transform="translate(76 128)"><use stroke="#fee8dc" mask="url(#u)" xlink:href="#b"/><use stroke="#fb722e" mask="url(#v)" xlink:href="#j"/></g><g fill="#fb722e"><path d="M3.597 219.858l-2.455-1.562c-.929-.59-.924-1.553 0-2.142l2.455-1.562 1.562-2.455c.59-.929 1.553-.924 2.142 0l1.562 2.455 2.454 1.562c.93.591.925 1.553 0 2.142l-2.454 1.562-1.562 2.455c-.591.929-1.553.924-2.142 0l-1.562-2.455M253.597 8.859l-2.454-1.562c-.93-.592-.925-1.554 0-2.143l2.454-1.562 1.562-2.454c.591-.93 1.554-.925 2.143 0l1.562 2.454 2.454 1.562c.93.591.924 1.554 0 2.143l-2.454 1.562-1.562 2.454c-.592.93-1.554.924-2.143 0l-1.562-2.454" opacity=".2"/></g><path fill="#fee8dc" d="M309.49 149.07l-3.141-1.999c-1.12-.712-1.128-1.863 0-2.58l3.14-2 2-3.14c.712-1.12 1.863-1.128 2.58 0l2 3.14 3.14 2c1.12.712 1.127 1.863 0 2.58l-3.14 2-2 3.14c-.713 1.12-1.863 1.128-2.58 0l-2-3.14"/><path fill="#6b4fbb" d="M47.068 79.067l-1.99 3.126c-.718 1.129-1.88 1.13-2.6 0l-1.99-3.126-3.125-1.99c-1.129-.718-1.131-1.88 0-2.6l3.126-1.989 1.989-3.126c.718-1.129 1.88-1.13 2.6 0l1.99 3.126 3.126 1.99c1.128.718 1.13 1.88 0 2.6l-3.126 1.989" opacity=".2"/></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/monitoring/unable_to_connect.svg b/app/assets/images/illustrations/monitoring/unable_to_connect.svg deleted file mode 100644 index 314c052f931..00000000000 --- a/app/assets/images/illustrations/monitoring/unable_to_connect.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 406 305" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><use id="g" xlink:href="#a"/><use id="f" xlink:href="#a"/><use id="h" xlink:href="#a"/><path id="e" d="M74 93h26v47H74z"/><path id="c" d="M74 93h26v47H74z"/><rect id="b" width="65" height="14" x="55" y="135" rx="4"/><rect id="d" width="175" height="118" rx="10"/><rect id="a" width="159" rx="10" height="56"/><rect id="i" width="160" y="2" rx="10" height="56" fill="#f9f9f9"/><mask id="q" width="65" height="14" x="0" y="0" fill="#fff"><use xlink:href="#b"/></mask><mask id="p" width="26" height="47" x="0" y="0" fill="#fff"><use xlink:href="#c"/></mask><mask id="r" width="175" height="118" x="0" y="0" fill="#fff"><use xlink:href="#d"/></mask><mask id="o" width="26" height="47" x="0" y="0" fill="#fff"><use xlink:href="#e"/></mask><mask id="k" width="159" height="56" x="0" y="0" fill="#fff"><use xlink:href="#f"/></mask><mask id="j" width="159" height="56" x="0" y="0" fill="#fff"><use xlink:href="#g"/></mask><mask id="l" width="159" height="56" x="0" y="0" fill="#fff"><use xlink:href="#h"/></mask></defs><g fill="none" fill-rule="evenodd"><g transform="translate(245 65)"><use xlink:href="#i"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#j)" xlink:href="#g"/><g fill-rule="nonzero"><path fill="#fb722e" d="M134 31a2 2 0 1 0 .001-3.999A2 2 0 0 0 134 31m0 4a6 6 0 1 1 0-12 6 6 0 0 1 0 12"/><path fill="#fee8dc" d="M117 31a2 2 0 1 0 .001-3.999A2 2 0 0 0 117 31m0 4a6 6 0 1 1 0-12 6 6 0 0 1 0 12m-17-4a2 2 0 1 0 .001-3.999A2 2 0 0 0 100 31m0 4a6 6 0 1 1 0-12 6 6 0 0 1 0 12"/></g><g fill="#d2caea"><rect width="50" height="4" x="19" y="20" rx="2"/><rect width="50" height="4" x="19" y="34" rx="2"/></g><g transform="translate(0 59)"><use xlink:href="#i"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#k)" xlink:href="#f"/><g fill-rule="nonzero"><path fill="#fee8dc" d="M134 30a2 2 0 1 0 .001-3.999A2 2 0 0 0 134 30m0 4a6 6 0 1 1 0-12 6 6 0 0 1 0 12"/><path fill="#fb722e" d="M117 30a2 2 0 1 0 .001-3.999A2 2 0 0 0 117 30m0 4a6 6 0 1 1 0-12 6 6 0 0 1 0 12"/><path fill="#fee8dc" d="M100 30a2 2 0 1 0 .001-3.999A2 2 0 0 0 100 30m0 4a6 6 0 1 1 0-12 6 6 0 0 1 0 12"/></g><rect width="50" height="4" x="19" y="19" fill="#d2caea" rx="2" id="m"/><rect width="50" height="4" x="19" y="33" fill="#d2caea" rx="2" id="n"/></g><g transform="translate(0 118)"><use xlink:href="#i"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#l)" xlink:href="#h"/><g fill-rule="nonzero"><path fill="#fb722e" d="M134 30a2 2 0 1 0 .001-3.999A2 2 0 0 0 134 30m0 4a6 6 0 1 1 0-12 6 6 0 0 1 0 12"/><path fill="#fee8dc" d="M117 30a2 2 0 1 0 .001-3.999A2 2 0 0 0 117 30m0 4a6 6 0 1 1 0-12 6 6 0 0 1 0 12m-17-4a2 2 0 1 0 .001-3.999A2 2 0 0 0 100 30m0 4a6 6 0 1 1 0-12 6 6 0 0 1 0 12"/></g><use xlink:href="#m"/><use xlink:href="#n"/></g></g><g fill="#eee" transform="translate(164 120)"><rect width="29" height="4" y="29" rx="2"/><rect width="28" height="4" x="55" y="29" rx="2"/></g><g transform="translate(180 120)"><circle cx="30" cy="30" r="24" fill="#fef0ea"/><g fill="#fb722e"><circle cx="30.5" cy="30.5" r="30.5" opacity=".1"/><circle cx="30.5" cy="30.5" r="19.5" opacity=".1"/></g><circle cx="30.5" cy="30.5" r="13.5" fill="#fff"/><path fill="#fb722e" d="M32.621 30.5l2.481-2.481a1.492 1.492 0 0 0-.006-2.115 1.491 1.491 0 0 0-2.115-.006L30.5 28.379l-2.481-2.481a1.492 1.492 0 0 0-2.115.006 1.491 1.491 0 0 0-.006 2.115l2.481 2.481-2.481 2.481a1.492 1.492 0 0 0 .006 2.115c.59.59 1.533.589 2.115.006l2.481-2.481 2.481 2.481c.586.586 1.529.58 2.115-.006.59-.59.589-1.533.006-2.115L32.621 30.5"/></g><g transform="translate(1 78)"><rect width="65" height="14" x="55" y="137" fill="#f9f9f9" rx="4"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#o)" xlink:href="#e"/><rect width="175" height="118" y="3" fill="#f9f9f9" rx="10"/><g fill="#fff" stroke="#eee" stroke-width="8"><use mask="url(#p)" xlink:href="#c"/><use mask="url(#q)" xlink:href="#b"/><use mask="url(#r)" xlink:href="#d"/></g><g fill-rule="nonzero"><path fill="#eee" d="M163 105V12H11v93h152M7 11.99A3.998 3.998 0 0 1 10.995 8h152.01A3.999 3.999 0 0 1 167 11.99v93.02a3.998 3.998 0 0 1-3.995 3.99H10.995A3.999 3.999 0 0 1 7 105.01V11.99"/><path fill="#d2caea" d="M86 92c-11.598 0-21-9.402-21-21s9.402-21 21-21 21 9.402 21 21-9.402 21-21 21m0-4c9.389 0 17-7.611 17-17s-7.611-17-17-17-17 7.611-17 17 7.611 17 17 17"/></g><path fill="#6b4fbb" d="M83 63a3.001 3.001 0 0 1 6 0v7.993a3.001 3.001 0 0 1-6 0V63m3 18.997a3 3 0 1 1 0-6 3 3 0 0 1 0 6"/><g fill="#eee"><rect width="134" height="4" x="20" y="30" rx="2"/><rect width="14" height="4" x="20" y="20" rx="2"/><circle cx="87" cy="21" r="5"/></g></g></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/multi-editor_all_changes_committed_empty.svg b/app/assets/images/illustrations/multi-editor_all_changes_committed_empty.svg deleted file mode 100644 index 06d73941c33..00000000000 --- a/app/assets/images/illustrations/multi-editor_all_changes_committed_empty.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 80 80"><g fill="none" fill-rule="evenodd"><path fill="#EEE" d="M44.242 59.348c-3.7 1.576-7.3 1.994-10.902.84a7.002 7.002 0 0 1-9.085-.699l-4.243-4.243a7 7 0 0 1-.238-9.649c-.701-3.024-.419-6.083.646-9.206l-6.287-2.426a5.6 5.6 0 0 1-2.274-8.824l8.233-9.811a5.6 5.6 0 0 1 6.306-1.625l8.045 3.105c.772-.797 1.564-1.6 2.374-2.41C44.841 6.376 55.265 2.135 68.09 1.677a10 10 0 0 1 1.119.023c5.507.42 9.63 5.226 9.209 10.733-.935 12.225-5.373 22.309-13.315 30.25a410.76 410.76 0 0 1-1.661 1.653l3.247 8.412a5.6 5.6 0 0 1-1.625 6.306l-9.81 8.233a5.6 5.6 0 0 1-8.825-2.274l-2.186-5.665zm-22.92-26.923l10.406-12.402-6.822-2.633a1.6 1.6 0 0 0-1.801.464l-8.233 9.811a1.6 1.6 0 0 0 .65 2.521l5.8 2.239zm26.646 25.4l2.239 5.8a1.6 1.6 0 0 0 2.521.649l9.81-8.232a1.6 1.6 0 0 0 .465-1.802l-2.633-6.822-12.402 10.406zm-19.69-5.627c8.751 8.752 16.065 5.587 33.995-12.343 7.25-7.25 11.292-16.433 12.155-27.727a6 6 0 0 0-6.196-6.454c-11.846.423-21.303 4.271-28.586 11.554-17.03 17.03-20.414 25.924-11.368 34.97z"/><path fill="#FDC4A8" fill-rule="nonzero" d="M52.54 28.376a4 4 0 1 0 5.656-5.657 4 4 0 0 0-5.657 5.657zm-2.83 2.829A8 8 0 1 1 61.025 19.89a8 8 0 0 1-11.313 11.314z"/><path fill="#FEE1D3" d="M15.063 54.54a2 2 0 0 1 0 2.828L3.749 68.68A2 2 0 1 1 .92 65.853l11.314-11.314a2 2 0 0 1 2.829 0zm9.899 9.899a2 2 0 0 1 0 2.828l-8.485 8.485a2 2 0 1 1-2.829-2.828l8.486-8.485a2 2 0 0 1 2.828 0z"/><path fill="#FDC4A8" d="M20.012 59.489a2 2 0 0 1 0 2.828L4.456 77.874a2 2 0 0 1-2.829-2.829L17.184 59.49a2 2 0 0 1 2.828 0z"/></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/multi-editor_no_changes_empty.svg b/app/assets/images/illustrations/multi-editor_no_changes_empty.svg deleted file mode 100644 index ebeea1f3dd9..00000000000 --- a/app/assets/images/illustrations/multi-editor_no_changes_empty.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 80 80"><g fill="none" fill-rule="evenodd" transform="translate(7 3)"><path fill="#EEE" fill-rule="nonzero" d="M54 18a2 2 0 1 1 0-4h4c.843 0 1.675.105 2.48.31a2 2 0 1 1-.99 3.876A6.015 6.015 0 0 0 58 18h-4zm9.735 4.228a2 2 0 0 1 3.822-1.18A10 10 0 0 1 68 24v3.513a2 2 0 1 1-4 0V24c0-.61-.09-1.204-.265-1.772zM64 35.513a2 2 0 1 1 4 0v6a2 2 0 1 1-4 0v-6zm0 14a2 2 0 1 1 4 0v6a2 2 0 1 1-4 0v-6zm0 14a2 2 0 1 1 4 0V66a9.97 9.97 0 0 1-.963 4.286 2 2 0 1 1-3.613-1.716A5.969 5.969 0 0 0 64 66v-2.487zm-5.255 8.441a2 2 0 1 1 .49 3.97c-.401.05-.806.075-1.218.076h-5.042a2 2 0 1 1 0-4h5.038c.246 0 .49-.016.732-.046zM44.975 72a2 2 0 1 1 0 4h-6a2 2 0 1 1 0-4h6zm-14 0a2 2 0 1 1 0 4H26c-.429 0-.855-.027-1.276-.08a2 2 0 0 1 .506-3.969c.254.033.51.049.77.049h4.975zm-10.438-3.514a2 2 0 1 1-3.64 1.66A9.97 9.97 0 0 1 16 66v-2.538a2 2 0 1 1 4 0V66c0 .871.185 1.713.537 2.486zM8 2a6 6 0 0 0-6 6v42a6 6 0 0 0 6 6h32a6 6 0 0 0 6-6V8a6 6 0 0 0-6-6H8zm0-4h32c5.523 0 10 4.477 10 10v42c0 5.523-4.477 10-10 10H8C2.477 60-2 55.523-2 50V8C-2 2.477 2.477-2 8-2z"/><rect width="10" height="4" x="8" y="16" fill="#EFEDF8" rx="2"/><rect width="10" height="4" x="21" y="16" fill="#6B4FBB" rx="2"/><rect width="10" height="4" x="8" y="32" fill="#E1DBF1" rx="2"/><rect width="6" height="4" x="34" y="16" fill="#EFEDF8" rx="2"/><rect width="6" height="4" x="8" y="24" fill="#6B4FBB" rx="2"/><rect width="6" height="4" x="17" y="24" fill="#EFEDF8" rx="2"/><rect width="6" height="4" x="21" y="32" fill="#6B4FBB" rx="2"/><rect width="6" height="4" x="8" y="40" fill="#6B4FBB" rx="2"/><rect width="6" height="4" x="17" y="40" fill="#EFEDF8" rx="2"/><rect width="6" height="4" x="26" y="40" fill="#C3B8E3" rx="2"/><rect width="10" height="4" x="26" y="24" fill="#C3B8E3" rx="2"/></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/multi-editor_no_staged_files_empty.svg b/app/assets/images/illustrations/multi-editor_no_staged_files_empty.svg deleted file mode 100644 index 08321ef526b..00000000000 --- a/app/assets/images/illustrations/multi-editor_no_staged_files_empty.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 80 80"><g fill="none" fill-rule="evenodd" transform="translate(0 3)"><path fill="#EEE" fill-rule="nonzero" d="M40.843 5.864a2 2 0 1 1 .348-3.985l5.977.523a2 2 0 1 1-.348 3.985l-5.977-.523zm13.946 1.22a2 2 0 1 1 .349-3.985l5.977.523a2 2 0 1 1-.348 3.985l-5.978-.523zm13.947 1.22a2 2 0 1 1 .349-3.984 11.952 11.952 0 0 1 6.655 2.75 2 2 0 1 1-2.569 3.066 7.953 7.953 0 0 0-4.435-1.832zm7.28 7.357a2 2 0 1 1 3.99-.301c.048.639.045 1.283-.01 1.934l-.385 4.4a2 2 0 1 1-3.985-.349l.384-4.395c.037-.433.039-.863.007-1.29zm-1.088 13.654a2 2 0 0 1 3.985.348l-.523 5.978a2 2 0 1 1-3.984-.349l.522-5.977zm-1.22 13.947a2 2 0 1 1 3.985.348l-.523 5.977a2 2 0 1 1-3.985-.348l.523-5.977zM72.305 56.7a2 2 0 0 1 3.79 1.282 11.995 11.995 0 0 1-4.253 5.81 2 2 0 0 1-2.373-3.22 7.996 7.996 0 0 0 2.836-3.872zm-9.054 5.33a2 2 0 1 1-.349 3.985l-5.977-.522a2 2 0 1 1 .349-3.985l5.977.523zM32.793 10.675a2 2 0 1 1-3.675-1.579 12.02 12.02 0 0 1 4.696-5.456 2 2 0 0 1 2.112 3.397 8.02 8.02 0 0 0-3.133 3.638z"/><rect width="48" height="58" x="2" y="14" fill="#FAFAFA" rx="10"/><path fill="#EEE" fill-rule="nonzero" d="M12 16a8 8 0 0 0-8 8v38a8 8 0 0 0 8 8h28a8 8 0 0 0 8-8V24a8 8 0 0 0-8-8H12zm0-4h28c6.627 0 12 5.373 12 12v38c0 6.627-5.373 12-12 12H12C5.373 74 0 68.627 0 62V24c0-6.627 5.373-12 12-12z"/><rect width="24" height="4" x="11" y="30" fill="#E5E5E5" rx="2"/><rect width="30" height="4" x="11" y="41" fill="#E5E5E5" rx="2"/><rect width="20" height="4" x="11" y="52" fill="#E5E5E5" rx="2"/></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/multi_file_editor_empty.svg b/app/assets/images/illustrations/multi_file_editor_empty.svg deleted file mode 100644 index bd376f0a050..00000000000 --- a/app/assets/images/illustrations/multi_file_editor_empty.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="430" height="300"><g fill="none" fill-rule="evenodd" transform="translate(35 29)"><path fill="#EEE" fill-rule="nonzero" d="M90 23a2 2 0 1 1 0-4h10a2 2 0 0 1 0 4H90zm20 0a2 2 0 0 1 0-4h10a2 2 0 0 1 0 4h-10zm20 0a2 2 0 0 1 0-4h10a2 2 0 0 1 0 4h-10zm20 0a2 2 0 0 1 0-4h10a2 2 0 0 1 0 4h-10zm20 0a2 2 0 0 1 0-4h10a2 2 0 0 1 0 4h-10zm20 0a2 2 0 0 1 0-4h10a2 2 0 0 1 0 4h-10zm20 0a2 2 0 0 1 0-4h10a2 2 0 0 1 0 4h-10zm20 0a2 2 0 0 1 0-4h10a2 2 0 0 1 0 4h-10zm20 0a2 2 0 0 1 0-4h10a2 2 0 0 1 0 4h-10zm20 0a2 2 0 0 1 0-4h10a2 2 0 0 1 0 4h-10zm20 0a2 2 0 0 1 0-4h10a2 2 0 0 1 0 4h-10zm20 0a2 2 0 0 1 0-4h10a2 2 0 0 1 0 4h-10zm20 0a2 2 0 0 1 0-4h1a11.98 11.98 0 0 1 9.457 4.612 2 2 0 0 1-3.151 2.464A7.981 7.981 0 0 0 331 23h-1zm9 11.39a2 2 0 0 1 4 0v10a2 2 0 0 1-4 0v-10zm0 180a2 2 0 1 1 4 0V223c0 .56-.038 1.114-.114 1.662a2 2 0 0 1-3.962-.55A8.21 8.21 0 0 0 339 223v-8.61zm-4.769 15.931a2 2 0 0 1 1.618 3.658A11.967 11.967 0 0 1 331 235h-5.782a2 2 0 0 1 0-4H331c1.13 0 2.224-.233 3.231-.679zm-19.013.679a2 2 0 1 1 0 4h-10a2 2 0 0 1 0-4h10zm-20 0a2 2 0 1 1 0 4h-10a2 2 0 0 1 0-4h10zm-20 0a2 2 0 1 1 0 4h-10a2 2 0 0 1 0-4h10zm-20 0a2 2 0 1 1 0 4h-10a2 2 0 0 1 0-4h10zm-20 0a2 2 0 1 1 0 4h-10a2 2 0 0 1 0-4h10zm-20 0a2 2 0 1 1 0 4h-10a2 2 0 0 1 0-4h10zm-20 0a2 2 0 1 1 0 4h-10a2 2 0 0 1 0-4h10zm-20 0a2 2 0 1 1 0 4h-10a2 2 0 0 1 0-4h10zm-20 0a2 2 0 1 1 0 4h-10a2 2 0 0 1 0-4h10zm-20 0a2 2 0 1 1 0 4h-10a2 2 0 0 1 0-4h10zM115 231a2 2 0 0 1 0 4h-10a2 2 0 0 1 0-4h10zm-26.2 4c.131-.646.2-1.315.2-2v-2h4a2 2 0 0 1 0 4h-4.2z"/><path fill="#EEE" fill-rule="nonzero" d="M103 211h258a6 6 0 0 0 6-6V63a6 6 0 0 0-6-6H166a5 5 0 0 1-5-5v-8.5a5.5 5.5 0 0 0-5.5-5.5H109a6 6 0 0 0-6 6v167zm62-167.5V52a1 1 0 0 0 1 1h195c5.523 0 10 4.477 10 10v142c0 5.523-4.477 10-10 10H99V44c0-5.523 4.477-10 10-10h46.5a9.5 9.5 0 0 1 9.5 9.5z"/><rect width="40" height="4" x="118" y="78" fill="#6B4FBB" rx="2"/><rect width="30" height="4" x="118" y="90" fill="#EFEDF8" rx="2"/><rect width="30" height="4" x="153" y="90" fill="#E1DBF1" rx="2"/><rect width="150" height="4" x="118" y="102" fill="#EFEDF8" rx="2"/><rect width="90" height="4" x="118" y="114" fill="#E1DBF1" rx="2"/><rect width="60" height="4" x="118" y="138" fill="#EFEDF8" rx="2"/><rect width="20" height="4" x="118" y="150" fill="#6B4FBB" rx="2"/><rect width="20" height="4" x="144" y="150" fill="#C3B8E3" rx="2"/><rect width="20" height="4" x="170" y="150" fill="#E1DBF1" rx="2"/><rect width="130" height="4" x="118" y="162" fill="#EFEDF8" rx="2"/><rect width="30" height="4" x="118" y="174" fill="#C3B8E3" rx="2"/><rect width="30" height="4" x="154" y="174" fill="#EFEDF8" rx="2"/><rect width="30" height="4" x="190" y="174" fill="#EFEDF8" rx="2"/><rect width="40" height="4" x="118" y="186" fill="#E1DBF1" rx="2"/><path fill="#F9F9F9" d="M89 24.292l11.434 19.326v170.326L89 226.336V24.292z"/><path fill="#EEE" fill-rule="nonzero" d="M89 229.286v-5.9l9.434-10.223V44.165L89 28.22v-7.856l13.434 22.707v171.655L89 229.286zM10 4a6 6 0 0 0-6 6v223a6 6 0 0 0 6 6h69a6 6 0 0 0 6-6V10a6 6 0 0 0-6-6H10zm0-4h69c5.523 0 10 4.477 10 10v223c0 5.523-4.477 10-10 10H10c-5.523 0-10-4.477-10-10V10C0 4.477 4.477 0 10 0z"/><circle cx="25" cy="23" r="11" fill="#FEF0E8"/><path fill="#FEE1D3" d="M46 17h16a2 2 0 1 1 0 4H46a2 2 0 1 1 0-4zm0 8h27a2 2 0 1 1 0 4H46a2 2 0 1 1 0-4z"/><path fill="#EEE" d="M16 50h4a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2zm14 2h24a2 2 0 1 1 0 4H30a2 2 0 1 1 0-4zm-4 12h4a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2zm14 2h24a2 2 0 1 1 0 4H40a2 2 0 1 1 0-4zM26 78h4a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2zm14 2h24a2 2 0 1 1 0 4H40a2 2 0 1 1 0-4z"/><g transform="translate(14 110)"><rect width="8" height="8" fill="#FEE1D3" rx="2"/><rect width="28" height="4" x="14" y="2" fill="#FEF0E8" rx="2"/></g><path fill="#EEE" d="M16 140h4a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2zm14 2h24a2 2 0 1 1 0 4H30a2 2 0 1 1 0-4zm-14 14h4a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2zm14 2h24a2 2 0 1 1 0 4H30a2 2 0 1 1 0-4zm-14 14h4a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2zm14 2h24a2 2 0 1 1 0 4H30a2 2 0 1 1 0-4zm-14 14h4a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2zm14 2h24a2 2 0 1 1 0 4H30a2 2 0 1 1 0-4z"/><g transform="translate(24 124)"><rect width="8" height="8" fill="#FEE1D3" rx="2"/><rect width="28" height="4" x="14" y="2" fill="#FEF0E8" rx="2"/></g><g fill="#FC6D26" transform="translate(24 92)"><rect width="8" height="8" rx="2"/><rect width="28" height="4" x="14" y="2" rx="2"/></g><path fill="#FDC4A8" fill-rule="nonzero" d="M152 50.5a4.5 4.5 0 1 1 0-9 4.5 4.5 0 0 1 0 9zm0-3a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z"/></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/no_commits.svg b/app/assets/images/illustrations/no_commits.svg deleted file mode 100644 index 76fa25156dd..00000000000 --- a/app/assets/images/illustrations/no_commits.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 168 107" xmlns:xlink="http://www.w3.org/1999/xlink"><g fill="#eee" fill-rule="evenodd"><path d="M4.01 2h1.102a1 1 0 0 0 0-2H4.01A4.001 4.001 0 0 0 0 4a1 1 0 0 0 2 0c0-1.108.892-2 2.01-2m12.702 0a1 1 0 0 0 0-2h-5.7a1 1 0 0 0 0 2h5.7m11.6 0a1 1 0 0 0 0-2h-5.7a1 1 0 0 0 0 2h5.7m11.6 0a1 1 0 0 0 0-2h-5.7a1 1 0 0 0 0 2h5.7m11.6 0a1 1 0 0 0 0-2h-5.7a1 1 0 0 0 0 2h5.7m11.6 0a1 1 0 0 0 0-2h-5.7a1 1 0 0 0 0 2h5.7m11.6 0a1 1 0 0 0 0-2h-5.7a1 1 0 0 0 0 2h5.7m11.6 0a1 1 0 0 0 0-2h-5.7a1 1 0 0 0 0 2h5.7m11.6 0a1 1 0 0 0 0-2h-5.7a1 1 0 0 0 0 2h5.7m11.6 0a1 1 0 0 0 0-2h-5.7a1 1 0 0 0 0 2h5.7m11.6 0a1 1 0 0 0 0-2h-5.7a1 1 0 0 0 0 2h5.7m11.6 0a1 1 0 0 0 0-2h-5.7a1 1 0 0 0 0 2h5.7m11.6 0a1 1 0 0 0 0-2h-5.7a1 1 0 0 0 0 2h5.7m11.6 0a1 1 0 0 0 0-2h-5.7a1 1 0 0 0 0 2h5.7M164 2c.822 0 1.554.503 1.86 1.254a1 1 0 1 0 1.853-.753 4.01 4.01 0 0 0-3.712-2.5h-2.188a1 1 0 0 0 0 2h2.188m2.01 12.518a1 1 0 0 0 2 0v-5.7a1 1 0 0 0-2 0v5.7m0 11.6a1 1 0 0 0 2 0v-5.7a1 1 0 0 0-2 0v5.7m0 11.6a1 1 0 0 0 2 0v-5.7a1 1 0 0 0-2 0v5.7m0 6.282c0 1.108-.892 2-2.01 2h-.72a1 1 0 0 0 0 2h.72a4.001 4.001 0 0 0 4.01-4v-.382a1 1 0 0 0-2 0v.382m-14.325 2a1 1 0 0 0 0 2h5.7a1 1 0 0 0 0-2h-5.7m-11.6 0a1 1 0 0 0 0 2h5.7a1 1 0 0 0 0-2h-5.7m-11.6 0a1 1 0 0 0 0 2h5.7a1 1 0 0 0 0-2h-5.7m-11.6 0a1 1 0 0 0 0 2h5.7a1 1 0 0 0 0-2h-5.7m-11.6 0a1 1 0 0 0 0 2h5.7a1 1 0 0 0 0-2h-5.7m-11.6 0a1 1 0 0 0 0 2h5.7a1 1 0 0 0 0-2h-5.7m-11.6 0a1 1 0 0 0 0 2h5.7a1 1 0 0 0 0-2h-5.7m-11.6 0a1 1 0 0 0 0 2h5.7a1 1 0 0 0 0-2h-5.7m-11.6 0a1 1 0 0 0 0 2h5.7a1 1 0 0 0 0-2h-5.7m-11.6 0a1 1 0 0 0 0 2h5.7a1 1 0 0 0 0-2h-5.7m-11.6 0a1 1 0 0 0 0 2h5.7a1 1 0 0 0 0-2h-5.7m-11.6 0a1 1 0 0 0 0 2h5.7a1 1 0 0 0 0-2h-5.7m-11.6 0a1 1 0 0 0 0 2h5.7a1 1 0 0 0 0-2h-5.7m-8.47 0a2.01 2.01 0 0 1-1.782-1.085 1 1 0 0 0-1.775.923 4.007 4.007 0 0 0 3.556 2.162h2.57a1 1 0 0 0 0-2h-2.57m-2.01-12.136a1 1 0 0 0-2 0v5.7a1 1 0 0 0 2 0v-5.7m0-11.6a1 1 0 0 0-2 0v5.7a1 1 0 0 0 2 0v-5.7m0-11.6a1 1 0 0 0-2 0v5.7a1 1 0 0 0 2 0v-5.7m0-6.664a1 1 0 0 0-2 0v.764a1 1 0 0 0 2 0v-.764" id="a"/><circle cx="21" cy="24" r="10"/><rect width="33" height="3" x="37" y="18" rx="1.5" id="b"/><rect width="53" height="3" x="37" y="27" rx="1.5" id="c"/><path d="M131 29c0 .552.447.999.996.999h22.01c.545 0 .996-.451.996-.999v-9a.998.998 0 0 0-.996-.999h-22.01c-.545 0-.996.451-.996.999v9m.996-12h22.01a2.998 2.998 0 0 1 2.996 2.999v9a3.003 3.003 0 0 1-2.996 2.999h-22.01A2.998 2.998 0 0 1 129 28.999v-9A3.003 3.003 0 0 1 131.996 17" id="d"/><g transform="translate(0 59)"><use xlink:href="#a"/><circle cx="21" cy="24" r="10"/><use xlink:href="#b"/><use xlink:href="#c"/><use xlink:href="#d"/></g></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/pending_job_empty.svg b/app/assets/images/illustrations/pending_job_empty.svg deleted file mode 100644 index 8de695afa18..00000000000 --- a/app/assets/images/illustrations/pending_job_empty.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="430" height="200" viewBox="0 0 430 200"><g fill="none" fill-rule="evenodd"><g transform="translate(138 65)"><path fill="#E5E5E5" fill-rule="nonzero" d="M35 70a2 2 0 1 1 0-4c2.542 0 5.042-.305 7.463-.904a2 2 0 1 1 .96 3.884A35.075 35.075 0 0 1 35 70zm18.21-5.105a2 2 0 1 1-2.083-3.414 31.143 31.143 0 0 0 5.896-4.664 2 2 0 1 1 2.842 2.815 35.143 35.143 0 0 1-6.654 5.263zM66.106 51.06a2 2 0 0 1-3.552-1.838 30.77 30.77 0 0 0 2.612-7.042 2 2 0 1 1 3.892.922 34.77 34.77 0 0 1-2.952 7.958zm3.816-18.433a2 2 0 1 1-3.991.268 30.873 30.873 0 0 0-1.407-7.38 2 2 0 0 1 3.808-1.223 34.873 34.873 0 0 1 1.59 8.335zm-6.346-17.842a2 2 0 0 1-3.264 2.312 31.188 31.188 0 0 0-5.054-5.564 2 2 0 0 1 2.615-3.027 35.188 35.188 0 0 1 5.703 6.279zM48.895 2.867a2 2 0 0 1-1.59 3.67 30.758 30.758 0 0 0-7.206-2.12 2 2 0 1 1 .653-3.946 34.758 34.758 0 0 1 8.143 2.396zM30.263.318a2 2 0 0 1 .537 3.964c-2.505.339-4.94.98-7.266 1.907a2 2 0 1 1-1.48-3.716A34.774 34.774 0 0 1 30.263.318zM12.907 7.853a2 2 0 0 1 2.527 3.1 31.196 31.196 0 0 0-5.213 5.416 2 2 0 0 1-3.196-2.406 35.196 35.196 0 0 1 5.882-6.11zM1.99 23.343a2 2 0 0 1 3.772 1.331 30.82 30.82 0 0 0-1.619 7.337 2 2 0 1 1-3.982-.38 34.82 34.82 0 0 1 1.829-8.289zM.719 42.086a2 2 0 1 1 3.917-.806 30.757 30.757 0 0 0 2.4 7.118 2 2 0 1 1-3.605 1.73 34.757 34.757 0 0 1-2.713-8.042zM9.393 58.86a2 2 0 0 1 2.926-2.728 31.167 31.167 0 0 0 5.751 4.841 2 2 0 1 1-2.187 3.349 35.167 35.167 0 0 1-6.49-5.462zm16.245 9.873a2 2 0 1 1 1.067-3.855 30.979 30.979 0 0 0 7.434 1.11 2 2 0 1 1-.11 3.998 34.979 34.979 0 0 1-8.391-1.253z"/><circle cx="35" cy="35" r="16" stroke="#E1DBF1" stroke-width="4"/><path fill="#6B4FBB" d="M37 33h5a2 2 0 1 1 0 4h-7a2 2 0 0 1-2-2v-8a2 2 0 1 1 4 0v6z"/></g><g transform="translate(247 30)"><rect width="116" height="135" y="5" fill="#F9F9F9" rx="10"/><rect width="116" height="134" x="5" fill="#FFF" stroke="#EEE" stroke-width="4" stroke-linecap="round" rx="10"/><g transform="translate(23 23)"><rect width="16" height="4" fill="#E1DBF1" rx="2"/><rect width="16" height="4" x="32" y="12" fill="#E1DBF1" rx="2"/><rect width="16" height="4" x="44" fill="#EEE" rx="2"/><rect width="16" height="4" x="12" y="24" fill="#E1DBF1" rx="2"/><rect width="16" height="4" x="64" y="36" fill="#FEF0E8" rx="2"/><rect width="8" height="4" x="20" fill="#FEE1D3" rx="2"/><rect width="8" height="4" x="32" y="36" fill="#FC6D26" rx="2"/><rect width="8" height="4" x="52" y="12" fill="#FEF0E8" rx="2"/><rect width="8" height="4" x="64" fill="#FEF0E8" rx="2"/><rect width="12" height="4" x="16" y="48" fill="#E1DBF1" rx="2"/><rect width="8" height="4" x="44" y="36" fill="#FC6D26" rx="2"/><rect width="4" height="4" x="56" y="36" fill="#E1DBF1" rx="2"/><rect width="4" height="4" x="64" y="60" fill="#E1DBF1" rx="2"/><rect width="4" height="4" x="72" y="60" fill="#FC6D26" rx="2"/><rect width="8" height="4" x="32" fill="#FC6D26" rx="2"/><rect width="28" height="4" y="36" fill="#EEE" rx="2"/><rect width="28" height="4" x="44" y="48" fill="#EEE" rx="2"/><rect width="28" height="4" x="32" y="60" fill="#EFEDF8" rx="2"/><rect width="28" height="4" y="12" fill="#6B4FBB" rx="2"/><rect width="28" height="4" x="32" y="24" fill="#C3B8E3" rx="2"/><rect width="8" height="4" y="24" fill="#FEF0E8" rx="2"/><rect width="8" height="4" x="32" y="48" fill="#6B4FBB" rx="2"/><rect width="12" height="4" y="48" fill="#FC6D26" rx="2"/><rect width="12" height="4" y="60" fill="#FEF0E8" rx="2"/><rect width="12" height="4" x="16" y="60" fill="#FEF0E8" rx="2"/></g><g transform="translate(23 95)"><rect width="16" height="4" fill="#EFEDF8" rx="2"/><rect width="16" height="4" x="18" y="12" fill="#FC6D26" rx="2"/><rect width="16" height="4" x="44" fill="#6B4FBB" rx="2"/><rect width="8" height="4" x="20" fill="#FEE1D3" rx="2"/><rect width="8" height="4" x="38" y="12" fill="#FEF0E8" rx="2"/><rect width="8" height="4" x="64" fill="#FEF0E8" rx="2"/><rect width="8" height="4" x="32" fill="#FC6D26" rx="2"/><rect width="14" height="4" y="12" fill="#EEE" rx="2"/></g></g><path fill="#FC6D26" fill-rule="nonzero" d="M81 119c-10.493 0-19-8.507-19-19s8.507-19 19-19 19 8.507 19 19-8.507 19-19 19zm0-4c8.284 0 15-6.716 15-15 0-8.284-6.716-15-15-15-8.284 0-15 6.716-15 15 0 8.284 6.716 15 15 15zm-5-20a2 2 0 0 1 2 2v6a2 2 0 1 1-4 0v-6a2 2 0 0 1 2-2zm10 0a2 2 0 0 1 2 2v6a2 2 0 1 1-4 0v-6a2 2 0 0 1 2-2z"/><path fill="#E5E5E5" fill-rule="nonzero" d="M108 102c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004a1.994 1.994 0 0 1-1.998-2zm14 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004a1.994 1.994 0 0 1-1.998-2zm93 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004a1.994 1.994 0 0 1-1.998-2zm14 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004a1.994 1.994 0 0 1-1.998-2z"/></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/pipelines_empty.svg b/app/assets/images/illustrations/pipelines_empty.svg deleted file mode 100644 index f3107c8f062..00000000000 --- a/app/assets/images/illustrations/pipelines_empty.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 250 150"><g fill="none" fill-rule="evenodd"><g fill="#e5e5e5" transform="translate(0 102)"><rect width="74" height="4" x="34" y="21" opacity=".5" rx="2"/><path d="M152 23c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4A1.994 1.994 0 0 1 152 23m14 0c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4A1.994 1.994 0 0 1 166 23m14 0c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4A1.994 1.994 0 0 1 180 23m14 0c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4A1.994 1.994 0 0 1 194 23m14 0c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4A1.994 1.994 0 0 1 208 23"/></g><g fill="#31af64"><path fill-rule="nonzero" d="M19 144c-10.493 0-19-8.507-19-19s8.507-19 19-19 19 8.507 19 19-8.507 19-19 19m0-4c8.284 0 15-6.716 15-15 0-8.284-6.716-15-15-15-8.284 0-15 6.716-15 15 0 8.284 6.716 15 15 15"/><path d="M17.07 127.02l-2.829-2.829a1.995 1.995 0 0 0-2.828 0 1.995 1.995 0 0 0 0 2.828l4.243 4.243a1.995 1.995 0 0 0 2.822.006l7.79-7.79a1.997 1.997 0 0 0-.006-2.823 1.992 1.992 0 0 0-2.823-.006l-6.37 6.37"/></g><g fill="#e52c5a"><path fill-rule="nonzero" d="M126 149.5c-12.979 0-23.5-10.521-23.5-23.5s10.521-23.5 23.5-23.5 23.5 10.521 23.5 23.5-10.521 23.5-23.5 23.5m0-5c10.217 0 18.5-8.283 18.5-18.5s-8.283-18.5-18.5-18.5-18.5 8.283-18.5 18.5 8.283 18.5 18.5 18.5"/><path d="M130.24 126l2.833-2.833a3 3 0 0 0-4.243-4.243l-2.833 2.833-2.833-2.833a3 3 0 0 0-4.243 4.243l2.833 2.833-2.833 2.833a3 3 0 0 0 4.243 4.243l2.833-2.833 2.833 2.833a3 3 0 0 0 4.243-4.243L130.24 126"/></g><path fill="#e5e5e5" fill-rule="nonzero" d="M236 139c-7.732 0-14-6.268-14-14s6.268-14 14-14 14 6.268 14 14-6.268 14-14 14m0-4c5.523 0 10-4.477 10-10s-4.477-10-10-10-10 4.477-10 10 4.477 10 10 10"/><g transform="translate(73 4)"><path stroke="#e5e5e5" stroke-width="4" d="M64.82 76H98c4.419 0 8-3.579 8-7.99V7.99C106 3.577 102.417 0 98 0H8.009c-4.419 0-8 3.579-8 7.99v60.02c0 4.413 3.583 7.99 8 7.99h31.935l9.263 9.855a4.357 4.357 0 0 0 6.354 0L64.824 76"/><rect width="18" height="6" x="11" y="19" fill="#fc8a51" rx="3"/><rect width="18" height="6" x="35" y="35" fill="#e52c5a" rx="3"/><rect width="18" height="6" x="29" y="51" fill="#e5e5e5" rx="3"/><rect width="12" height="6" x="35" y="19" fill="#fde5d8" rx="3"/><rect width="12" height="6" x="53" y="51" fill="#e52c5a" rx="3"/><rect width="12" height="6" x="11" y="51" fill="#b5a7dd" rx="3"/><rect width="18" height="6" x="77" y="19" fill="#fc8a51" rx="3"/><rect width="18" height="6" x="11" y="35" fill="#fde5d8" rx="3"/><rect width="6" height="6" x="53" y="19" fill="#e52c5a" rx="3"/><g fill="#fde5d8"><rect width="6" height="6" x="65" y="19" rx="3"/><rect width="6" height="6" x="71" y="35" rx="3"/></g><rect width="6" height="6" x="59" y="35" fill="#e52c5a" rx="3"/></g><path fill="#6b4fbb" fill-rule="nonzero" d="M151.869 77.403c-13.26 9.264-31.649 7.977-43.484-3.858-13.279-13.279-13.279-34.806 0-48.084 13.278-13.278 34.805-13.278 48.083 0 11.836 11.836 13.118 30.23 3.858 43.485.133.111.262.229.387.354l15.556 15.555a6.004 6.004 0 0 1 0 8.486 5.997 5.997 0 0 1-8.486 0l-15.555-15.556a6.051 6.051 0 0 1-.355-.387m-1.06-9.512c10.154-10.154 10.154-26.617 0-36.77-10.153-10.154-26.616-10.154-36.77 0-10.153 10.153-10.153 26.616 0 36.77 10.154 10.153 26.617 10.153 36.77 0"/></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/pipelines_failed.svg b/app/assets/images/illustrations/pipelines_failed.svg deleted file mode 100644 index 8daf7da86ed..00000000000 --- a/app/assets/images/illustrations/pipelines_failed.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 446 249" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M260.03 114h23.972v-.013c19.972-.53 36-16.887 36-36.987 0-20.435-16.565-37-37-37-.993 0-1.977.039-2.95.116-4.95-14.605-18.773-25.12-35.05-25.12a36.87 36.87 0 0 0-15.32 3.311c-6.649-9.841-17.909-16.311-30.68-16.311-20.435 0-37 16.565-37 37 0 .701.019 1.397.058 2.088C145.95 45.083 134 59.645 134 76.996c0 20.435 16.565 37 37 37 .324 0 .646-.004.968-.012"/><ellipse id="b" cx="41" cy="41" rx="41" ry="41"/><mask id="c" width="186" height="112" x="0" y="0" fill="#fff"><use xlink:href="#a"/></mask><mask id="d" width="82" height="82" x="0" y="0" fill="#fff"><use xlink:href="#b"/></mask></defs><g fill="none" fill-rule="evenodd"><path stroke="#b5a7dd" stroke-width="4" d="M228.415 137.792c8.443 17.156 21.89 32.082 39.688 42.358"/><path fill="#fb722e" d="M284.464 183.822a2.006 2.006 0 0 1 2.74-.727l6.914 3.992a2.001 2.001 0 0 1 .741 2.737 2.006 2.006 0 0 1-2.74.727l-6.914-3.992a2.001 2.001 0 0 1-.74-2.737m-5 8.66a2.006 2.006 0 0 1 2.74-.726l6.913 3.991a2.001 2.001 0 0 1 .741 2.737 2.006 2.006 0 0 1-2.74.727l-6.914-3.991a2.001 2.001 0 0 1-.74-2.737"/><path fill="#fde5d8" fill-rule="nonzero" d="M267.072 189.947l5.196 3a5.998 5.998 0 0 0 8.195-2.194l3.005-5.205a5.995 5.995 0 0 0-2.198-8.193l-5.196-3-9 15.588m6.032-18.447a3.005 3.005 0 0 1 4.098-1.11l6.07 3.505c4.784 2.761 6.426 8.871 3.662 13.658l-3.005 5.204c-2.76 4.782-8.875 6.42-13.659 3.658l-6.07-3.505a2.999 2.999 0 0 1-1.088-4.104l9.992-17.306"/><g fill-rule="nonzero"><path fill="#e5e5e5" d="M260.597 18.747C266.208 9.657 276.116 4 287 4c17.12 0 31 13.879 31 31 0 7.02-2.34 13.685-6.58 19.1l3.149 2.466A34.855 34.855 0 0 0 322 35.001c0-19.33-15.67-35-35-35-12.286 0-23.476 6.384-29.808 16.647l3.404 2.1"/><path fill="#b5a7dd" d="M281.982 23.991l-2.526 1.154-2.992-2.993a.4.4 0 0 0-.564.009l-1.738 1.738a.392.392 0 0 0-.009.564l2.987 2.987-1.147 2.524a12.26 12.26 0 0 0-1.04 3.883l-.269 2.76-4.08 1.093a.399.399 0 0 0-.275.492l.636 2.375c.06.223.273.346.485.29l4.087-1.096 1.611 2.262a12.017 12.017 0 0 0 2.827 2.828l2.26 1.612-1.094 4.08a.399.399 0 0 0 .29.485l2.374.636a.393.393 0 0 0 .493-.275l1.093-4.08 2.763-.267a12.14 12.14 0 0 0 3.862-1.035l2.526-1.154 2.992 2.992a.4.4 0 0 0 .564-.008l1.738-1.738a.392.392 0 0 0 .009-.564l-2.987-2.987 1.147-2.524a12.26 12.26 0 0 0 1.04-3.883l.27-2.76 4.08-1.093a.399.399 0 0 0 .274-.493l-.636-2.374a.393.393 0 0 0-.485-.29l-4.087 1.096-1.611-2.262a12.017 12.017 0 0 0-2.826-2.828l-2.26-1.612 1.093-4.08a.399.399 0 0 0-.29-.485l-2.373-.636a.393.393 0 0 0-.493.274l-1.094 4.081-2.763.266c-1.336.129-2.64.48-3.862 1.036m3.48-5.02l.375-1.4a4.393 4.393 0 0 1 5.392-3.103l2.375.636a4.399 4.399 0 0 1 3.117 5.383l-.375 1.401a16.077 16.077 0 0 1 3.761 3.767l1.405-.376a4.397 4.397 0 0 1 5.386 3.118l.636 2.375a4.398 4.398 0 0 1-3.103 5.39l-1.402.376a16.217 16.217 0 0 1-1.378 5.143l1.027 1.026a4.392 4.392 0 0 1-.008 6.22l-1.739 1.738a4.4 4.4 0 0 1-6.224.008l-1.028-1.028a16.09 16.09 0 0 1-5.14 1.381l-.376 1.4a4.393 4.393 0 0 1-5.392 3.104l-2.374-.636a4.399 4.399 0 0 1-3.118-5.383l.376-1.401a16.077 16.077 0 0 1-3.762-3.767l-1.404.376a4.397 4.397 0 0 1-5.386-3.118l-.637-2.374a4.398 4.398 0 0 1 3.103-5.391l1.402-.376a16.217 16.217 0 0 1 1.378-5.143l-1.026-1.026a4.392 4.392 0 0 1 .008-6.22l1.738-1.738a4.4 4.4 0 0 1 6.224-.008l1.028 1.028a16.09 16.09 0 0 1 5.141-1.381"/><path fill="#6b4fbb" d="M286.367 37.355a2.439 2.439 0 1 0 1.262-4.711 2.439 2.439 0 0 0-1.262 4.711m-1.035 3.864a6.44 6.44 0 1 1 3.333-12.44 6.44 6.44 0 0 1-3.333 12.44"/></g><use fill="#fff" stroke="#e5e5e5" stroke-width="8" mask="url(#c)" stroke-linejoin="round" xlink:href="#a"/><g transform="translate(175 58)"><use fill="#fff" stroke="#e5e5e5" stroke-width="8" mask="url(#d)" xlink:href="#b"/><g fill-rule="nonzero"><path fill="#e5e5e5" d="M41 78c20.435 0 37-16.565 37-37S61.435 4 41 4 4 20.565 4 41s16.565 37 37 37m0 4C18.356 82 0 63.644 0 41S18.356 0 41 0s41 18.356 41 41-18.356 41-41 41"/><path fill="#b5a7dd" d="M34.363 26.44l-2.527 1.154-3.211-3.211a1.495 1.495 0 0 0-2.117-.005l-2.131 2.13a1.504 1.504 0 0 0 .005 2.117l3.206 3.206-1.147 2.524a16.09 16.09 0 0 0-.897 2.503 16.08 16.08 0 0 0-.475 2.616l-.269 2.76-4.379 1.174a1.495 1.495 0 0 0-1.063 1.83l.78 2.911a1.504 1.504 0 0 0 1.836 1.054l4.387-1.176 1.612 2.263a15.954 15.954 0 0 0 3.737 3.742l2.26 1.612-1.173 4.38a1.495 1.495 0 0 0 1.053 1.835l2.908.78a1.504 1.504 0 0 0 1.83-1.063l1.174-4.38 2.763-.266a15.977 15.977 0 0 0 5.108-1.372l2.527-1.154 3.211 3.212a1.495 1.495 0 0 0 2.117.005l2.131-2.131a1.504 1.504 0 0 0-.005-2.117l-3.206-3.206 1.147-2.524a16.09 16.09 0 0 0 .897-2.503 16.1 16.1 0 0 0 .475-2.616l.269-2.76 4.379-1.173a1.495 1.495 0 0 0 1.063-1.83l-.78-2.912a1.504 1.504 0 0 0-1.836-1.054l-4.387 1.176-1.612-2.262a15.954 15.954 0 0 0-3.737-3.743l-2.26-1.612 1.173-4.38a1.495 1.495 0 0 0-1.053-1.835l-2.908-.779a1.504 1.504 0 0 0-1.83 1.063l-1.174 4.38-2.763.265c-1.767.17-3.493.636-5.108 1.373m4.726-5.355l.455-1.699a5.504 5.504 0 0 1 6.73-3.89l2.907.778a5.495 5.495 0 0 1 3.882 6.735l-.455 1.699a19.95 19.95 0 0 1 4.673 4.68l1.704-.457a5.503 5.503 0 0 1 6.734 3.886l.78 2.91a5.493 5.493 0 0 1-3.894 6.73l-1.701.455a20.134 20.134 0 0 1-.593 3.265 20.134 20.134 0 0 1-1.119 3.124l1.245 1.246a5.507 5.507 0 0 1 .008 7.774l-2.13 2.13a5.5 5.5 0 0 1-7.775-.001l-1.248-1.248c-2 .914-4.157 1.502-6.387 1.717l-.455 1.699a5.504 5.504 0 0 1-6.73 3.89l-2.907-.778a5.495 5.495 0 0 1-3.882-6.735l.455-1.699a19.95 19.95 0 0 1-4.673-4.68l-1.704.457a5.503 5.503 0 0 1-6.734-3.886l-.78-2.91a5.493 5.493 0 0 1 3.894-6.73l1.701-.455a20.258 20.258 0 0 1 1.712-6.389l-1.245-1.246a5.507 5.507 0 0 1-.008-7.774l2.13-2.13a5.5 5.5 0 0 1 7.775.001l1.248 1.248c2-.914 4.157-1.502 6.387-1.717"/><path fill="#6b4fbb" d="M39.965 44.863a4 4 0 1 0 2.07-7.727 4 4 0 0 0-2.07 7.727m-1.036 3.864a8 8 0 1 1 4.142-15.455 8 8 0 0 1-4.142 15.455"/></g></g><path fill="#e5e5e5" fill-rule="nonzero" d="M144 169.541v30.01a4.002 4.002 0 0 0 4 3.995h20c2.209 0 4-1.789 4-3.995v-30.01a4.002 4.002 0 0 0-4-3.995h-20c-2.209 0-4 1.789-4 3.995m-4 0c0-4.416 3.583-7.995 8-7.995h20c4.416 0 8 3.584 8 7.995v30.01c0 4.416-3.583 7.995-8 7.995h-20c-4.416 0-8-3.584-8-7.995v-30.01"/><g fill="#fb722e" transform="translate(140 161)"><rect width="4" height="11" x="10" y="18.545" rx="2"/><rect width="4" height="11" x="21" y="18.545" rx="2"/></g><path fill="#e5e5e5" fill-rule="nonzero" d="M445.16 245.34c-16.874-11.778-110.62-20.336-222.14-20.336-111.61 0-205.4 8.571-222.18 20.364a2 2 0 1 0 2.3 3.272c15.756-11.07 109.46-19.636 219.88-19.636 110.34 0 203.99 8.55 219.85 19.617a2.001 2.001 0 0 0 2.29-3.28"/></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/pipelines_pending.svg b/app/assets/images/illustrations/pipelines_pending.svg deleted file mode 100644 index 25038366e92..00000000000 --- a/app/assets/images/illustrations/pipelines_pending.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="430" height="220" viewBox="0 0 430 220"><g fill="none" fill-rule="evenodd"><path fill="#EEE" fill-rule="nonzero" d="M189.8 182l2.4-12H114c-5.523 0-10-4.477-10-10V34c0-5.523 4.477-10 10-10h200c5.523 0 10 4.477 10 10v126c0 5.523-4.477 10-10 10h-78.2l2.4 12h22.52a9.651 9.651 0 0 1 9.28 7 5.491 5.491 0 0 1-5.28 7H164.159a5.787 5.787 0 0 1-5.659-7 8.855 8.855 0 0 1 8.659-7H189.8zM114 28a6 6 0 0 0-6 6v126a6 6 0 0 0 6 6h200a6 6 0 0 0 6-6V34a6 6 0 0 0-6-6H114zm5 6h190a5 5 0 0 1 5 5v116a5 5 0 0 1-5 5H119a5 5 0 0 1-5-5V39a5 5 0 0 1 5-5zm0 4a1 1 0 0 0-1 1v116a1 1 0 0 0 1 1h190a1 1 0 0 0 1-1V39a1 1 0 0 0-1-1H119zm112.72 132h-35.44l-2.4 12h40.24l-2.4-12zm-64.561 16c-2.29 0-4.268 1.6-4.748 3.838A1.787 1.787 0 0 0 164.16 192h100.56a1.491 1.491 0 0 0 1.435-1.901A5.651 5.651 0 0 0 260.72 186h-93.561z"/><path fill="#FEF0E8" d="M177.965 99H194a2 2 0 1 1 0 4h-16.322c-1.374 6.29-6.976 11-13.678 11-6.702 0-12.304-4.71-13.678-11h-3.365l-7.395 9.249a2 2 0 0 1-3.049.089L128.11 103h-5.844a2 2 0 1 1 0-4H129a2 2 0 0 1 1.487.662l7.423 8.248 6.523-8.159a2 2 0 0 1 1.562-.751h4.04c.513-7.265 6.57-13 13.965-13 7.396 0 13.452 5.735 13.965 13zM164 110c5.523 0 10-4.477 10-10s-4.477-10-10-10-10 4.477-10 10 4.477 10 10 10z"/><path fill="#EFEDF8" d="M273.847 103c-.962 6.23-6.347 11-12.847 11-6.5 0-11.885-4.77-12.847-11H232a2 2 0 0 1 0-4h16.153c.962-6.23 6.347-11 12.847-11 6.5 0 11.885 4.77 12.847 11h3.998l8.404-9.338a2 2 0 0 1 3.048.09L296.692 99H305a2 2 0 0 1 0 4h-9.27a2 2 0 0 1-1.562-.751l-6.523-8.16-7.423 8.249a2 2 0 0 1-1.487.662h-4.888zM261 110a9 9 0 1 0 0-18 9 9 0 0 0 0 18z"/><path fill="#FEE1D3" fill-rule="nonzero" d="M213 119c-10.493 0-19-8.507-19-19s8.507-19 19-19 19 8.507 19 19-8.507 19-19 19zm0-4c8.284 0 15-6.716 15-15 0-8.284-6.716-15-15-15-8.284 0-15 6.716-15 15 0 8.284 6.716 15 15 15z"/><path fill="#FC6D26" d="M211.586 101.828L208.757 99a2 2 0 1 0-2.828 2.828l4.243 4.243c.39.39.902.586 1.414.586.512 0 1.023-.195 1.414-.586L220.071 99a2 2 0 1 0-2.828-2.828l-5.657 5.656z"/><path fill="#FDC4A8" d="M162.95 101.07l-1.768-1.767a1.5 1.5 0 0 0-2.121 2.121l2.828 2.829c.293.293.677.439 1.06.439.385 0 .769-.146 1.062-.44l4.242-4.242a1.5 1.5 0 1 0-2.121-2.121l-3.182 3.182z"/><path fill="#6B4FBB" d="M256.39 104.841A6 6 0 1 0 261 95v6l-4.61 3.841z"/><path fill="#FEF0E8" fill-rule="nonzero" d="M99 99h-5a2 2 0 1 0 0 4h5a2 2 0 1 0 0-4zm-16 0h-5a2 2 0 1 0 0 4h5a2 2 0 1 0 0-4zm-14.384-.078l-3.643-3.425a2 2 0 1 0-2.74 2.914l3.643 3.425a2 2 0 1 0 2.74-2.914zm-11.657-10.96l-3.642-3.425a2 2 0 1 0-2.74 2.914l3.642 3.425a2 2 0 0 0 2.74-2.914zm-11.656-10.96l-3.643-3.425a2 2 0 0 0-2.74 2.914l3.643 3.425a2 2 0 1 0 2.74-2.914zm-14.367-3.885l-3.593 3.477a2 2 0 0 0 2.782 2.875l3.593-3.477a2 2 0 0 0-2.782-2.875zM19.44 84.244l-3.593 3.477a2 2 0 1 0 2.781 2.874l3.593-3.477a2 2 0 0 0-2.781-2.874zM7.94 95.371l-3.593 3.477a2 2 0 1 0 2.782 2.874l3.593-3.477a2 2 0 1 0-2.782-2.874z"/><path fill="#E1DBF1" fill-rule="nonzero" d="M423.611 99.56l-3.598 3.472a2 2 0 0 0 2.777 2.879l3.599-3.472a2 2 0 0 0-2.778-2.878zm-11.514 11.11l-3.598 3.472a2 2 0 0 0 2.777 2.878l3.598-3.471a2 2 0 0 0-2.777-2.879zm-11.514 11.11l-3.599 3.471a2 2 0 1 0 2.778 2.879l3.598-3.472a2 2 0 1 0-2.777-2.879zm-8.799 4.48l-3.642-3.426a2 2 0 0 0-2.74 2.915l3.642 3.425a2 2 0 0 0 2.74-2.915zm-11.656-10.96l-3.643-3.426a2 2 0 1 0-2.74 2.914l3.643 3.426a2 2 0 1 0 2.74-2.915zm-11.657-10.96l-3.643-3.426a2 2 0 1 0-2.74 2.914l3.643 3.425a2 2 0 1 0 2.74-2.914zM353.001 99h-5a2 2 0 1 0 0 4h5a2 2 0 0 0 0-4zm-16 0h-5a2 2 0 1 0 0 4h5a2 2 0 0 0 0-4z"/></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/priority_labels.svg b/app/assets/images/illustrations/priority_labels.svg deleted file mode 100644 index b79c551d3d7..00000000000 --- a/app/assets/images/illustrations/priority_labels.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="116" height="68" viewBox="181 0 116 68"><g fill="none" fill-rule="evenodd" transform="translate(182)"><rect width="78" height="34" x="37" y="34" fill="#FAFAFA" rx="3"/><rect width="78" height="34" x="31" y="28" fill="#FFF" stroke="#EEE" stroke-width="4" stroke-linecap="round" rx="3"/><path fill="#FFF" stroke="#FC6D26" stroke-width="3" d="M34 35.8c-.6 0-1.4 0-1.8.4L29 38.8c-1 .7-1.7.4-2-.7l-.6-4c0-.5-.5-1.2-1-1.5L22 30.2c-1-.6-1-1.5 0-2l3.7-2c.5-.2 1-.8 1.2-1.3l1-4.2c.3-1 1-1.3 2-.5l3 3c.3.3 1 .6 1.6.6l4.2-.3c1 0 1.5.7 1 1.7L38 29c-.3.6-.3 1.4 0 2l1.3 3.8c.4 1 0 1.8-1.2 1.6l-4-.6z" stroke-linecap="round"/><path fill="#FDE5D8" d="M51.6 14.3c-.2-.2-.8-.4-1-.3l-2.8.5c-.7 0-1-.4-.8-1l1-2.8V9.5L46.6 7c-.3-.7 0-1.2.8-1h2.7c.3 0 .8-.2 1-.5l2-2c.6-.5 1-.4 1.3.3l.7 2.8c0 .3.4.8.7 1l2.3 1.2c.7.3.7 1 0 1.3l-2.2 1.7-.6 1-.4 3c-.2.6-.7.8-1.3.4l-2-1.7zM5.4 43.2c-.2-.2-.5-.2-.7-.2l-1.8.3c-.6 0-1-.2-.7-.7l.7-1.8V40l-1-1.7c0-.4 0-.7.6-.7h1.8c.3 0 .6 0 .8-.2L6.5 36c.3-.3.7-.2.8.2l.5 2 .5.5 1.6.8c.3.2.3.7 0 1l-1.6 1c-.2 0-.4.4-.4.7l-.4 2c0 .3-.4.5-.8.2l-1.4-1.2zm5-34C10.2 9 10 9 9.7 9L8 9.3c-.6 0-1-.2-.7-.7L8 6.8V6L7 4.3c0-.4 0-.7.6-.7h1.8c.3 0 .6 0 .8-.2L11.5 2c.3-.3.7-.2.8.2l.5 2 .5.5 1.6.8c.3.2.3.7 0 1l-1.6 1c-.2 0-.4.4-.4.7l-.4 2c0 .3-.4.5-.8.2l-1.4-1.2z"/><rect width="52" height="4" x="43" y="38" fill="#EEE" rx="2"/><rect width="36" height="4" x="43" y="48" fill="#EEE" rx="2"/></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/service_desk_callout.svg b/app/assets/images/illustrations/service_desk_callout.svg deleted file mode 100644 index 2886388279e..00000000000 --- a/app/assets/images/illustrations/service_desk_callout.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#F9F9F9" d="M2.12 42c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3C74.353 61.032 58.425 76 39 76 19.575 76 3.647 61.032 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><rect width="7" height="1" x="59" y="38" fill="#E1DBF2" rx=".5"/><path fill="#6B4FBB" d="M60.5 42a3.5 3.5 0 0 0 0-7v7z"/><rect width="7" height="1" x="12" y="38" fill="#E1DBF2" transform="matrix(-1 0 0 1 31 0)" rx=".5"/><path fill="#6B4FBB" d="M17.5 42a3.5 3.5 0 0 1 0-7v7z"/><path fill="#E1DBF1" fill-rule="nonzero" d="M39 58c10.493 0 19-8.507 19-19s-8.507-19-19-19-19 8.507-19 19 8.507 19 19 19zm0 4c-12.703 0-23-10.297-23-23s10.297-23 23-23 23 10.297 23 23-10.297 23-23 23z"/><path fill="#6B4FBB" d="M35 56a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/><path fill="#E1DBF1" fill-rule="nonzero" d="M26.5 40c0 4.143 3.355 7.5 7.494 7.5h10.012A7.497 7.497 0 0 0 51.5 40c0-4.143-3.355-7.5-7.494-7.5H33.994A7.497 7.497 0 0 0 26.5 40zm-3 0c0-5.799 4.698-10.5 10.494-10.5h10.012C49.802 29.5 54.5 34.2 54.5 40c0 5.799-4.698 10.5-10.494 10.5H33.994C28.198 50.5 23.5 45.8 23.5 40z"/><path fill="#6B4FBB" fill-rule="nonzero" d="M35.255 42.406a1 1 0 1 1 1.872-.703 2.001 2.001 0 0 0 3.76-.038 1 1 0 1 1 1.886.665 4 4 0 0 1-7.518.076zM31.5 40a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm15 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z"/><path fill="#6B4FBB" d="M38 22h2a1 1 0 0 1 0 2h-2a1 1 0 0 1 0-2zm0 3h2a1 1 0 0 1 0 2h-2a1 1 0 0 1 0-2z" style="mix-blend-mode:multiply"/></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/service_desk_empty.svg b/app/assets/images/illustrations/service_desk_empty.svg deleted file mode 100644 index daaaeae6a17..00000000000 --- a/app/assets/images/illustrations/service_desk_empty.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="226" height="178" viewBox="0 0 226 178"><g fill="none" fill-rule="evenodd"><path fill="#EEE" fill-rule="nonzero" d="M109.496 165.895a78.17 78.17 0 0 0 6.158.08 2 2 0 0 0-.11-4c-1.94.053-3.886.028-5.84-.074a2 2 0 0 0-2.1 1.893 1.996 1.996 0 0 0 1.89 2.102zm18.408-1.245a76 76 0 0 0 6-1.4 2 2 0 1 0-1.064-3.856c-1.875.52-3.772.96-5.686 1.327a2.001 2.001 0 0 0 .75 3.93zm17.572-5.636a76.28 76.28 0 0 0 5.486-2.803 2 2 0 1 0-1.962-3.485 72.42 72.42 0 0 1-5.2 2.656 2.003 2.003 0 0 0 1.676 3.635zm44.342-74.897a75.786 75.786 0 0 0-.674-6.127 2.002 2.002 0 0 0-3.956.598c.29 1.92.505 3.857.64 5.805a1.998 1.998 0 0 0 2.133 1.857 2 2 0 0 0 1.858-2.133zm-3.505-18.144a76.141 76.141 0 0 0-2.13-5.78 2.001 2.001 0 0 0-3.695 1.534 72.381 72.381 0 0 1 2.02 5.476 1.999 1.999 0 1 0 3.805-1.229zm-7.754-16.73a77.053 77.053 0 0 0-3.454-5.1 1.998 1.998 0 0 0-2.797-.423 1.998 1.998 0 0 0-.424 2.796 73.06 73.06 0 0 1 3.273 4.835c.58.94 1.814 1.23 2.753.647a2.001 2.001 0 0 0 .646-2.754zm-11.582-14.446a76.37 76.37 0 0 0-4.572-4.128 1.999 1.999 0 1 0-2.559 3.073 72.633 72.633 0 0 1 4.334 3.913 2.001 2.001 0 1 0 2.798-2.86zm-101.422-4.91a77.634 77.634 0 0 0-4.64 4.05 2.001 2.001 0 0 0 2.749 2.906 72.611 72.611 0 0 1 4.4-3.84 2 2 0 1 0-2.509-3.115zM52.7 43.062a75.962 75.962 0 0 0-3.546 5.04 2 2 0 1 0 3.363 2.168 72.314 72.314 0 0 1 3.36-4.777 2 2 0 0 0-3.177-2.432zm-9.373 15.924c-.82 1.882-1.56 3.8-2.226 5.745a2 2 0 1 0 3.787 1.294 72.253 72.253 0 0 1 2.108-5.443 1.998 1.998 0 0 0-1.036-2.63 2.001 2.001 0 0 0-2.633 1.036zm-5.26 17.74a76.33 76.33 0 0 0-.777 6.11 2 2 0 0 0 3.985.347c.17-1.947.415-3.88.737-5.793a2 2 0 0 0-3.945-.664zM74.87 155.55a76.028 76.028 0 0 0 5.437 2.897 2 2 0 1 0 1.737-3.603 71.34 71.34 0 0 1-5.152-2.745 1.998 1.998 0 0 0-2.737.714 2.002 2.002 0 0 0 .715 2.738zm16.97 7.34a76.606 76.606 0 0 0 5.975 1.498 2 2 0 1 0 .816-3.916 72.52 72.52 0 0 1-5.662-1.42 1.999 1.999 0 1 0-1.129 3.837z"/><path fill="#F9F9F9" d="M2.12 130c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3-1.527 19.032-17.455 34-36.88 34-19.425 0-35.353-14.968-36.88-34z"/><path fill="#EEE" fill-rule="nonzero" d="M39 166c-21.54 0-39-17.46-39-39s17.46-39 39-39 39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 92 39 92 4 107.67 4 127s15.67 35 35 35z"/><path fill="#FDC4A8" fill-rule="nonzero" d="M53.925 116.226A1.995 1.995 0 0 0 53 116H25a1.99 1.99 0 0 0-.898.212l14.663 13.406c.39.357.99.348 1.37-.02l13.79-13.372zm1.075 4.53L42.92 132.47a5 5 0 0 1-6.854.1L23 120.624V138a2 2 0 0 0 2 2h28a2 2 0 0 0 2-2v-17.244zM25 112h28a6 6 0 0 1 6 6v20a6 6 0 0 1-6 6H25a6 6 0 0 1-6-6v-20a6 6 0 0 1 6-6z"/><path fill="#F9F9F9" d="M150.12 131c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3-1.527 19.032-17.455 34-36.88 34-19.425 0-35.353-14.968-36.88-34z"/><path fill="#EEE" fill-rule="nonzero" d="M187 167c-21.54 0-39-17.46-39-39s17.46-39 39-39 39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35s-15.67-35-35-35-35 15.67-35 35 15.67 35 35 35z"/><path fill="#E1DBF1" fill-rule="nonzero" d="M180.51 137H199a2 2 0 0 0 2-2v-16a2 2 0 0 0-2-2h-24a2 2 0 0 0-2 2v22.743l7.51-4.743zm1.157 4l-9.6 6.062a2 2 0 0 1-3.067-1.69V119a6 6 0 0 1 6-6h24a6 6 0 0 1 6 6v16a6 6 0 0 1-6 6h-17.333z"/><path fill="#6B4FBB" d="M180 129a2 2 0 1 1-.001-3.999A2 2 0 0 1 180 129zm7 0a2 2 0 1 1-.001-3.999A2 2 0 0 1 187 129zm7 0a2 2 0 1 1-.001-3.999A2 2 0 0 1 194 129z"/><g><path fill="#F9F9F9" d="M76.12 42c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3-1.527 19.032-17.455 34-36.88 34-19.425 0-35.353-14.968-36.88-34z"/><path fill="#EEE" fill-rule="nonzero" d="M113 78c-21.54 0-39-17.46-39-39S91.46 0 113 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S132.33 4 113 4 78 19.67 78 39s15.67 35 35 35z"/><g transform="translate(133 35)"><rect width="7" height="1" y="3" fill="#E1DBF2" rx=".5"/><path fill="#6B4FBB" d="M1.5 7a3.5 3.5 0 1 0 0-7v7z"/></g><g transform="matrix(-1 0 0 1 93 35)"><rect width="7" height="1" y="3" fill="#E1DBF2" rx=".5"/><path fill="#6B4FBB" d="M1.5 7a3.5 3.5 0 1 0 0-7v7z"/></g><path fill="#E1DBF1" fill-rule="nonzero" d="M113 58c10.493 0 19-8.507 19-19s-8.507-19-19-19-19 8.507-19 19 8.507 19 19 19zm0 4c-12.703 0-23-10.297-23-23s10.297-23 23-23 23 10.297 23 23-10.297 23-23 23z"/><path fill="#6B4FBB" d="M109 56a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/><path fill="#E1DBF1" fill-rule="nonzero" d="M97.5 40c0-5.8 4.698-10.5 10.494-10.5h10.012c5.796 0 10.494 4.7 10.494 10.5s-4.698 10.5-10.494 10.5h-10.012C102.198 50.5 97.5 45.8 97.5 40zm3 0c0 4.143 3.355 7.5 7.494 7.5h10.012A7.496 7.496 0 0 0 125.5 40c0-4.143-3.355-7.5-7.494-7.5h-10.012A7.496 7.496 0 0 0 100.5 40z"/><path fill="#6B4FBB" fill-rule="nonzero" d="M109.255 42.406a.998.998 0 0 1 .584-1.287.997.997 0 0 1 1.287.583 2 2 0 0 0 3.76-.038 1 1 0 0 1 1.886.665 4.001 4.001 0 0 1-7.518.076zM105.5 40a1.5 1.5 0 1 1 .001-3.001A1.5 1.5 0 0 1 105.5 40zm15 0a1.5 1.5 0 1 1 .001-3.001A1.5 1.5 0 0 1 120.5 40z"/><path fill="#6B4FBB" d="M112 22h2a1 1 0 0 1 0 2h-2a1 1 0 0 1 0-2zm0 3h2a1 1 0 0 1 0 2h-2a1 1 0 0 1 0-2z" style="mix-blend-mode:multiply"/></g></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/slack_logo.svg b/app/assets/images/illustrations/slack_logo.svg deleted file mode 100644 index b8d7906c2e1..00000000000 --- a/app/assets/images/illustrations/slack_logo.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" viewBox="0 0 121.94154 121.84154" width="121.942" height="121.842"><style id="style200">.st0{fill:#ecb32d}.st1{fill:#63c1a0}.st2{fill:#e01a59}.st3{fill:#331433}.st4{fill:#d62027}.st5{fill:#89d3df}.st6{fill:#258b74}.st7{fill:#819c3c}</style><path class="st0" d="M79.03 7.511c-1.9-5.7-8-8.8-13.7-7-5.7 1.9-8.8 8-7 13.7l28.1 86.4c1.9 5.3 7.7 8.3 13.2 6.7 5.8-1.7 9.3-7.8 7.4-13.4 0-.2-28-86.4-28-86.4z" id="path202" fill="#ecb32d"/><path class="st1" d="M35.53 21.611c-1.9-5.7-8-8.8-13.7-7-5.7 1.9-8.8 8-7 13.7l28.1 86.4c1.9 5.3 7.7 8.3 13.2 6.7 5.8-1.7 9.3-7.8 7.4-13.4 0-.2-28-86.4-28-86.4z" id="path204" fill="#63c1a0"/><path class="st2" d="M114.43 79.011c5.7-1.9 8.8-8 7-13.7-1.9-5.7-8-8.8-13.7-7l-86.5 28.2c-5.3 1.9-8.3 7.7-6.7 13.2 1.7 5.8 7.8 9.3 13.4 7.4.2 0 86.5-28.1 86.5-28.1z" id="path206" fill="#e01a59"/><path class="st3" d="M39.23 103.511c5.6-1.8 12.9-4.2 20.7-6.7-1.8-5.6-4.2-12.9-6.7-20.7l-20.7 6.7z" id="path208" fill="#331433"/><path class="st4" d="M82.83 89.311c7.8-2.5 15.1-4.9 20.7-6.7-1.8-5.6-4.2-12.9-6.7-20.7l-20.7 6.7z" id="path210" fill="#d62027"/><path class="st5" d="M100.23 35.511c5.7-1.9 8.8-8 7-13.7-1.9-5.7-8-8.8-13.7-7l-86.4 28.1c-5.3 1.9-8.3 7.7-6.7 13.2 1.7 5.8 7.8 9.3 13.4 7.4.2 0 86.4-28 86.4-28z" id="path212" fill="#89d3df"/><path class="st6" d="M25.13 59.911c5.6-1.8 12.9-4.2 20.7-6.7-2.5-7.8-4.9-15.1-6.7-20.7l-20.7 6.7z" id="path214" fill="#258b74"/><path class="st7" d="M68.63 45.811c7.8-2.5 15.1-4.9 20.7-6.7-2.5-7.8-4.9-15.1-6.7-20.7l-20.7 6.7z" id="path216" fill="#819c3c"/></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/todos_all_done.svg b/app/assets/images/illustrations/todos_all_done.svg deleted file mode 100644 index 6387497a6fb..00000000000 --- a/app/assets/images/illustrations/todos_all_done.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 293 216"><g fill="none" fill-rule="evenodd"><g transform="rotate(-5 211.388 -693.89)"><rect width="163.6" height="200" x=".2" fill="#FFF" stroke="#EEE" stroke-width="3" stroke-linecap="round" stroke-dasharray="6 9" rx="6"/><g transform="translate(24 38)"><path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/><path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/><rect width="76" height="3" x="40" y="11" fill="#6B4FBB" opacity=".5" rx="1.5"/><rect width="43" height="3" x="40" y="21" fill="#6B4FBB" opacity=".5" rx="1.5"/></g><g transform="translate(24 83)"><path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/><path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/><rect width="76" height="3" x="40" y="11" fill="#B5A7DD" rx="1.5"/><rect width="43" height="3" x="40" y="21" fill="#B5A7DD" rx="1.5"/></g><g transform="translate(24 130)"><path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/><path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/><rect width="76" height="3" x="40" y="11" fill="#B5A7DD" rx="1.5"/><rect width="43" height="3" x="40" y="21" fill="#B5A7DD" rx="1.5"/></g></g><path fill="#FFCE29" d="M30 11l-1.8 4-2-4-4-1.8 4-2 2-4 2 4 4 2M286 60l-2.7 6.3-3-6-6-3 6-3 3-6 2.8 6.2 6.6 2.8M263 97l-2 4-2-4-4-2 4-2 2-4 2 4 4 2M12 85l-2.7 6.3-3-6-6-3 6-3 3-6 2.8 6.2 6.6 2.8"/></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/todos_empty.svg b/app/assets/images/illustrations/todos_empty.svg deleted file mode 100644 index 4de6cb403b9..00000000000 --- a/app/assets/images/illustrations/todos_empty.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 284 337" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="a" width="180" height="220" x="66.2" y="74.4" rx="6"/><mask id="l" width="180" height="220" x="0" y="0" fill="#fff"><use xlink:href="#a"/></mask><rect id="b" width="180" height="220" rx="6"/><mask id="m" width="180" height="220" x="0" y="0" fill="#fff"><use xlink:href="#b"/></mask><rect id="c" width="28" height="28" rx="4"/><mask id="n" width="28" height="28" x="0" y="0" fill="#fff"><use xlink:href="#c"/></mask><rect id="d" width="28" height="28" rx="4"/><mask id="o" width="28" height="28" x="0" y="0" fill="#fff"><use xlink:href="#d"/></mask><circle id="e" cx="21.5" cy="21.5" r="21.5"/><mask id="p" width="43" height="43" x="0" y="0" fill="#fff"><use xlink:href="#e"/></mask><circle id="f" cx="26.5" cy="26.5" r="26.5"/><mask id="q" width="53" height="53" x="0" y="0" fill="#fff"><use xlink:href="#f"/></mask><circle id="g" cx="9.5" cy="4.5" r="4.5"/><mask id="r" width="13" height="13" x="-2" y="-2"><path fill="#fff" d="M3-2h13v13H3z"/><use xlink:href="#g"/></mask><circle id="h" cx="26.5" cy="26.5" r="26.5"/><mask id="s" width="53" height="53" x="0" y="0" fill="#fff"><use xlink:href="#h"/></mask><circle id="i" cx="21.5" cy="21.5" r="21.5"/><mask id="t" width="43" height="43" x="0" y="0" fill="#fff"><use xlink:href="#i"/></mask><path id="j" d="M18 38h15c10.5 0 19-8.5 19-19S43.5 0 33 0H19C8.5 0 0 8.5 0 19c0 6.3 3 12 7.8 15.3l5.2 9c.6 1 1.4 1 2 0l3-5.3z"/><mask id="u" width="52" height="44" x="0" y="0" fill="#fff"><use xlink:href="#j"/></mask><circle id="k" cx="18.5" cy="18.5" r="18.5"/><mask id="v" width="37" height="37" x="0" y="0" fill="#fff"><use xlink:href="#k"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(-6 -4)"><use stroke="#EEE" stroke-width="6" mask="url(#l)" transform="rotate(-5 156.245 184.425)" xlink:href="#a"/><g transform="rotate(5 -707.333 618.042)"><use fill="#FFF" stroke="#EEE" stroke-width="6" mask="url(#m)" xlink:href="#b"/><g transform="translate(29 24)"><path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/><path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/><rect width="86" height="3" x="40" y="11" fill="#6B4FBB" opacity=".5" rx="1.5"/><rect width="43" height="3" x="40" y="21" fill="#6B4FBB" opacity=".5" rx="1.5"/></g><g transform="translate(29 69)"><path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/><path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/><rect width="86" height="3" x="40" y="11" fill="#B5A7DD" rx="1.5"/><rect width="43" height="3" x="40" y="21" fill="#B5A7DD" rx="1.5"/></g><g transform="translate(28 160)"><use stroke="#E5E5E5" stroke-width="6" mask="url(#n)" opacity=".7" xlink:href="#c"/><rect width="26" height="3" x="41" y="7" fill="#ECECEC" rx="1.5"/><rect width="43" height="3" x="41" y="17" fill="#ECECEC" rx="1.5"/></g><g transform="translate(28 116)"><use stroke="#E5E5E5" stroke-width="6" mask="url(#o)" xlink:href="#d"/><rect width="86" height="3" x="41" y="7" fill="#E5E5E5" rx="1.5"/><rect width="43" height="3" x="41" y="17" fill="#E5E5E5" rx="1.5"/></g></g><g transform="rotate(-15 601.917 -782.362)"><use fill="#FFF" stroke="#B5A7DD" stroke-width="6" mask="url(#p)" xlink:href="#e"/><text fill="#6B4FBB" font-family="SourceSansPro-Black, Source Sans Pro" font-size="20" font-weight="700" letter-spacing="-.1"><tspan x="12" y="27">@</tspan></text></g><g transform="rotate(15 -686.59 1035.907)"><use fill="#FFF" stroke="#FDE5D8" stroke-width="6" mask="url(#q)" xlink:href="#f"/><path fill="#FC6D26" d="M26.5 38.2c3.3 0 9.5-2.5 9.5-9.6 0-7-2.4-6.6-9.5-6.6-7 0-9.5-.4-9.5 6.6s6.2 9.6 9.5 9.6z"/><g transform="translate(17 14)"><use fill="#FC6D26" xlink:href="#g"/><use stroke="#FFF" stroke-width="4" mask="url(#r)" xlink:href="#g"/></g></g><g transform="rotate(15 -85.125 65.185)"><use fill="#FFF" stroke="#B5A7DD" stroke-width="6" mask="url(#s)" xlink:href="#h"/><path fill="#6B4FBB" d="M24 18.5c0-1.4 1-2.5 2.5-2.5 1.4 0 2.5 1 2.5 2.5v9c0 1.4-1 2.5-2.5 2.5-1.4 0-2.5-1-2.5-2.5v-9zM26.5 37c1.4 0 2.5-1 2.5-2.5 0-1.4-1-2.5-2.5-2.5-1.4 0-2.5 1-2.5 2.5 0 1.4 1 2.5 2.5 2.5z"/></g><g transform="rotate(-15 716.492 78.873)"><use fill="#FFF" stroke="#FDE5D8" stroke-width="6" mask="url(#t)" xlink:href="#i"/><path fill="#FC6D26" d="M20 23v-3h3v3h-3zm0 3v1.5c0 .8-.7 1.5-1.5 1.5s-1.5-.7-1.5-1.5V26h-2.5c-.8 0-1.5-.7-1.5-1.5s.7-1.5 1.5-1.5H17v-3h-1.5c-.8 0-1.5-.7-1.5-1.5s.7-1.5 1.5-1.5H17v-2.5c0-.8.7-1.5 1.5-1.5s1.5.7 1.5 1.5V17h3v-1.5c0-.8.7-1.5 1.5-1.5s1.5.7 1.5 1.5V17h2.5c.8 0 1.5.7 1.5 1.5s-.7 1.5-1.5 1.5H26v3h1.5c.8 0 1.5.7 1.5 1.5s-.7 1.5-1.5 1.5H26v2.5c0 .8-.7 1.5-1.5 1.5s-1.5-.7-1.5-1.5V26h-3z"/></g><g transform="rotate(-15 129.114 -585.74)"><use stroke="#FDE5D8" stroke-width="6" mask="url(#u)" xlink:href="#j"/><circle cx="16" cy="20" r="2" fill="#FC6D26"/><circle cx="27" cy="20" r="2" fill="#FC6D26"/><circle cx="38" cy="20" r="2" fill="#FC6D26"/></g><g transform="rotate(-15 1254.8 -458.986)"><use stroke="#FDE5D8" stroke-width="6" mask="url(#v)" xlink:href="#k"/><path fill="#FC6D26" d="M10.6 19l2-2c.5-.5.5-1 0-1.5-.3-.4-1-.4-1.3 0l-2.8 2.8c-.2.2-.3.4-.3.7 0 .3 0 .5.3.7l2.8 2.8c.4.4 1 .4 1.4 0 .4-.4.4-1 0-1.4l-2-2zm14.8 0l-2-2c-.5-.5-.5-1 0-1.5.3-.4 1-.4 1.3 0l2.8 2.8c.2.2.3.4.3.7 0 .3 0 .5-.3.7l-2.8 2.8c-.4.4-1 .4-1.4 0-.4-.4-.4-1 0-1.4l2-2z"/><rect width="2" height="7" x="17" y="15.1" fill="#FC6D26" opacity=".5" transform="rotate(15 18.002 18.64)" rx="1"/></g></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/welcome/add_new_group.svg b/app/assets/images/illustrations/welcome/add_new_group.svg deleted file mode 100644 index b10a3ae8812..00000000000 --- a/app/assets/images/illustrations/welcome/add_new_group.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#000" fill-opacity=".03" d="M2.12 42C2.04 43 2 44 2 45c0 20.43 16.57 37 37 37s37-16.57 37-37c0-1-.04-2-.12-3C74.35 61.03 58.42 76 39 76S3.65 61.03 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><path fill="#E1DBF2" fill-rule="nonzero" d="M59.65 32.65H60l-2-2.42-2 2.4-2-2.4-2 2.4-2-2.4-2 2.4-2-2.4-2 2.42h.77C45.57 34.6 46 36.75 46 39c0 2.84-.7 5.5-1.92 7.86 1.97 2.28 4.83 3.64 7.92 3.64 5.8 0 10.5-4.74 10.5-10.6 0-2.8-1.08-5.36-2.85-7.25zM43.18 29.6c2.4-2.1 5.52-3.3 8.82-3.3 7.46 0 13.5 6.1 13.5 13.6S59.46 53.5 52 53.5c-3.68 0-7.1-1.5-9.6-4.04C39.3 53.44 34.44 56 29 56c-9.4 0-17-7.6-17-17s7.6-17 17-17c3.22 0 6.23.9 8.8 2.45 2.13 1.3 3.97 3.05 5.38 5.16zM17 34c-.65 1.54-1 3.23-1 5 0 7.18 5.82 13 13 13s13-5.82 13-13c0-1.77-.35-3.46-1-5h-9c-.53 0-1.04-.2-1.4-.6L29 31.84l-1.6 1.58c-.36.4-.87.6-1.4.6h-9zm21.38-4a12.996 12.996 0 0 0-18.76 0h5.55l2.42-2.4c.74-.8 2-.8 2.8 0l2.4 2.4h5.54z"/><path fill="#6B4FBB" d="M47.6 42.32c-.66 0-1.2-.54-1.2-1.2 0-.68.54-1.22 1.2-1.22.66 0 1.2.54 1.2 1.2 0 .68-.54 1.22-1.2 1.22zm8.8 0c-.66 0-1.2-.54-1.2-1.2 0-.68.54-1.22 1.2-1.22.66 0 1.2.54 1.2 1.2 0 .68-.54 1.22-1.2 1.22zM25 44h8c0 2.2-1.8 4-4 4s-4-1.8-4-4zm-1.5-1c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm11 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5z"/></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/welcome/add_new_project.svg b/app/assets/images/illustrations/welcome/add_new_project.svg deleted file mode 100644 index 4b8dc34c088..00000000000 --- a/app/assets/images/illustrations/welcome/add_new_project.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#F9F9F9" d="M2.12 42c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3C74.353 61.032 58.425 76 39 76S3.647 61.032 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><path fill="#E1DBF2" fill-rule="nonzero" d="M30 24c-2.21 0-4 1.79-4 4v22c0 2.21 1.79 4 4 4h18c2.21 0 4-1.79 4-4V28c0-2.21-1.79-4-4-4H30zm0-4h18a8 8 0 0 1 8 8v22a8 8 0 0 1-8 8H30a8 8 0 0 1-8-8V28a8 8 0 0 1 8-8z"/><path fill="#6B4FBB" d="M33 30h8a2 2 0 1 1 0 4h-8a2 2 0 1 1 0-4zm0 7h12a2 2 0 1 1 0 4H33a2 2 0 1 1 0-4zm0 7h12a2 2 0 1 1 0 4H33a2 2 0 1 1 0-4z"/></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/welcome/add_new_user.svg b/app/assets/images/illustrations/welcome/add_new_user.svg deleted file mode 100644 index d4c184989bf..00000000000 --- a/app/assets/images/illustrations/welcome/add_new_user.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#000" fill-opacity=".03" d="M2.12 42C2.04 43 2 44 2 45c0 20.43 16.57 37 37 37s37-16.57 37-37c0-1-.04-2-.12-3C74.35 61.03 58.42 76 39 76S3.65 61.03 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><path fill="#E1DBF2" d="M44 31l-2.5-3-2.5 3-2.5-3-2.5 3-2.5-3-2.5 3h-2.72c2.65-4.2 7.36-7 12.72-7s10.07 2.8 12.72 7H49l-2.5-3-2.5 3z"/><path fill="#E1DBF2" fill-rule="nonzero" d="M39 57c-9.4 0-17-7.6-17-17s7.6-17 17-17 17 7.6 17 17-7.6 17-17 17zm0-4c7.18 0 13-5.82 13-13s-5.82-13-13-13-13 5.82-13 13 5.82 13 13 13z"/><path fill="#6B4FBB" d="M35 45h8c0 2.2-1.8 4-4 4s-4-1.8-4-4zm-1.5-2c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm11 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5z"/></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/welcome/configure_server.svg b/app/assets/images/illustrations/welcome/configure_server.svg deleted file mode 100644 index f9dda816f11..00000000000 --- a/app/assets/images/illustrations/welcome/configure_server.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#000" fill-opacity=".03" d="M2.12 42C2.04 43 2 44 2 45c0 20.43 16.57 37 37 37s37-16.57 37-37c0-1-.04-2-.12-3C74.35 61.03 58.42 76 39 76S3.65 61.03 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><path fill="#FEE1D3" fill-rule="nonzero" d="M24.92 35.15a4.012 4.012 0 0 1-.6-5.63l1.26-1.55c1.4-1.72 3.9-2 5.63-.6l.7.56c.7-.4 1.4-.73 2.1-1V26c0-2.2 1.8-4 4-4h2c2.2 0 4 1.8 4 4v.92c.8.28 1.5.62 2.1 1l.7-.55c1.7-1.4 4.3-1.12 5.7.6l1.3 1.55c1.4 1.72 1.2 4.23-.6 5.63l-.7.6c.3.74.4 1.5.5 2.3l.9.2c2.2.5 3.5 2.64 3 4.8L56.4 45c-.5 2.15-2.64 3.5-4.8 3l-.88-.2c-.44.63-.92 1.24-1.46 1.8l.4.82c.9 1.98.1 4.38-1.9 5.35l-1.8.87c-2 .97-4.37.15-5.34-1.84l-.46-.85c-.34.03-.74.05-1.13.05-.4 0-.8-.02-1.2-.05l-.4.85c-.95 2-3.34 2.8-5.33 1.84l-1.8-.87a4.011 4.011 0 0 1-1.83-5.35l.4-.8c-.54-.58-1.02-1.2-1.46-1.83l-.8.2c-2.2.5-4.3-.9-4.8-3l-.4-2c-.5-2.2.85-4.3 3-4.8l.9-.2c.1-.8.3-1.6.5-2.3l-.7-.6zm4.95.77c-.53 1.2-.83 2.47-.87 3.8-.02.9-.66 1.68-1.55 1.9l-2.32.53.45 1.94 2.3-.6c.9-.2 1.8.2 2.23 1 .7 1.1 1.5 2.2 2.5 3 .7.6.9 1.6.5 2.4l-1 2.1 1.8.9 1.1-2.1c.4-.8 1.3-1.3 2.2-1.1.7.1 1.3.2 2 .2s1.3-.1 2-.2c.9-.2 1.8.3 2.2 1.1l1 2.1 1.8-.9-1.2-2c-.4-.8-.2-1.8.5-2.4 1-.85 1.84-1.88 2.45-3.05.4-.82 1.33-1.24 2.2-1.04l2.33.54.45-1.95-2.32-.54c-.9-.2-1.52-.97-1.54-1.88-.03-1.4-.33-2.6-.86-3.8-.4-.9-.2-1.8.5-2.4l1.9-1.5-1.3-1.6-1.8 1.5c-.8.5-1.8.6-2.5 0-1.1-.8-2.3-1.4-3.5-1.7-.9-.2-1.5-1-1.5-1.9V26h-2v2.38c0 .9-.6 1.7-1.5 1.93-1.3.4-2.5 1-3.5 1.7-.8.6-1.8.6-2.5 0l-1.9-1.5-1.26 1.6 1.8 1.5c.7.6.94 1.6.6 2.4z"/><path fill="#FC6D26" fill-rule="nonzero" d="M39 46c-3.3 0-6-2.7-6-6s2.7-6 6-6 6 2.7 6 6-2.7 6-6 6zm0-4c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z"/></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/welcome/ee_trial.svg b/app/assets/images/illustrations/welcome/ee_trial.svg deleted file mode 100644 index 6d0dcf0020c..00000000000 --- a/app/assets/images/illustrations/welcome/ee_trial.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="330" height="132" viewBox="0 0 330 132"><g fill="none" fill-rule="evenodd"><path fill="#000" fill-opacity=".03" d="M174.12 42c-.08 1-.12 2-.12 3 0 20.43 16.57 37 37 37s37-16.57 37-37c0-1-.04-2-.12-3-1.53 19.03-17.46 34-36.88 34s-35.35-14.97-36.88-34z"/><path fill="#EEE" fill-rule="nonzero" d="M211 78c-21.54 0-39-17.46-39-39s17.46-39 39-39 39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S230.33 4 211 4s-35 15.67-35 35 15.67 35 35 35z"/><g fill-rule="nonzero"><path fill="#FEE1D3" d="M211.5 51c-6.42 0-12.26-2.84-17.43-8.4a4.008 4.008 0 0 1-.27-5.13C199 30.57 204.92 27 211.5 27s12.5 3.56 17.7 10.47a3.994 3.994 0 0 1-.27 5.12c-5.17 5.53-11 8.4-17.43 8.4zm0-4c5.25 0 10.05-2.34 14.5-7.13-4.5-5.98-9.3-8.87-14.5-8.87-5.2 0-10 2.9-14.5 8.87 4.45 4.8 9.25 7.13 14.5 7.13z"/><path fill="#FC6D26" d="M211 47c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm0-4c2.2 0 4-1.8 4-4s-1.8-4-4-4-4 1.8-4 4 1.8 4 4 4zm0-1c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3z"/></g><path fill="#000" fill-opacity=".03" d="M88.12 83c-.08 1-.12 2-.12 3 0 20.43 16.57 37 37 37s37-16.57 37-37c0-1-.04-2-.12-3-1.53 19.03-17.46 34-36.88 34s-35.35-14.97-36.88-34z"/><path fill="#EEE" fill-rule="nonzero" d="M125 119c-21.54 0-39-17.46-39-39s17.46-39 39-39 39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35s-15.67-35-35-35-35 15.67-35 35 15.67 35 35 35z"/><path fill="#FEE1D3" fill-rule="nonzero" d="M116 86.34c2.33.83 4 3.05 4 5.66 0 3.3-2.7 6-6 6s-6-2.7-6-6c0-2.6 1.67-4.83 4-5.66V72h4v14.34zM128 66c5.52 0 10 4.48 10 10v12h-4V76c0-3.3-2.7-6-6-6v1.83c0 .55-.45 1-1 1-.24 0-.47-.1-.65-.24l-4.46-3.87c-.46-.36-.5-1-.15-1.4.03-.05.07-.1.1-.12l4.47-3.82c.42-.35 1.05-.3 1.4.1.16.2.25.43.25.66V66zm-14 28c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z"/><path fill="#FC6D26" fill-rule="nonzero" d="M114 74c-3.3 0-6-2.7-6-6s2.7-6 6-6 6 2.7 6 6-2.7 6-6 6zm0-4c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm22 28c-3.3 0-6-2.7-6-6s2.7-6 6-6 6 2.7 6 6-2.7 6-6 6zm0-4c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z"/><path fill="#000" fill-opacity=".03" d="M2.12 52C2.04 53 2 54 2 55c0 20.43 16.57 37 37 37s37-16.57 37-37c0-1-.04-2-.12-3C74.35 71.03 58.42 86 39 86S3.65 71.03 2.12 52z"/><path fill="#EEE" fill-rule="nonzero" d="M39 88C17.46 88 0 70.54 0 49s17.46-39 39-39 39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 14 39 14 4 29.67 4 49s15.67 35 35 35z"/><path fill="#6B4FBB" fill-rule="nonzero" d="M48 41h-4c0-2.76-2.24-5-5-5s-5 2.24-5 5h-4a9 9 0 0 1 18 0zm-18 0h4v3h-4v-3zm14 0h4v3h-4v-3z"/><path fill="#E1DBF2" fill-rule="nonzero" d="M30 47c-.55 0-1 .45-1 1v12c0 .55.45 1 1 1h18c.55 0 1-.45 1-1V48c0-.55-.45-1-1-1H30zm0-4h18c2.76 0 5 2.24 5 5v12c0 2.76-2.24 5-5 5H30c-2.76 0-5-2.24-5-5V48c0-2.76 2.24-5 5-5z"/><path fill="#6B4FBB" d="M38 53.73c-.6-.34-1-1-1-1.73 0-1.1.9-2 2-2s2 .9 2 2c0 .74-.4 1.4-1 1.73V55c0 .55-.45 1-1 1s-1-.45-1-1v-1.27z"/><path fill="#000" fill-opacity=".03" d="M254.12 92c-.08 1-.12 2-.12 3 0 20.43 16.57 37 37 37s37-16.57 37-37c0-1-.04-2-.12-3-1.53 19.03-17.46 34-36.88 34s-35.35-14.97-36.88-34z"/><path fill="#EEE" fill-rule="nonzero" d="M291 128c-21.54 0-39-17.46-39-39s17.46-39 39-39 39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35s-15.67-35-35-35-35 15.67-35 35 15.67 35 35 35z"/><path fill="#6B4BBE" fill-rule="nonzero" d="M292 78c5.52 0 10 4.48 10 10 0 2.28-.76 4.43-2.14 6.18-1.03 1.3-.8 3.2.5 4.22 1.3 1.02 3.2.8 4.2-.5 2.22-2.8 3.44-6.26 3.44-9.9 0-8.84-7.16-16-16-16v-3.13c0-.2-.06-.4-.17-.56-.3-.42-.93-.54-1.38-.23l-9.2 6.13c-.1.06-.2.16-.28.27-.3.45-.18 1.08.28 1.38l9.2 6.13c.16.1.35.17.55.17.55 0 1-.45 1-1V78z"/><path fill="#E1DBF2" fill-rule="nonzero" d="M290 100c-5.52 0-10-4.48-10-10 0-2.25.74-4.38 2.1-6.12 1-1.3.77-3.2-.54-4.2-1.3-1.02-3.2-.78-4.2.53A15.796 15.796 0 0 0 274 90c0 8.84 7.16 16 16 16v3.13c0 .55.45 1 1 1 .2 0 .4-.06.55-.17l9.2-6.13c.46-.3.6-.93.28-1.38-.07-.1-.17-.2-.28-.28l-9.2-6.13c-.45-.3-1.08-.2-1.38.27-.1.2-.17.4-.17.6v3.1z"/></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/welcome/globe.svg b/app/assets/images/illustrations/welcome/globe.svg deleted file mode 100644 index c2daae5f317..00000000000 --- a/app/assets/images/illustrations/welcome/globe.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#F9F9F9" d="M2.12 42c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3C74.353 61.032 58.425 76 39 76 19.575 76 3.647 61.032 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><path fill="#E1DBF2" d="M30.24 27.823A14.98 14.98 0 0 0 24 40c0 2.549.636 4.949 1.757 7.051-.297-2.684.644-4.026 2.823-4.026 3.707 0 2.462 5.365 4.473 5.761 2.01.396 4.175.396 4.267 3.29.04 1.257-.265 2.157-.917 2.7a15.095 15.095 0 0 0 8.555-1.006c.035-1.91.303-4.941 2.21-5.61 2.373-.833-.55-1.431.734-3.368 1.17-1.762-3.297-5.2 0-4.832 3.477.388 5.044-.816 6.024-1.456a14.903 14.903 0 0 0-1.373-4.94c-.873.4-2.19.465-3.702-.538-.757-.502-1.084-3.944-2.107-3.944-3.823 0-4.065 3.17-5.994 3.944-1.076.431-4.193 3.773-5.614 3.596-1.126-.14-1.071-4.417-2.45-5.166-1.359-.738-2.174-1.948-2.447-3.633zM39 59c-10.493 0-19-8.507-19-19s8.507-19 19-19 19 8.507 19 19-8.507 19-19 19z"/></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/welcome/lightbulb.svg b/app/assets/images/illustrations/welcome/lightbulb.svg deleted file mode 100644 index fce10312085..00000000000 --- a/app/assets/images/illustrations/welcome/lightbulb.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#F9F9F9" d="M2.12 42c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3C74.353 61.032 58.425 76 39 76S3.647 61.032 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><path fill="#6B4FBB" d="M33 52h12a2 2 0 1 1 0 4H33a2 2 0 1 1 0-4zm1 5h10a2 2 0 1 1 0 4H34a2 2 0 1 1 0-4z"/><path fill="#E1DBF2" fill-rule="nonzero" d="M45.542 46.932l.346-2.36a8.004 8.004 0 0 1 1.566-3.705c3.025-3.946 4.485-7.29 4.547-9.96C52.153 24.41 46.843 20 39 20c-7.777 0-13 4.374-13 11 0 2.4 1.462 5.73 4.573 9.846a8.009 8.009 0 0 1 1.536 3.683l.353 2.456 13.08-.054zm-17.038.624L28.15 45.1a3.997 3.997 0 0 0-.768-1.842C23.794 38.51 22 34.424 22 31c0-9.39 7.61-15 17-15s17.218 5.614 17 15c-.085 3.64-1.875 7.74-5.37 12.3a3.99 3.99 0 0 0-.784 1.853l-.346 2.36a4.003 4.003 0 0 1-3.942 3.42l-13.08.053a4 4 0 0 1-3.974-3.43z"/><path fill="#6B4FBB" d="M41 38.732a2 2 0 1 1 2 0V42a1 1 0 0 1-2 0v-3.268zm-6 0a2 2 0 1 1 2 0V42a1 1 0 0 1-2 0v-3.268z"/></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/wiki-fro-logged-out-users.svg b/app/assets/images/illustrations/wiki-fro-logged-out-users.svg deleted file mode 100644 index c71841f72e5..00000000000 --- a/app/assets/images/illustrations/wiki-fro-logged-out-users.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="412" height="260" viewBox="0 0 412 260" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M6.447.894L12 12H0L5.553.894a.5.5 0 0 1 .894 0z"/></defs><g fill="none" fill-rule="evenodd"><path fill="#FEF0E8" fill-rule="nonzero" d="M338 50.287C322.695 41.45 303.124 46.694 294.287 62c-8.836 15.305-3.592 34.876 11.713 43.712 15.306 8.837 34.877 3.593 43.713-11.712 8.837-15.306 3.593-34.877-11.713-43.713zm2-3.464C357.22 56.763 363.118 78.78 353.177 96c-9.941 17.218-31.958 23.118-49.177 13.176-17.218-9.94-23.118-31.958-13.177-49.176C300.764 42.78 322.782 36.88 340 46.823z"/><g transform="rotate(-150 171.003 8.53)"><path fill="#FC6D26" fill-rule="nonzero" d="M4 16v25a2 2 0 1 0 4 0V16H4zm8-4v29a6 6 0 1 1-12 0V12h12z"/><use fill="#D8D8D8" xlink:href="#a"/><path stroke="#FDC4A8" stroke-width="4" d="M6 4.472L3.236 10h5.528L6 4.472z"/><path fill="#FC6D26" d="M9 6L6.447.894a.5.5 0 0 0-.894 0L3 6c.836.628 1.874 1 3 1a4.978 4.978 0 0 0 3-1z"/></g><path fill="#F9F9F9" d="M263.116 237.116A10.002 10.002 0 0 1 254 243h-86c-11.046 0-20-8.954-20-20V121c0-4.056 2.414-7.547 5.884-9.116A9.964 9.964 0 0 0 153 116v106c0 8.837 7.163 16 16 16h90c1.467 0 2.86-.316 4.116-.884z"/><path fill="#EEE" fill-rule="nonzero" d="M214.5 106H163c-5.523 0-10 4.477-10 10v106c0 8.837 7.163 16 16 16h90c5.523 0 10-4.477 10-10v-17.999a10.036 10.036 0 0 1-4 3.167V228a6 6 0 0 1-6 6h-90c-6.627 0-12-5.373-12-12V116a6 6 0 0 1 6-6h7v-4h44.5z"/><path fill="#EEE" fill-rule="nonzero" d="M260 218.268V214h-90a6 6 0 0 0 0 12h86a4 4 0 0 0 4-4v-.268a1.99 1.99 0 0 1-1 .268h-50a2 2 0 0 1 0-4h50c.364 0 .706.097 1 .268zM170 210h90.5a3.5 3.5 0 0 1 3.5 3.5v8.5a8 8 0 0 1-8 8h-86c-5.523 0-10-4.477-10-10s4.477-10 10-10z"/><path fill="#EEE" fill-rule="nonzero" d="M174 110v100h87a6 6 0 0 0 6-6v-88a6 6 0 0 0-6-6h-87zm-4-4h91c5.523 0 10 4.477 10 10v88c0 5.523-4.477 10-10 10h-91V106z"/><path fill="#EFEDF8" d="M230 99h18a6 6 0 0 1 6 6v31.35a3 3 0 0 1-4.68 2.484l-9.277-6.274a1.5 1.5 0 0 0-1.664-.01l-9.731 6.395a3 3 0 0 1-4.648-2.507V105a6 6 0 0 1 6-6z"/><path fill="#C3B8E3" fill-rule="nonzero" d="M236.182 129.207a5.5 5.5 0 0 1 6.102.04l7.716 5.219V105a2 2 0 0 0-2-2h-18a2 2 0 0 0-2 2v29.584l8.182-5.377zM230 99h18a6 6 0 0 1 6 6v31.35a3 3 0 0 1-4.68 2.484l-9.277-6.274a1.5 1.5 0 0 0-1.664-.01l-9.731 6.395a3 3 0 0 1-4.648-2.507V105a6 6 0 0 1 6-6z"/><g fill-rule="nonzero"><path fill="#EFEDF8" d="M156 74c14.912 0 27-12.088 27-27s-12.088-27-27-27-27 12.088-27 27 12.088 27 27 27zm0 4c-17.12 0-31-13.88-31-31s13.88-31 31-31 31 13.88 31 31-13.88 31-31 31z"/><path fill="#6B4FBB" d="M147.535 44.916l-.116 1.086a8.446 8.446 0 0 0 .093 2.44l.2 1.08-2.262 1.202a.495.495 0 0 0-.213.678l.941 1.77c.128.239.434.332.68.201l2.25-1.196.785.775a8.544 8.544 0 0 0 1.967 1.45l.975.522-.486 2.5a.495.495 0 0 0 .392.59l1.968.383a.504.504 0 0 0 .585-.401l.489-2.515 1.086-.13a8.584 8.584 0 0 0 2.363-.633l1.005-.43 1.68 1.933a.495.495 0 0 0 .708.055l1.513-1.315a.504.504 0 0 0 .044-.708l-1.67-1.922.583-.94c.431-.696.761-1.45.978-2.239l.292-1.063 2.547-.089a.495.495 0 0 0 .488-.515l-.07-2.003a.504.504 0 0 0-.523-.48l-2.56.09-.367-1.037a8.446 8.446 0 0 0-1.139-2.159l-.644-.882 1.509-2.076a.495.495 0 0 0-.106-.702l-1.621-1.178a.504.504 0 0 0-.7.116l-1.494 2.057-1.05-.362a8.459 8.459 0 0 0-2.398-.455l-1.1-.047-.66-2.466a.495.495 0 0 0-.613-.36l-1.936.519a.504.504 0 0 0-.35.617l.661 2.466-.93.59a8.459 8.459 0 0 0-1.848 1.594l-.728.838-2.322-1.034a.495.495 0 0 0-.665.25l-.815 1.83a.504.504 0 0 0 .26.661l2.344 1.044zm-3.565 1.697a3.504 3.504 0 0 1-1.78-4.622l.815-1.83a3.495 3.495 0 0 1 4.626-1.77l.346.154c.259-.245.529-.477.81-.697l-.106-.394a3.504 3.504 0 0 1 2.471-4.292l1.936-.519a3.495 3.495 0 0 1 4.286 2.481l.106.395c.353.05.703.116 1.05.198l.222-.306a3.504 3.504 0 0 1 4.89-.78l1.622 1.178a3.495 3.495 0 0 1 .769 4.892l-.258.355c.184.312.354.633.508.962l.42-.014a3.504 3.504 0 0 1 3.625 3.373l.07 2.003a3.495 3.495 0 0 1-3.382 3.618l-.4.014c-.127.332-.27.659-.426.978l.256.294a3.504 3.504 0 0 1-.34 4.941l-1.512 1.315a3.495 3.495 0 0 1-4.94-.351l-.283-.325a11.669 11.669 0 0 1-1.05.28l-.082.424a3.504 3.504 0 0 1-4.103 2.774l-1.967-.382a3.495 3.495 0 0 1-2.765-4.11l.075-.383a11.547 11.547 0 0 1-.858-.633l-.354.188a3.504 3.504 0 0 1-4.738-1.442l-.94-1.77a3.495 3.495 0 0 1 1.453-4.734l.37-.197a11.436 11.436 0 0 1-.041-1.088l-.4-.178zm13.326 5.608a5.5 5.5 0 1 1-2.847-10.625 5.5 5.5 0 0 1 2.847 10.625zm-.776-2.898a2.5 2.5 0 1 0-1.294-4.83 2.5 2.5 0 0 0 1.294 4.83z"/></g><g fill-rule="nonzero"><path fill="#EFEDF8" d="M326.979 222.047c14.403 3.86 29.209-4.688 33.068-19.092 3.86-14.403-4.688-29.209-19.092-33.068-14.403-3.86-29.209 4.688-33.068 19.092-3.86 14.404 4.688 29.209 19.092 33.068zm-1.035 3.864c-16.538-4.431-26.352-21.43-21.92-37.967 4.43-16.538 21.429-26.352 37.966-21.92 16.538 4.43 26.352 21.429 21.92 37.966-4.43 16.538-21.429 26.352-37.966 21.92z"/><path fill="#6B4FBB" d="M329.376 201.598c-4.668-2.621-7.155-8.157-5.706-13.566 1.715-6.402 8.295-10.201 14.697-8.486 6.402 1.716 10.2 8.296 8.485 14.697-1.45 5.41-6.371 8.96-11.725 8.897a3.03 3.03 0 0 1-.074.365l-1.812 6.761a3 3 0 0 1-5.795-1.552l1.812-6.762a3.03 3.03 0 0 1 .118-.354zm3.815-2.733a8 8 0 1 0 4.14-15.455 8 8 0 0 0-4.14 15.455z"/></g><path fill="#FEF0E8" fill-rule="nonzero" d="M91.373 193c17.071-4.574 27.202-22.12 22.628-39.191-4.575-17.071-22.121-27.202-39.192-22.628-17.071 4.574-27.202 22.121-22.628 39.192 4.574 17.071 22.121 27.202 39.192 22.627zm1.035 3.864c-19.204 5.146-38.945-6.25-44.09-25.456-5.146-19.204 6.25-38.945 25.455-44.09 19.205-5.146 38.945 6.25 44.091 25.455 5.146 19.205-6.25 38.945-25.456 44.091z"/><path fill="#FDC4A8" fill-rule="nonzero" d="M70.067 152.122l6.73 25.114 19.318-5.176-6.73-25.114-19.318 5.176zm-1.035-3.864l19.318-5.176a4 4 0 0 1 4.9 2.828l6.729 25.114a4 4 0 0 1-2.829 4.9L77.832 181.1a4 4 0 0 1-4.9-2.829l-6.729-25.114a4 4 0 0 1 2.829-4.899z"/><path fill="#FC6D26" d="M76.898 154.433l7.727-2.07a2 2 0 0 1 1.036 3.863l-7.728 2.07a2 2 0 1 1-1.035-3.863zm1.812 6.761l5.795-1.553a2 2 0 0 1 1.035 3.864l-5.795 1.553a2 2 0 1 1-1.035-3.864zm1.811 6.762l7.728-2.07a2 2 0 0 1 1.035 3.863l-7.727 2.07a2 2 0 1 1-1.036-3.863z"/></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/wiki_login_empty.svg b/app/assets/images/illustrations/wiki_login_empty.svg deleted file mode 100644 index 1cfa47220a5..00000000000 --- a/app/assets/images/illustrations/wiki_login_empty.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="386" height="298" viewBox="0 0 386 298" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M4 51h16v15.997A5.003 5.003 0 0 1 15.003 72H8.997A5.005 5.005 0 0 1 4 66.997V51z"/><rect id="b" width="24" height="10" y="44" rx="3"/></defs><g fill="none" fill-rule="evenodd" transform="translate(0 3)"><g transform="rotate(15 23.151 968.24)"><rect width="53" height="44" fill="#FFF" stroke="#FDE5D8" stroke-width="3" stroke-linecap="round" rx="5"/><path fill="#FDE5D8" d="M29.5 28.3l2.758-3.861c.962-1.347 2.527-1.34 3.484 0l6.516 9.122c.962 1.347.399 2.439-1.252 2.439H17.994c-1.653 0-2.21-1.099-1.252-2.439l6.516-9.122c.962-1.347 2.527-1.34 3.484 0L29.5 28.3z" opacity=".6"/><circle cx="16" cy="16" r="6" fill="#FDB997"/></g><g transform="scale(-1 1) rotate(25 -75.08 -334.15)"><rect width="3" height="11" x="12.45" y="23.45" fill="#6B4FBB" transform="rotate(45 13.95 28.95)" rx="1.5"/><rect width="3" height="14" x="9.45" y="15.45" fill="#6B4FBB" transform="rotate(45 10.95 22.45)" rx="1.5"/><path fill="#FFF" stroke="#E1DCF1" stroke-width="3" d="M16 39.6C6.871 37.747 0 29.676 0 20 0 8.954 8.954 0 20 0s20 8.954 20 20c0 8.955-5.886 16.536-14 19.084v15.91A5.007 5.007 0 0 1 21 60c-2.761 0-5-2.244-5-5.006V39.6zm4-7.6c6.627 0 12-5.373 12-12S26.627 8 20 8 8 13.373 8 20s5.373 12 12 12z"/></g><g transform="scale(1 -1) rotate(-15 -383.616 -172.407)"><path stroke="#FDE5D8" stroke-width="3" d="M1.5 38.5h9V4c0-1.378-1.12-2.5-2.496-2.5H3.996A2.503 2.503 0 0 0 1.5 4v34.5z"/><rect width="2" height="27" x="5" y="7" fill="#FDA77D" opacity=".8" rx="1"/><path stroke="#FDE5D8" stroke-width="3" d="M2.427 41.553h7.146L6 48.699l-3.573-7.146z"/></g><g transform="rotate(-30 420.145 -545.422)"><path fill="#FFF" stroke="#FDE5D8" stroke-width="3" d="M9 3c0-1.657 1.347-3 3-3 1.657 0 3 1.352 3 3v43H9V3z"/><use fill="#FFF" xlink:href="#a"/><path stroke="#FDE5D8" stroke-width="3" d="M5.5 52.5v14.497A3.505 3.505 0 0 0 8.997 70.5h6.006a3.503 3.503 0 0 0 3.497-3.503V52.5h-13z"/><rect width="2" height="14" x="9" y="51" fill="#FDA77D" rx="1"/><rect width="2" height="14" x="13" y="51" fill="#FDA77D" rx="1"/><use fill="#FFF" xlink:href="#b"/><rect width="21" height="7" x="1.5" y="45.5" stroke="#FDE5D8" stroke-width="3" rx="3"/></g><g transform="translate(72 97.488)"><rect width="125" height="160" fill="#FFF" stroke="#E5E5E5" stroke-width="4" stroke-linecap="round" rx="10"/><rect width="125" height="160" x="125" fill="#FFF" stroke="#E5E5E5" stroke-width="4" stroke-linecap="round" rx="10"/><path fill="#FFF" stroke="#E5E5E5" stroke-width="4" d="M7 12.008C7 8.69 9.686 6 12.993 6H125v148H12.993C9.683 154 7 151.305 7 147.992V12.008zm236 0C243 8.69 240.314 6 237.007 6H125v148h112.007c3.31 0 5.993-2.695 5.993-6.008V12.008z" stroke-linecap="round"/><rect width="84" height="42" x="142" y="29" stroke="#EEE" stroke-width="4" rx="3"/><rect width="88" height="4" x="141" y="93" fill="#E5E5E5" rx="2"/><rect width="88" height="4" x="141" y="107" fill="#BFBFBF" rx="2"/><rect width="56" height="4" x="141" y="121" fill="#E5E5E5" rx="2"/><rect width="56" height="4" x="22" y="93" fill="#E5E5E5" rx="2"/><rect width="26" height="4" x="22" y="27" fill="#BFBFBF" rx="2"/><rect width="56" height="4" x="22" y="41" fill="#E5E5E5" rx="2"/><rect width="36" height="4" x="22" y="55" fill="#BFBFBF" rx="2"/><rect width="56" height="4" x="22" y="69" fill="#E5E5E5" rx="2"/><rect width="36" height="4" x="22" y="107" fill="#E5E5E5" rx="2"/><rect width="56" height="4" x="22" y="121" fill="#BFBFBF" rx="2"/></g><path stroke="#B5A7DD" stroke-width="2.5" d="M23.139 182.922l-1.347-.6a2.004 2.004 0 0 1-1.02-2.64l.815-1.831a1.995 1.995 0 0 1 2.645-1.01l1.308.583a9.959 9.959 0 0 1 2.177-1.876l-.376-1.402a2.004 2.004 0 0 1 1.41-2.455l1.937-.519a1.995 1.995 0 0 1 2.449 1.421l.375 1.402a9.959 9.959 0 0 1 2.824.536l.84-1.158a2.004 2.004 0 0 1 2.796-.448l1.622 1.178a1.995 1.995 0 0 1 .437 2.797l-.867 1.193a9.946 9.946 0 0 1 1.341 2.541l1.461-.05a2.004 2.004 0 0 1 2.075 1.926l.07 2.003a1.995 1.995 0 0 1-1.935 2.067l-1.445.05c-.256.93-.644 1.817-1.15 2.632l.944 1.087a2.004 2.004 0 0 1-.191 2.825l-1.513 1.315a1.995 1.995 0 0 1-2.824-.204l-.963-1.108a10.084 10.084 0 0 1-2.776.744l-.28 1.441a2.004 2.004 0 0 1-2.344 1.588l-1.967-.382a1.995 1.995 0 0 1-1.579-2.35l.275-1.414a10.044 10.044 0 0 1-2.312-1.704l-1.277.678a2.004 2.004 0 0 1-2.709-.822l-.94-1.77a1.995 1.995 0 0 1 .833-2.705l1.29-.687a9.946 9.946 0 0 1-.11-2.872zm10.98 4.93a4 4 0 1 0-2.07-7.727 4 4 0 0 0 2.07 7.728z"/><ellipse cx="197" cy="289.988" fill="#F9F9F9" rx="125" ry="4.5"/><path fill="#6B4FBB" d="M164 100.492a3.002 3.002 0 0 1 3.001-3.004H183a3.006 3.006 0 0 1 3.001 3.004v34.988c0 2.213-1.45 2.954-3.24 1.651l-7.76-5.643-7.76 5.643c-1.789 1.302-3.24.566-3.24-1.651v-34.988z"/><g opacity=".2"><path fill="#FC8A51" d="M5.747 234.768l-2.688 1.114c-1.017.422-1.803-.134-1.754-1.228l.128-2.907-1.115-2.688c-.422-1.017.135-1.803 1.229-1.754l2.907.128 2.687-1.115c1.018-.422 1.803.135 1.755 1.229l-.128 2.907 1.114 2.687c.422 1.018-.134 1.803-1.228 1.755l-2.907-.128zM191.564 37.953l-3.72.164c-1.326.059-1.992-.88-1.48-2.115l1.426-3.438-.164-3.72c-.059-1.326.88-1.992 2.115-1.48l3.438 1.426 3.72-.164c1.326-.059 1.992.88 1.48 2.114l-1.426 3.44.164 3.719c.059 1.326-.88 1.992-2.114 1.48l-3.44-1.426z"/><path fill="#6B4FBB" d="M348.789 75.876l-1.967-2.144c-.744-.812-.49-1.74.555-2.07l2.775-.873 2.144-1.967c.812-.744 1.74-.49 2.07.555l.873 2.775 1.967 2.144c.744.812.49 1.74-.555 2.07l-2.775.873-2.144 1.967c-.812.745-1.74.49-2.07-.555l-.873-2.775zm9.261 164.735l-2.907-.125c-1.1-.048-1.577-.884-1.07-1.855l1.344-2.58.126-2.908c.047-1.1.883-1.577 1.855-1.07l2.58 1.344 2.907.126c1.1.047 1.577.883 1.07 1.855l-1.344 2.58-.125 2.907c-.048 1.1-.884 1.577-1.856 1.07l-2.58-1.344zM88.789 75.876l-1.967-2.144c-.744-.812-.49-1.74.555-2.07l2.775-.873 2.144-1.967c.812-.744 1.74-.49 2.07.555l.873 2.775 1.967 2.144c.744.812.49 1.74-.555 2.07l-2.775.873-2.144 1.967c-.812.745-1.74.49-2.07-.555l-.873-2.775z"/></g></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/wiki_logout_empty.svg b/app/assets/images/illustrations/wiki_logout_empty.svg deleted file mode 100644 index c71841f72e5..00000000000 --- a/app/assets/images/illustrations/wiki_logout_empty.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="412" height="260" viewBox="0 0 412 260" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M6.447.894L12 12H0L5.553.894a.5.5 0 0 1 .894 0z"/></defs><g fill="none" fill-rule="evenodd"><path fill="#FEF0E8" fill-rule="nonzero" d="M338 50.287C322.695 41.45 303.124 46.694 294.287 62c-8.836 15.305-3.592 34.876 11.713 43.712 15.306 8.837 34.877 3.593 43.713-11.712 8.837-15.306 3.593-34.877-11.713-43.713zm2-3.464C357.22 56.763 363.118 78.78 353.177 96c-9.941 17.218-31.958 23.118-49.177 13.176-17.218-9.94-23.118-31.958-13.177-49.176C300.764 42.78 322.782 36.88 340 46.823z"/><g transform="rotate(-150 171.003 8.53)"><path fill="#FC6D26" fill-rule="nonzero" d="M4 16v25a2 2 0 1 0 4 0V16H4zm8-4v29a6 6 0 1 1-12 0V12h12z"/><use fill="#D8D8D8" xlink:href="#a"/><path stroke="#FDC4A8" stroke-width="4" d="M6 4.472L3.236 10h5.528L6 4.472z"/><path fill="#FC6D26" d="M9 6L6.447.894a.5.5 0 0 0-.894 0L3 6c.836.628 1.874 1 3 1a4.978 4.978 0 0 0 3-1z"/></g><path fill="#F9F9F9" d="M263.116 237.116A10.002 10.002 0 0 1 254 243h-86c-11.046 0-20-8.954-20-20V121c0-4.056 2.414-7.547 5.884-9.116A9.964 9.964 0 0 0 153 116v106c0 8.837 7.163 16 16 16h90c1.467 0 2.86-.316 4.116-.884z"/><path fill="#EEE" fill-rule="nonzero" d="M214.5 106H163c-5.523 0-10 4.477-10 10v106c0 8.837 7.163 16 16 16h90c5.523 0 10-4.477 10-10v-17.999a10.036 10.036 0 0 1-4 3.167V228a6 6 0 0 1-6 6h-90c-6.627 0-12-5.373-12-12V116a6 6 0 0 1 6-6h7v-4h44.5z"/><path fill="#EEE" fill-rule="nonzero" d="M260 218.268V214h-90a6 6 0 0 0 0 12h86a4 4 0 0 0 4-4v-.268a1.99 1.99 0 0 1-1 .268h-50a2 2 0 0 1 0-4h50c.364 0 .706.097 1 .268zM170 210h90.5a3.5 3.5 0 0 1 3.5 3.5v8.5a8 8 0 0 1-8 8h-86c-5.523 0-10-4.477-10-10s4.477-10 10-10z"/><path fill="#EEE" fill-rule="nonzero" d="M174 110v100h87a6 6 0 0 0 6-6v-88a6 6 0 0 0-6-6h-87zm-4-4h91c5.523 0 10 4.477 10 10v88c0 5.523-4.477 10-10 10h-91V106z"/><path fill="#EFEDF8" d="M230 99h18a6 6 0 0 1 6 6v31.35a3 3 0 0 1-4.68 2.484l-9.277-6.274a1.5 1.5 0 0 0-1.664-.01l-9.731 6.395a3 3 0 0 1-4.648-2.507V105a6 6 0 0 1 6-6z"/><path fill="#C3B8E3" fill-rule="nonzero" d="M236.182 129.207a5.5 5.5 0 0 1 6.102.04l7.716 5.219V105a2 2 0 0 0-2-2h-18a2 2 0 0 0-2 2v29.584l8.182-5.377zM230 99h18a6 6 0 0 1 6 6v31.35a3 3 0 0 1-4.68 2.484l-9.277-6.274a1.5 1.5 0 0 0-1.664-.01l-9.731 6.395a3 3 0 0 1-4.648-2.507V105a6 6 0 0 1 6-6z"/><g fill-rule="nonzero"><path fill="#EFEDF8" d="M156 74c14.912 0 27-12.088 27-27s-12.088-27-27-27-27 12.088-27 27 12.088 27 27 27zm0 4c-17.12 0-31-13.88-31-31s13.88-31 31-31 31 13.88 31 31-13.88 31-31 31z"/><path fill="#6B4FBB" d="M147.535 44.916l-.116 1.086a8.446 8.446 0 0 0 .093 2.44l.2 1.08-2.262 1.202a.495.495 0 0 0-.213.678l.941 1.77c.128.239.434.332.68.201l2.25-1.196.785.775a8.544 8.544 0 0 0 1.967 1.45l.975.522-.486 2.5a.495.495 0 0 0 .392.59l1.968.383a.504.504 0 0 0 .585-.401l.489-2.515 1.086-.13a8.584 8.584 0 0 0 2.363-.633l1.005-.43 1.68 1.933a.495.495 0 0 0 .708.055l1.513-1.315a.504.504 0 0 0 .044-.708l-1.67-1.922.583-.94c.431-.696.761-1.45.978-2.239l.292-1.063 2.547-.089a.495.495 0 0 0 .488-.515l-.07-2.003a.504.504 0 0 0-.523-.48l-2.56.09-.367-1.037a8.446 8.446 0 0 0-1.139-2.159l-.644-.882 1.509-2.076a.495.495 0 0 0-.106-.702l-1.621-1.178a.504.504 0 0 0-.7.116l-1.494 2.057-1.05-.362a8.459 8.459 0 0 0-2.398-.455l-1.1-.047-.66-2.466a.495.495 0 0 0-.613-.36l-1.936.519a.504.504 0 0 0-.35.617l.661 2.466-.93.59a8.459 8.459 0 0 0-1.848 1.594l-.728.838-2.322-1.034a.495.495 0 0 0-.665.25l-.815 1.83a.504.504 0 0 0 .26.661l2.344 1.044zm-3.565 1.697a3.504 3.504 0 0 1-1.78-4.622l.815-1.83a3.495 3.495 0 0 1 4.626-1.77l.346.154c.259-.245.529-.477.81-.697l-.106-.394a3.504 3.504 0 0 1 2.471-4.292l1.936-.519a3.495 3.495 0 0 1 4.286 2.481l.106.395c.353.05.703.116 1.05.198l.222-.306a3.504 3.504 0 0 1 4.89-.78l1.622 1.178a3.495 3.495 0 0 1 .769 4.892l-.258.355c.184.312.354.633.508.962l.42-.014a3.504 3.504 0 0 1 3.625 3.373l.07 2.003a3.495 3.495 0 0 1-3.382 3.618l-.4.014c-.127.332-.27.659-.426.978l.256.294a3.504 3.504 0 0 1-.34 4.941l-1.512 1.315a3.495 3.495 0 0 1-4.94-.351l-.283-.325a11.669 11.669 0 0 1-1.05.28l-.082.424a3.504 3.504 0 0 1-4.103 2.774l-1.967-.382a3.495 3.495 0 0 1-2.765-4.11l.075-.383a11.547 11.547 0 0 1-.858-.633l-.354.188a3.504 3.504 0 0 1-4.738-1.442l-.94-1.77a3.495 3.495 0 0 1 1.453-4.734l.37-.197a11.436 11.436 0 0 1-.041-1.088l-.4-.178zm13.326 5.608a5.5 5.5 0 1 1-2.847-10.625 5.5 5.5 0 0 1 2.847 10.625zm-.776-2.898a2.5 2.5 0 1 0-1.294-4.83 2.5 2.5 0 0 0 1.294 4.83z"/></g><g fill-rule="nonzero"><path fill="#EFEDF8" d="M326.979 222.047c14.403 3.86 29.209-4.688 33.068-19.092 3.86-14.403-4.688-29.209-19.092-33.068-14.403-3.86-29.209 4.688-33.068 19.092-3.86 14.404 4.688 29.209 19.092 33.068zm-1.035 3.864c-16.538-4.431-26.352-21.43-21.92-37.967 4.43-16.538 21.429-26.352 37.966-21.92 16.538 4.43 26.352 21.429 21.92 37.966-4.43 16.538-21.429 26.352-37.966 21.92z"/><path fill="#6B4FBB" d="M329.376 201.598c-4.668-2.621-7.155-8.157-5.706-13.566 1.715-6.402 8.295-10.201 14.697-8.486 6.402 1.716 10.2 8.296 8.485 14.697-1.45 5.41-6.371 8.96-11.725 8.897a3.03 3.03 0 0 1-.074.365l-1.812 6.761a3 3 0 0 1-5.795-1.552l1.812-6.762a3.03 3.03 0 0 1 .118-.354zm3.815-2.733a8 8 0 1 0 4.14-15.455 8 8 0 0 0-4.14 15.455z"/></g><path fill="#FEF0E8" fill-rule="nonzero" d="M91.373 193c17.071-4.574 27.202-22.12 22.628-39.191-4.575-17.071-22.121-27.202-39.192-22.628-17.071 4.574-27.202 22.121-22.628 39.192 4.574 17.071 22.121 27.202 39.192 22.627zm1.035 3.864c-19.204 5.146-38.945-6.25-44.09-25.456-5.146-19.204 6.25-38.945 25.455-44.09 19.205-5.146 38.945 6.25 44.091 25.455 5.146 19.205-6.25 38.945-25.456 44.091z"/><path fill="#FDC4A8" fill-rule="nonzero" d="M70.067 152.122l6.73 25.114 19.318-5.176-6.73-25.114-19.318 5.176zm-1.035-3.864l19.318-5.176a4 4 0 0 1 4.9 2.828l6.729 25.114a4 4 0 0 1-2.829 4.9L77.832 181.1a4 4 0 0 1-4.9-2.829l-6.729-25.114a4 4 0 0 1 2.829-4.899z"/><path fill="#FC6D26" d="M76.898 154.433l7.727-2.07a2 2 0 0 1 1.036 3.863l-7.728 2.07a2 2 0 1 1-1.035-3.863zm1.812 6.761l5.795-1.553a2 2 0 0 1 1.035 3.864l-5.795 1.553a2 2 0 1 1-1.035-3.864zm1.811 6.762l7.728-2.07a2 2 0 0 1 1.035 3.863l-7.727 2.07a2 2 0 1 1-1.036-3.863z"/></g></svg>
\ No newline at end of file diff --git a/app/assets/javascripts/activities.js b/app/assets/javascripts/activities.js index 6a0662ba903..c117d080bda 100644 --- a/app/assets/javascripts/activities.js +++ b/app/assets/javascripts/activities.js @@ -1,5 +1,6 @@ /* eslint-disable no-param-reassign, class-methods-use-this */ +import $ from 'jquery'; import Cookies from 'js-cookie'; import Pager from './pager'; import { localTimeAgo } from './lib/utils/datetime_utility'; diff --git a/app/assets/javascripts/ajax_loading_spinner.js b/app/assets/javascripts/ajax_loading_spinner.js index 2bc77859c26..bd08308904c 100644 --- a/app/assets/javascripts/ajax_loading_spinner.js +++ b/app/assets/javascripts/ajax_loading_spinner.js @@ -1,3 +1,5 @@ +import $ from 'jquery'; + export default class AjaxLoadingSpinner { static init() { const $elements = $('.js-ajax-loading-spinner'); diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 464611f66f0..cbcefb2c18f 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import _ from 'underscore'; import axios from './lib/utils/axios_utils'; diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index 26e62732b33..6da33a26e58 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -1,4 +1,6 @@ /* eslint-disable class-methods-use-this */ + +import $ from 'jquery'; import _ from 'underscore'; import Cookies from 'js-cookie'; import { __ } from './locale'; diff --git a/app/assets/javascripts/behaviors/copy_to_clipboard.js b/app/assets/javascripts/behaviors/copy_to_clipboard.js index b669b63d23c..e2a73a1797c 100644 --- a/app/assets/javascripts/behaviors/copy_to_clipboard.js +++ b/app/assets/javascripts/behaviors/copy_to_clipboard.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import Clipboard from 'clipboard'; function showTooltip(target, title) { diff --git a/app/assets/javascripts/behaviors/details_behavior.js b/app/assets/javascripts/behaviors/details_behavior.js index 7c9dbcc8d6e..1d63f5baeee 100644 --- a/app/assets/javascripts/behaviors/details_behavior.js +++ b/app/assets/javascripts/behaviors/details_behavior.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; $(() => { $('body').on('click', '.js-details-target', function target() { diff --git a/app/assets/javascripts/behaviors/index.js b/app/assets/javascripts/behaviors/index.js index 8d021de7998..84fef4d8b4f 100644 --- a/app/assets/javascripts/behaviors/index.js +++ b/app/assets/javascripts/behaviors/index.js @@ -1,6 +1,7 @@ import './autosize'; import './bind_in_out'; -import initCopyAsGFM from './copy_as_gfm'; +import './markdown/render_gfm'; +import initCopyAsGFM from './markdown/copy_as_gfm'; import initCopyToClipboard from './copy_to_clipboard'; import './details_behavior'; import installGlEmojiElement from './gl_emoji'; diff --git a/app/assets/javascripts/behaviors/copy_as_gfm.js b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js index ffe90595b5d..75cf90de0b5 100644 --- a/app/assets/javascripts/behaviors/copy_as_gfm.js +++ b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js @@ -1,8 +1,9 @@ /* eslint-disable class-methods-use-this, object-shorthand, no-unused-vars, no-use-before-define, no-new, max-len, no-restricted-syntax, guard-for-in, no-continue */ +import $ from 'jquery'; import _ from 'underscore'; -import { insertText, getSelectedFragment, nodeMatchesSelector } from '../lib/utils/common_utils'; -import { placeholderImage } from '../lazy_loader'; +import { insertText, getSelectedFragment, nodeMatchesSelector } from '~/lib/utils/common_utils'; +import { placeholderImage } from '~/lazy_loader'; const gfmRules = { // The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert diff --git a/app/assets/javascripts/render_gfm.js b/app/assets/javascripts/behaviors/markdown/render_gfm.js index 05a623ca6d9..dbff2bd4b10 100644 --- a/app/assets/javascripts/render_gfm.js +++ b/app/assets/javascripts/behaviors/markdown/render_gfm.js @@ -1,6 +1,7 @@ +import $ from 'jquery'; +import syntaxHighlight from '~/syntax_highlight'; import renderMath from './render_math'; import renderMermaid from './render_mermaid'; -import syntaxHighlight from './syntax_highlight'; // Render Gitlab flavoured Markdown // diff --git a/app/assets/javascripts/render_math.js b/app/assets/javascripts/behaviors/markdown/render_math.js index eabdb01b2a9..7dcf1aeed17 100644 --- a/app/assets/javascripts/render_math.js +++ b/app/assets/javascripts/behaviors/markdown/render_math.js @@ -1,5 +1,6 @@ -import { __ } from './locale'; -import flash from './flash'; +import $ from 'jquery'; +import { __ } from '~/locale'; +import flash from '~/flash'; // Renders math using KaTeX in any element with the // `js-render-math` class diff --git a/app/assets/javascripts/render_mermaid.js b/app/assets/javascripts/behaviors/markdown/render_mermaid.js index d4f18955bd2..56b1896e9f1 100644 --- a/app/assets/javascripts/render_mermaid.js +++ b/app/assets/javascripts/behaviors/markdown/render_mermaid.js @@ -1,3 +1,5 @@ +import flash from '~/flash'; + // Renders diagrams and flowcharts from text using Mermaid in any element with the // `js-render-mermaid` class. // @@ -12,8 +14,6 @@ // </pre> // -import Flash from './flash'; - export default function renderMermaid($els) { if (!$els.length) return; @@ -52,6 +52,6 @@ export default function renderMermaid($els) { }); }); }).catch((err) => { - Flash(`Can't load mermaid module: ${err}`); + flash(`Can't load mermaid module: ${err}`); }); } diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js index 312edc0cd69..3ec932bdb73 100644 --- a/app/assets/javascripts/behaviors/quick_submit.js +++ b/app/assets/javascripts/behaviors/quick_submit.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import '../commons/bootstrap'; import { isInIssuePage } from '../lib/utils/common_utils'; @@ -72,5 +73,5 @@ $(document).on('keyup.quick_submit', '.js-quick-submit input[type=submit], .js-q title, trigger: 'manual', }); - $this.tooltip('show').one('blur', () => $this.tooltip('hide')); + $this.tooltip('show').one('blur click', () => $this.tooltip('hide')); }); diff --git a/app/assets/javascripts/behaviors/requires_input.js b/app/assets/javascripts/behaviors/requires_input.js index e10cb2e3dc4..ffff4ddb71a 100644 --- a/app/assets/javascripts/behaviors/requires_input.js +++ b/app/assets/javascripts/behaviors/requires_input.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import _ from 'underscore'; import '../commons/bootstrap'; diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js index 417ac31fc86..4446be0e52f 100644 --- a/app/assets/javascripts/behaviors/toggler_behavior.js +++ b/app/assets/javascripts/behaviors/toggler_behavior.js @@ -1,3 +1,6 @@ +import $ from 'jquery'; +import { getLocationHash } from '../lib/utils/url_utility'; + // Toggle button. Show/hide content inside parent container. // Button does not change visibility. If button has icon - it changes chevron style. // @@ -5,14 +8,13 @@ // %button.js-toggle-button // %div.js-toggle-content // -import { getLocationHash } from '../lib/utils/url_utility'; $(() => { function toggleContainer(container, toggleState) { const $container = $(container); $container - .find('.js-toggle-button .fa') + .find('.js-toggle-button .fa-chevron-up, .js-toggle-button .fa-chevron-down') .toggleClass('fa-chevron-up', toggleState) .toggleClass('fa-chevron-down', toggleState !== undefined ? !toggleState : undefined); @@ -22,7 +24,7 @@ $(() => { } $('body').on('click', '.js-toggle-button', function toggleButton(e) { - e.target.classList.toggle('open'); + e.currentTarget.classList.toggle(e.currentTarget.dataset.toggleOpenClass || 'open'); toggleContainer($(this).closest('.js-toggle-container')); const targetTag = e.currentTarget.tagName.toLowerCase(); diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js index 83cac896f86..ff1739b1679 100644 --- a/app/assets/javascripts/blob/blob_file_dropzone.js +++ b/app/assets/javascripts/blob/blob_file_dropzone.js @@ -1,4 +1,6 @@ /* eslint-disable func-names, object-shorthand, prefer-arrow-callback */ + +import $ from 'jquery'; import Dropzone from 'dropzone'; import { visitUrl } from '../lib/utils/url_utility'; import { HIDDEN_CLASS } from '../lib/utils/constants'; diff --git a/app/assets/javascripts/blob/blob_fork_suggestion.js b/app/assets/javascripts/blob/blob_fork_suggestion.js index 47c431fb809..476b9405a9e 100644 --- a/app/assets/javascripts/blob/blob_fork_suggestion.js +++ b/app/assets/javascripts/blob/blob_fork_suggestion.js @@ -1,3 +1,5 @@ +import $ from 'jquery'; + const defaults = { // Buttons that will show the `suggestionSections` // has `data-fork-path`, and `data-action` diff --git a/app/assets/javascripts/blob/file_template_mediator.js b/app/assets/javascripts/blob/file_template_mediator.js index 37074301b51..030ca1907e5 100644 --- a/app/assets/javascripts/blob/file_template_mediator.js +++ b/app/assets/javascripts/blob/file_template_mediator.js @@ -1,4 +1,6 @@ /* eslint-disable class-methods-use-this */ + +import $ from 'jquery'; import Flash from '../flash'; import FileTemplateTypeSelector from './template_selectors/type_selector'; import BlobCiYamlSelector from './template_selectors/ci_yaml_selector'; diff --git a/app/assets/javascripts/blob/file_template_selector.js b/app/assets/javascripts/blob/file_template_selector.js index 5ae30990aea..e52cf249f3a 100644 --- a/app/assets/javascripts/blob/file_template_selector.js +++ b/app/assets/javascripts/blob/file_template_selector.js @@ -1,3 +1,5 @@ +import $ from 'jquery'; + export default class FileTemplateSelector { constructor(mediator) { this.mediator = mediator; diff --git a/app/assets/javascripts/blob/template_selector.js b/app/assets/javascripts/blob/template_selector.js index 888883163c5..9dfdb06007d 100644 --- a/app/assets/javascripts/blob/template_selector.js +++ b/app/assets/javascripts/blob/template_selector.js @@ -1,5 +1,7 @@ /* eslint-disable class-methods-use-this, no-unused-vars */ +import $ from 'jquery'; + export default class TemplateSelector { constructor({ dropdown, data, pattern, wrapper, editor, $input } = {}) { this.pattern = pattern; @@ -76,7 +78,7 @@ export default class TemplateSelector { if (!skipFocus) this.editor.focus(); - if (this.editor instanceof jQuery) { + if (this.editor instanceof $) { this.editor.get(0).dispatchEvent(this.autosizeUpdateEvent); } } diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js index 92ea91c45a8..137e1f5a099 100644 --- a/app/assets/javascripts/blob/viewer/index.js +++ b/app/assets/javascripts/blob/viewer/index.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import Flash from '../../flash'; import { handleLocationHash } from '../../lib/utils/common_utils'; import axios from '../../lib/utils/axios_utils'; diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js index 931ed042dfd..4424232f642 100644 --- a/app/assets/javascripts/blob_edit/blob_bundle.js +++ b/app/assets/javascripts/blob_edit/blob_bundle.js @@ -1,5 +1,7 @@ /* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, vars-on-top, no-unused-vars, no-new, max-len */ /* global EditBlob */ + +import $ from 'jquery'; import NewCommitForm from '../new_commit_form'; import EditBlob from './edit_blob'; import BlobFileDropzone from '../blob/blob_file_dropzone'; diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js index d4f6adaccbc..82a3d494b67 100644 --- a/app/assets/javascripts/blob_edit/edit_blob.js +++ b/app/assets/javascripts/blob_edit/edit_blob.js @@ -1,5 +1,6 @@ /* global ace */ +import $ from 'jquery'; import axios from '~/lib/utils/axios_utils'; import createFlash from '~/flash'; import { __ } from '~/locale'; diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js index 9c4cc2338c8..3cffd91716a 100644 --- a/app/assets/javascripts/boards/components/board.js +++ b/app/assets/javascripts/boards/components/board.js @@ -1,4 +1,6 @@ /* eslint-disable comma-dangle, space-before-function-paren, one-var */ + +import $ from 'jquery'; import Sortable from 'vendor/Sortable'; import Vue from 'vue'; import AccessorUtilities from '../../lib/utils/accessor'; diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue index 23fec503586..84885ca9306 100644 --- a/app/assets/javascripts/boards/components/board_card.vue +++ b/app/assets/javascripts/boards/components/board_card.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/require-default-prop */ import './issue_card_inner'; import eventHub from '../eventhub'; @@ -34,6 +35,9 @@ export default { type: String, default: '', }, + groupId: { + type: Number, + }, }, data() { return { @@ -88,6 +92,7 @@ export default { :list="list" :issue="issue" :issue-link-base="issueLinkBase" + :group-id="groupId" :root-path="rootPath" :update-filters="true" /> diff --git a/app/assets/javascripts/boards/components/board_delete.js b/app/assets/javascripts/boards/components/board_delete.js index 8a1b177bba8..7be98825fda 100644 --- a/app/assets/javascripts/boards/components/board_delete.js +++ b/app/assets/javascripts/boards/components/board_delete.js @@ -1,5 +1,6 @@ /* eslint-disable comma-dangle, space-before-function-paren, no-alert */ +import $ from 'jquery'; import Vue from 'vue'; window.gl = window.gl || {}; diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue index 6637904d87d..0d03c1c419c 100644 --- a/app/assets/javascripts/boards/components/board_list.vue +++ b/app/assets/javascripts/boards/components/board_list.vue @@ -15,6 +15,11 @@ export default { loadingIcon, }, props: { + groupId: { + type: Number, + required: false, + default: 0, + }, disabled: { type: Boolean, required: true, @@ -170,6 +175,7 @@ export default { <loading-icon /> </div> <board-new-issue + :group-id="groupId" :list="list" v-if="list.type !== 'closed' && showIssueForm"/> <ul @@ -185,6 +191,7 @@ export default { :list="list" :issue="issue" :issue-link-base="issueLinkBase" + :group-id="groupId" :root-path="rootPath" :disabled="disabled" :key="issue.id" /> diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue index efface7143d..8d84c1735b8 100644 --- a/app/assets/javascripts/boards/components/board_new_issue.vue +++ b/app/assets/javascripts/boards/components/board_new_issue.vue @@ -1,12 +1,22 @@ <script> +import $ from 'jquery'; import eventHub from '../eventhub'; +import ProjectSelect from './project_select.vue'; import ListIssue from '../models/issue'; const Store = gl.issueBoards.BoardsStore; export default { name: 'BoardNewIssue', + components: { + ProjectSelect, + }, props: { + groupId: { + type: Number, + required: false, + default: 0, + }, list: { type: Object, required: true, @@ -16,10 +26,20 @@ export default { return { title: '', error: false, + selectedProject: {}, }; }, + computed: { + disabled() { + if (this.groupId) { + return this.title === '' || !this.selectedProject.name; + } + return this.title === ''; + }, + }, mounted() { this.$refs.input.focus(); + eventHub.$on('setSelectedProject', this.setSelectedProject); }, methods: { submit(e) { @@ -34,6 +54,7 @@ export default { labels, subscribed: true, assignees: [], + project_id: this.selectedProject.id, }); eventHub.$emit(`scroll-board-list-${this.list.id}`); @@ -62,52 +83,62 @@ export default { this.title = ''; eventHub.$emit(`hide-issue-form-${this.list.id}`); }, + setSelectedProject(selectedProject) { + this.selectedProject = selectedProject; + }, }, }; </script> <template> - <div class="card board-new-issue-form"> - <form @submit="submit($event)"> - <div - class="flash-container" - v-if="error" - > - <div class="flash-alert"> - An error occurred. Please try again. - </div> - </div> - <label - class="label-light" - :for="list.id + '-title'" - > - Title - </label> - <input - class="form-control" - type="text" - v-model="title" - ref="input" - autocomplete="off" - :id="list.id + '-title'" - /> - <div class="clearfix prepend-top-10"> - <button - class="btn btn-success pull-left" - type="submit" - :disabled="title === ''" - ref="submit-button" + <div class="board-new-issue-form"> + <div class="card"> + <form @submit="submit($event)"> + <div + class="flash-container" + v-if="error" > - Submit issue - </button> - <button - class="btn btn-default pull-right" - type="button" - @click="cancel" + <div class="flash-alert"> + An error occurred. Please try again. + </div> + </div> + <label + class="label-light" + :for="list.id + '-title'" > - Cancel - </button> - </div> - </form> + Title + </label> + <input + class="form-control" + type="text" + v-model="title" + ref="input" + autocomplete="off" + :id="list.id + '-title'" + /> + <project-select + v-if="groupId" + :group-id="groupId" + /> + <div class="clearfix prepend-top-10"> + <button + class="btn btn-success pull-left" + type="submit" + :disabled="disabled" + ref="submit-button" + > + Submit issue + </button> + <button + class="btn btn-default pull-right" + type="button" + @click="cancel" + > + Cancel + </button> + </div> + </form> + </div> </div> </template> + diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js index 9501e35b178..a44969272a1 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js +++ b/app/assets/javascripts/boards/components/board_sidebar.js @@ -1,11 +1,12 @@ /* eslint-disable comma-dangle, space-before-function-paren, no-new */ +import $ from 'jquery'; import Vue from 'vue'; import Flash from '../../flash'; import { __ } from '../../locale'; import Sidebar from '../../right_sidebar'; import eventHub from '../../sidebar/event_hub'; -import assigneeTitle from '../../sidebar/components/assignees/assignee_title'; +import assigneeTitle from '../../sidebar/components/assignees/assignee_title.vue'; import assignees from '../../sidebar/components/assignees/assignees.vue'; import DueDateSelectors from '../../due_date_select'; import './sidebar/remove_issue'; diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js b/app/assets/javascripts/boards/components/issue_card_inner.js index bf474879024..7e882a57202 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.js +++ b/app/assets/javascripts/boards/components/issue_card_inner.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import Vue from 'vue'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import eventHub from '../eventhub'; @@ -31,6 +32,10 @@ gl.issueBoards.IssueCardInner = Vue.extend({ required: false, default: false, }, + groupId: { + type: Number, + required: false, + }, }, data() { return { @@ -64,7 +69,13 @@ gl.issueBoards.IssueCardInner = Vue.extend({ return this.issue.assignees.length > this.numberOverLimit; }, cardUrl() { - return `${this.issueLinkBase}/${this.issue.iid}`; + let baseUrl = this.issueLinkBase; + + if (this.groupId && this.issue.project) { + baseUrl = this.issueLinkBase.replace(':project_path', this.issue.project.path); + } + + return `${baseUrl}/${this.issue.iid}`; }, issueId() { if (this.issue.iid) { @@ -148,7 +159,7 @@ gl.issueBoards.IssueCardInner = Vue.extend({ class="card-number" v-if="issueId" > - {{ issueId }} + <template v-if="groupId && issue.project">{{issue.project.path}}</template>{{ issueId }} </span> </h4> <div class="card-assignee"> diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js index 362ef43e6f7..71f49319c36 100644 --- a/app/assets/javascripts/boards/components/new_list_dropdown.js +++ b/app/assets/javascripts/boards/components/new_list_dropdown.js @@ -1,5 +1,6 @@ -/* eslint-disable func-names, no-new, space-before-function-paren, one-var, - promise/catch-or-return */ +/* eslint-disable func-names, no-new, space-before-function-paren, one-var, promise/catch-or-return, max-len */ + +import $ from 'jquery'; import axios from '~/lib/utils/axios_utils'; import _ from 'underscore'; import CreateLabelDropdown from '../../create_label'; diff --git a/app/assets/javascripts/boards/components/project_select.vue b/app/assets/javascripts/boards/components/project_select.vue new file mode 100644 index 00000000000..371774098b9 --- /dev/null +++ b/app/assets/javascripts/boards/components/project_select.vue @@ -0,0 +1,129 @@ +<script> + /* global ListIssue */ + + import $ from 'jquery'; + import _ from 'underscore'; + import eventHub from '../eventhub'; + import loadingIcon from '../../vue_shared/components/loading_icon.vue'; + import Api from '../../api'; + + export default { + name: 'BoardProjectSelect', + components: { + loadingIcon, + }, + props: { + groupId: { + type: Number, + required: true, + default: 0, + }, + }, + data() { + return { + loading: true, + selectedProject: {}, + }; + }, + computed: { + selectedProjectName() { + return this.selectedProject.name || 'Select a project'; + }, + }, + mounted() { + $(this.$refs.projectsDropdown).glDropdown({ + filterable: true, + filterRemote: true, + search: { + fields: ['name_with_namespace'], + }, + clicked: ({ $el, e }) => { + e.preventDefault(); + this.selectedProject = { + id: $el.data('project-id'), + name: $el.data('project-name'), + }; + eventHub.$emit('setSelectedProject', this.selectedProject); + }, + selectable: true, + data: (term, callback) => { + this.loading = true; + return Api.groupProjects(this.groupId, term, (projects) => { + this.loading = false; + callback(projects); + }); + }, + renderRow(project) { + return ` + <li> + <a href='#' class='dropdown-menu-link' data-project-id="${project.id}" data-project-name="${project.name}"> + ${_.escape(project.name)} + </a> + </li> + `; + }, + text: project => project.name, + }); + }, + }; +</script> + +<template> + <div> + <label class="label-light prepend-top-10"> + Project + </label> + <div + ref="projectsDropdown" + class="dropdown" + > + <button + class="dropdown-menu-toggle wide" + type="button" + data-toggle="dropdown" + aria-expanded="false" + > + {{ selectedProjectName }} + <i + class="fa fa-chevron-down" + aria-hidden="true" + > + </i> + </button> + <div class="dropdown-menu dropdown-menu-selectable dropdown-menu-full-width"> + <div class="dropdown-title"> + <span>Projects</span> + <button + aria-label="Close" + type="button" + class="dropdown-title-button dropdown-menu-close" + > + <i + aria-hidden="true" + data-hidden="true" + class="fa fa-times dropdown-menu-close-icon" + > + </i> + </button> + </div> + <div class="dropdown-input"> + <input + class="dropdown-input-field" + type="search" + placeholder="Search projects" + /> + <i + aria-hidden="true" + data-hidden="true" + class="fa fa-search dropdown-input-search" + > + </i> + </div> + <div class="dropdown-content"></div> + <div class="dropdown-loading"> + <loading-icon /> + </div> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.js b/app/assets/javascripts/boards/components/sidebar/remove_issue.js index 0ae32bb4d0a..09c683ff621 100644 --- a/app/assets/javascripts/boards/components/sidebar/remove_issue.js +++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.js @@ -24,7 +24,7 @@ gl.issueBoards.RemoveIssueBtn = Vue.extend({ }, computed: { updateUrl() { - return this.issueUpdate; + return this.issueUpdate.replace(':project_path', this.issue.project.path); }, }, methods: { @@ -32,17 +32,21 @@ gl.issueBoards.RemoveIssueBtn = Vue.extend({ const issue = this.issue; const lists = issue.getLists(); const listLabelIds = lists.map(list => list.label.id); - let labelIds = this.issue.labels + + let labelIds = issue.labels .map(label => label.id) .filter(id => !listLabelIds.includes(id)); if (labelIds.length === 0) { labelIds = ['']; } + const data = { issue: { label_ids: labelIds, }, }; + + // Post the remove data Vue.http.patch(this.updateUrl, data).catch(() => { Flash(__('Failed to remove issue from board, please try again.')); diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js index 57a7cc4ca30..fb40b9f5565 100644 --- a/app/assets/javascripts/boards/filtered_search_boards.js +++ b/app/assets/javascripts/boards/filtered_search_boards.js @@ -6,6 +6,7 @@ export default class FilteredSearchBoards extends FilteredSearchManager { constructor(store, updateUrl = false, cantEdit = []) { super({ page: 'boards', + stateFiltersSelector: '.issues-state-filters', }); this.store = store; diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index 8e31f1865f0..8b1c14c04ff 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -1,18 +1,20 @@ /* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */ +import $ from 'jquery'; import _ from 'underscore'; import Vue from 'vue'; import Flash from '~/flash'; import { __ } from '~/locale'; +import '~/vue_shared/models/label'; import FilteredSearchBoards from './filtered_search_boards'; import eventHub from './eventhub'; import sidebarEventHub from '~/sidebar/event_hub'; // eslint-disable-line import/first import './models/issue'; -import './models/label'; import './models/list'; import './models/milestone'; +import './models/project'; import './models/assignee'; import './stores/boards_store'; import './stores/modal_store'; @@ -89,7 +91,7 @@ export default () => { sidebarEventHub.$off('toggleSubscription', this.toggleSubscription); }, mounted () { - this.filterManager = new FilteredSearchBoards(Store.filter, true); + this.filterManager = new FilteredSearchBoards(Store.filter, true, Store.cantEdit); this.filterManager.setup(); Store.disabled = this.disabled; @@ -179,6 +181,7 @@ export default () => { return { modal: ModalStore.store, store: Store.state, + canAdminList: this.$options.el.hasAttribute('data-can-admin-list'), }; }, computed: { @@ -232,6 +235,7 @@ export default () => { :class="{ 'disabled': disabled }" :title="tooltipTitle" :aria-disabled="disabled" + v-if="canAdminList" @click="openModal"> Add issues </button> diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js b/app/assets/javascripts/boards/mixins/sortable_default_options.js index 38a0eb12f92..ac316c31deb 100644 --- a/app/assets/javascripts/boards/mixins/sortable_default_options.js +++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js @@ -1,6 +1,9 @@ /* eslint-disable no-unused-vars, no-mixed-operators, comma-dangle */ /* global DocumentTouch */ +import $ from 'jquery'; +import sortableConfig from '../../sortable/sortable_config'; + window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; @@ -18,19 +21,14 @@ gl.issueBoards.onEnd = () => { gl.issueBoards.touchEnabled = ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch; gl.issueBoards.getBoardSortableDefaultOptions = (obj) => { - const defaultSortOptions = { - animation: 200, - forceFallback: true, - fallbackClass: 'is-dragging', - fallbackOnBody: true, - ghostClass: 'is-ghost', + const defaultSortOptions = Object.assign({}, sortableConfig, { filter: '.board-delete, .btn', delay: gl.issueBoards.touchEnabled ? 100 : 0, scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100, scrollSpeed: 20, onStart: gl.issueBoards.onStart, - onEnd: gl.issueBoards.onEnd - }; + onEnd: gl.issueBoards.onEnd, + }); Object.keys(obj).forEach((key) => { defaultSortOptions[key] = obj[key]; }); return defaultSortOptions; diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js index 3bfb6d39ad5..4c5079efc8b 100644 --- a/app/assets/javascripts/boards/models/issue.js +++ b/app/assets/javascripts/boards/models/issue.js @@ -4,6 +4,7 @@ /* global ListAssignee */ import Vue from 'vue'; +import IssueProject from './project'; class ListIssue { constructor (obj, defaultAvatar) { @@ -23,6 +24,12 @@ class ListIssue { this.isLoading = {}; this.sidebarInfoEndpoint = obj.issue_sidebar_endpoint; this.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint; + this.milestone_id = obj.milestone_id; + this.project_id = obj.project_id; + + if (obj.project) { + this.project = new IssueProject(obj.project); + } if (obj.milestone) { this.milestone = new ListMilestone(obj.milestone); @@ -105,7 +112,8 @@ class ListIssue { data.issue.label_ids = ['']; } - return Vue.http.patch(url, data); + const projectPath = this.project ? this.project.path : ''; + return Vue.http.patch(url.replace(':project_path', projectPath), data); } } diff --git a/app/assets/javascripts/boards/models/project.js b/app/assets/javascripts/boards/models/project.js new file mode 100644 index 00000000000..a3d5c7af7ac --- /dev/null +++ b/app/assets/javascripts/boards/models/project.js @@ -0,0 +1,6 @@ +export default class IssueProject { + constructor(obj) { + this.id = obj.id; + this.path = obj.path; + } +} diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index 348cdeec737..20e78edf2a2 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -1,5 +1,7 @@ /* eslint-disable comma-dangle, space-before-function-paren, one-var, no-shadow, dot-notation, max-len */ /* global List */ + +import $ from 'jquery'; import _ from 'underscore'; import Cookies from 'js-cookie'; import { getUrlParamsArray } from '~/lib/utils/common_utils'; diff --git a/app/assets/javascripts/branches/branches_delete_modal.js b/app/assets/javascripts/branches/branches_delete_modal.js index cbc28374b80..839e369eaf6 100644 --- a/app/assets/javascripts/branches/branches_delete_modal.js +++ b/app/assets/javascripts/branches/branches_delete_modal.js @@ -1,3 +1,5 @@ +import $ from 'jquery'; + const MODAL_SELECTOR = '#modal-delete-branch'; class DeleteModal { diff --git a/app/assets/javascripts/breadcrumb.js b/app/assets/javascripts/breadcrumb.js index 10fbcfe96cf..1474d93dde6 100644 --- a/app/assets/javascripts/breadcrumb.js +++ b/app/assets/javascripts/breadcrumb.js @@ -1,3 +1,5 @@ +import $ from 'jquery'; + export const addTooltipToEl = (el) => { const textEl = el.querySelector('.js-breadcrumb-item-text'); diff --git a/app/assets/javascripts/build_artifacts.js b/app/assets/javascripts/build_artifacts.js index ace89398943..3fa16517388 100644 --- a/app/assets/javascripts/build_artifacts.js +++ b/app/assets/javascripts/build_artifacts.js @@ -1,4 +1,6 @@ /* eslint-disable func-names, prefer-arrow-callback, no-return-assign */ + +import $ from 'jquery'; import { visitUrl } from './lib/utils/url_utility'; import { convertPermissionToBoolean } from './lib/utils/common_utils'; diff --git a/app/assets/javascripts/build_variables.js b/app/assets/javascripts/build_variables.js index 35edf3e0017..d398e4a4c83 100644 --- a/app/assets/javascripts/build_variables.js +++ b/app/assets/javascripts/build_variables.js @@ -1,9 +1,9 @@ -/* eslint-disable func-names*/ +import $ from 'jquery'; export default function handleRevealVariables() { $('.js-reveal-variables') .off('click') - .on('click', function () { + .on('click', function click() { $('.js-build-variables').toggle(); $(this).hide(); }); diff --git a/app/assets/javascripts/ci_variable_list/native_form_variable_list.js b/app/assets/javascripts/ci_variable_list/native_form_variable_list.js index d54ea7df1c3..7cd5916ac9c 100644 --- a/app/assets/javascripts/ci_variable_list/native_form_variable_list.js +++ b/app/assets/javascripts/ci_variable_list/native_form_variable_list.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import VariableList from './ci_variable_list'; // Used for the variable list on scheduled pipeline edit page diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue index 1325a268214..f8dcdf3f60a 100644 --- a/app/assets/javascripts/clusters/components/applications.vue +++ b/app/assets/javascripts/clusters/components/applications.vue @@ -117,7 +117,10 @@ </script> <template> - <section class="settings no-animate expanded"> + <section + id="cluster-applications" + class="settings no-animate expanded" + > <div class="settings-header"> <h4> {{ s__('ClusterIntegration|Applications') }} @@ -183,7 +186,7 @@ <clipboard-button :text="ingressExternalIp" :title="s__('ClusterIntegration|Copy Ingress IP Address to clipboard')" - css-class="btn btn-default js-clipboard-btn" + class="js-clipboard-btn" /> </span> </div> diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js index 6504a0bbbfc..7f3d04655a7 100644 --- a/app/assets/javascripts/commit/image_file.js +++ b/app/assets/javascripts/commit/image_file.js @@ -1,5 +1,7 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-use-before-define, prefer-arrow-callback, no-else-return, consistent-return, prefer-template, quotes, one-var, one-var-declaration-per-line, no-unused-vars, no-return-assign, comma-dangle, quote-props, no-unused-expressions, no-sequences, object-shorthand, max-len */ +import $ from 'jquery'; + // Width where images must fits in, for 2-up this gets divided by 2 const availWidth = 900; const viewModes = ['two-up', 'swipe']; diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue index ce19069f103..466a5b5d635 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue @@ -20,10 +20,6 @@ type: String, required: true, }, - emptyStateSvgPath: { - type: String, - required: true, - }, errorStateSvgPath: { type: String, required: true, @@ -45,23 +41,14 @@ }, computed: { - /** - * Empty state is only rendered if after the first request we receive no pipelines. - * - * @return {Boolean} - */ - shouldRenderEmptyState() { - return !this.state.pipelines.length && - !this.isLoading && - this.hasMadeRequest && - !this.hasError; - }, - shouldRenderTable() { return !this.isLoading && this.state.pipelines.length > 0 && !this.hasError; }, + shouldRenderErrorState() { + return this.hasError && !this.isLoading; + }, }, created() { this.service = new PipelinesService(this.endpoint); @@ -92,25 +79,22 @@ <div class="content-list pipelines"> <loading-icon - label="Loading pipelines" + :label="s__('Pipelines|Loading Pipelines')" size="3" v-if="isLoading" + class="prepend-top-20" /> - <empty-state - v-if="shouldRenderEmptyState" - :help-page-path="helpPagePath" - :empty-state-svg-path="emptyStateSvgPath" - /> - - <error-state - v-if="shouldRenderErrorState" - :error-state-svg-path="errorStateSvgPath" + <svg-blank-state + v-else-if="shouldRenderErrorState" + :svg-path="errorStateSvgPath" + :message="s__(`Pipelines|There was an error fetching the pipelines. + Try again in a few moments or contact your support team.`)" /> <div class="table-holder" - v-if="shouldRenderTable" + v-else-if="shouldRenderTable" > <pipelines-table-component :pipelines="state.pipelines" diff --git a/app/assets/javascripts/commit_merge_requests.js b/app/assets/javascripts/commit_merge_requests.js index f76c9b7e690..102b4ee8463 100644 --- a/app/assets/javascripts/commit_merge_requests.js +++ b/app/assets/javascripts/commit_merge_requests.js @@ -1,5 +1,6 @@ /* global Flash */ +import $ from 'jquery'; import axios from './lib/utils/axios_utils'; import { n__, s__ } from './locale'; diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js index 2be63bd8c76..7e2a3573f81 100644 --- a/app/assets/javascripts/commits.js +++ b/app/assets/javascripts/commits.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import { pluralize } from './lib/utils/text_utility'; import { localTimeAgo } from './lib/utils/datetime_utility'; import Pager from './pager'; diff --git a/app/assets/javascripts/commons/vue.js b/app/assets/javascripts/commons/vue.js index 8b62d78c043..798623b94fb 100644 --- a/app/assets/javascripts/commons/vue.js +++ b/app/assets/javascripts/commons/vue.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import '../vue_shared/vue_resource_interceptor'; if (process.env.NODE_ENV !== 'production') { Vue.config.productionTip = false; diff --git a/app/assets/javascripts/compare.js b/app/assets/javascripts/compare.js index d5a35ed81a6..303a5bf4a53 100644 --- a/app/assets/javascripts/compare.js +++ b/app/assets/javascripts/compare.js @@ -1,4 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, object-shorthand, consistent-return, no-unused-vars, comma-dangle, vars-on-top, prefer-template, max-len */ + +import $ from 'jquery'; import { localTimeAgo } from './lib/utils/datetime_utility'; import axios from './lib/utils/axios_utils'; diff --git a/app/assets/javascripts/compare_autocomplete.js b/app/assets/javascripts/compare_autocomplete.js index fa341918fc1..260c91cac24 100644 --- a/app/assets/javascripts/compare_autocomplete.js +++ b/app/assets/javascripts/compare_autocomplete.js @@ -1,4 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, object-shorthand, comma-dangle, prefer-arrow-callback, no-else-return, newline-per-chained-call, wrap-iife, max-len */ + +import $ from 'jquery'; import { __ } from './locale'; import axios from './lib/utils/axios_utils'; import flash from './flash'; diff --git a/app/assets/javascripts/confirm_danger_modal.js b/app/assets/javascripts/confirm_danger_modal.js index eae4a7eab55..1638e09132b 100644 --- a/app/assets/javascripts/confirm_danger_modal.js +++ b/app/assets/javascripts/confirm_danger_modal.js @@ -1,31 +1,32 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, camelcase, one-var-declaration-per-line, no-else-return, max-len */ +import $ from 'jquery'; import { rstrip } from './lib/utils/common_utils'; -window.ConfirmDangerModal = (function() { - function ConfirmDangerModal(form, text) { - var project_path, submit; - this.form = form; - $('.js-confirm-text').text(text || ''); - $('.js-confirm-danger-input').val(''); - $('#modal-confirm-danger').modal('show'); - project_path = $('.js-confirm-danger-match').text(); - submit = $('.js-confirm-danger-submit'); - submit.disable(); - $('.js-confirm-danger-input').off('input'); - $('.js-confirm-danger-input').on('input', function() { - if (rstrip($(this).val()) === project_path) { - return submit.enable(); - } else { - return submit.disable(); - } - }); - $('.js-confirm-danger-submit').off('click'); - $('.js-confirm-danger-submit').on('click', (function(_this) { - return function() { - return _this.form.submit(); - }; - })(this)); - } +function openConfirmDangerModal($form, text) { + $('.js-confirm-text').text(text || ''); + $('.js-confirm-danger-input').val(''); + $('#modal-confirm-danger').modal('show'); - return ConfirmDangerModal; -})(); + const confirmTextMatch = $('.js-confirm-danger-match').text(); + const $submit = $('.js-confirm-danger-submit'); + $submit.disable(); + + $('.js-confirm-danger-input').off('input').on('input', function handleInput() { + const confirmText = rstrip($(this).val()); + if (confirmText === confirmTextMatch) { + $submit.enable(); + } else { + $submit.disable(); + } + }); + $('.js-confirm-danger-submit').off('click').on('click', () => $form.submit()); +} + +export default function initConfirmDangerModal() { + $(document).on('click', '.js-confirm-danger', (e) => { + e.preventDefault(); + const $btn = $(e.target); + const $form = $btn.closest('form'); + const text = $btn.data('confirmDangerMessage'); + openConfirmDangerModal($form, text); + }); +} diff --git a/app/assets/javascripts/contextual_sidebar.js b/app/assets/javascripts/contextual_sidebar.js index 74520675a7c..3a50e73ad85 100644 --- a/app/assets/javascripts/contextual_sidebar.js +++ b/app/assets/javascripts/contextual_sidebar.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import Cookies from 'js-cookie'; import _ from 'underscore'; import bp from './breakpoints'; diff --git a/app/assets/javascripts/create_label.js b/app/assets/javascripts/create_label.js index 9a4c9bfcc80..a999c21b2e9 100644 --- a/app/assets/javascripts/create_label.js +++ b/app/assets/javascripts/create_label.js @@ -1,4 +1,6 @@ /* eslint-disable func-names, prefer-arrow-callback */ + +import $ from 'jquery'; import Api from './api'; import { humanize } from './lib/utils/text_utility'; diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js index 46d89c825f9..87f8854f940 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import Vue from 'vue'; import Cookies from 'js-cookie'; import Flash from '../flash'; diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js index 3df082e8c0c..a044fc1ab42 100644 --- a/app/assets/javascripts/diff.js +++ b/app/assets/javascripts/diff.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import axios from '~/lib/utils/axios_utils'; import flash from '~/flash'; import { __ } from '~/locale'; diff --git a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js index aed7cac4e62..d1260ff5373 100644 --- a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js +++ b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js @@ -1,6 +1,7 @@ /* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, quotes, no-lonely-if, max-len */ /* global CommentsStore */ +import $ from 'jquery'; import Vue from 'vue'; const CommentAndResolveBtn = Vue.extend({ diff --git a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js index 300b02da663..180a6bd67e7 100644 --- a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js +++ b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js @@ -1,5 +1,6 @@ /* global CommentsStore */ +import $ from 'jquery'; import Vue from 'vue'; import collapseIcon from '../icons/collapse_icon.svg'; import Notes from '../../notes'; 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 fadc34959e1..8f9186dfb9a 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 DiscussionMixins */ /* global CommentsStore */ +import $ from 'jquery'; import Vue from 'vue'; import '../mixins/discussion'; diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js b/app/assets/javascripts/diff_notes/components/resolve_btn.js index cc9192deae3..df4c72ba0ed 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_btn.js +++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js @@ -2,6 +2,7 @@ /* global CommentsStore */ /* global ResolveService */ +import $ from 'jquery'; import Vue from 'vue'; import Flash from '../../flash'; diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js b/app/assets/javascripts/diff_notes/diff_notes_bundle.js index 5f49609fe88..e17daec6a92 100644 --- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js +++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js @@ -1,6 +1,7 @@ /* eslint-disable func-names, comma-dangle, new-cap, no-new, max-len */ /* global ResolveCount */ +import $ from 'jquery'; import Vue from 'vue'; import './models/discussion'; import './models/note'; diff --git a/app/assets/javascripts/diff_notes/models/discussion.js b/app/assets/javascripts/diff_notes/models/discussion.js index 1b8a9af9390..c97c559dd14 100644 --- a/app/assets/javascripts/diff_notes/models/discussion.js +++ b/app/assets/javascripts/diff_notes/models/discussion.js @@ -1,6 +1,7 @@ /* eslint-disable space-before-function-paren, camelcase, guard-for-in, no-restricted-syntax, no-unused-vars, max-len */ /* global NoteModel */ +import $ from 'jquery'; import Vue from 'vue'; import { localTimeAgo } from '../../lib/utils/datetime_utility'; diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 1ccf96a75dc..72f21f13860 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -1,4 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */ + +import $ from 'jquery'; import Flash from './flash'; import GfmAutoComplete from './gfm_auto_complete'; import { convertPermissionToBoolean } from './lib/utils/common_utils'; @@ -51,8 +53,12 @@ function initPageShortcuts(page) { function initGFMInput() { $('.js-gfm-input:not(.js-vue-textarea)').each((i, el) => { - const gfm = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources); - const enableGFM = convertPermissionToBoolean(el.dataset.supportsAutocomplete); + const gfm = new GfmAutoComplete( + gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources, + ); + const enableGFM = convertPermissionToBoolean( + el.dataset.supportsAutocomplete, + ); gfm.setup($(el), { emojis: true, members: enableGFM, @@ -65,9 +71,9 @@ function initGFMInput() { } function initPerformanceBar() { - if (document.querySelector('#peek')) { + if (document.querySelector('#js-peek')) { import('./performance_bar') - .then(m => new m.default({ container: '#peek' })) // eslint-disable-line new-cap + .then(m => new m.default({ container: '#js-peek' })) // eslint-disable-line new-cap .catch(() => Flash('Error loading performance bar module')); } } diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js index ba89e5726fa..5528ad9f38d 100644 --- a/app/assets/javascripts/dropzone_input.js +++ b/app/assets/javascripts/dropzone_input.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import Dropzone from 'dropzone'; import _ from 'underscore'; import './preview_markdown'; diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js index 417258e0092..842a4255f08 100644 --- a/app/assets/javascripts/due_date_select.js +++ b/app/assets/javascripts/due_date_select.js @@ -1,5 +1,6 @@ /* global dateFormat */ +import $ from 'jquery'; import Pikaday from 'pikaday'; import axios from './lib/utils/axios_utils'; import { parsePikadayDate, pikadayToString } from './lib/utils/datefix'; diff --git a/app/assets/javascripts/environments/components/environment_stop.vue b/app/assets/javascripts/environments/components/environment_stop.vue index 1eef17bf1fe..dda7429a726 100644 --- a/app/assets/javascripts/environments/components/environment_stop.vue +++ b/app/assets/javascripts/environments/components/environment_stop.vue @@ -3,6 +3,8 @@ * Renders the stop "button" that allows stop an environment. * Used in environments table. */ + + import $ from 'jquery'; import eventHub from '../event_hub'; import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import tooltip from '../../vue_shared/directives/tooltip'; diff --git a/app/assets/javascripts/experimental_flags.js b/app/assets/javascripts/experimental_flags.js index 6ee65ca72f9..1d60847147b 100644 --- a/app/assets/javascripts/experimental_flags.js +++ b/app/assets/javascripts/experimental_flags.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import Cookies from 'js-cookie'; export default () => { diff --git a/app/assets/javascripts/feature_highlight/feature_highlight.js b/app/assets/javascripts/feature_highlight/feature_highlight.js index d65cc6d5d7d..c50ac667c20 100644 --- a/app/assets/javascripts/feature_highlight/feature_highlight.js +++ b/app/assets/javascripts/feature_highlight/feature_highlight.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import _ from 'underscore'; import { getSelector, diff --git a/app/assets/javascripts/feature_highlight/feature_highlight_helper.js b/app/assets/javascripts/feature_highlight/feature_highlight_helper.js index 939d12237f3..f480e72961c 100644 --- a/app/assets/javascripts/feature_highlight/feature_highlight_helper.js +++ b/app/assets/javascripts/feature_highlight/feature_highlight_helper.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import axios from '../lib/utils/axios_utils'; import { __ } from '../locale'; import Flash from '../flash'; diff --git a/app/assets/javascripts/filterable_list.js b/app/assets/javascripts/filterable_list.js index a10f027de53..b17ba3c21db 100644 --- a/app/assets/javascripts/filterable_list.js +++ b/app/assets/javascripts/filterable_list.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import _ from 'underscore'; import axios from './lib/utils/axios_utils'; diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js index ee49a7be0b2..e6390f0855b 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js @@ -16,6 +16,7 @@ export default class FilteredSearchDropdownManager { page, isGroup, isGroupAncestor, + isGroupDecendent, filteredSearchTokenKeys, }) { this.container = FilteredSearchContainer.container; @@ -26,6 +27,7 @@ export default class FilteredSearchDropdownManager { this.page = page; this.groupsOnly = isGroup; this.groupAncestor = isGroupAncestor; + this.isGroupDecendent = isGroupDecendent; this.setupMapping(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index c6970d7837f..71b7e80335b 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -22,11 +22,13 @@ export default class FilteredSearchManager { page, isGroup = false, isGroupAncestor = false, + isGroupDecendent = false, filteredSearchTokenKeys = FilteredSearchTokenKeys, stateFiltersSelector = '.issues-state-filters', }) { this.isGroup = isGroup; this.isGroupAncestor = isGroupAncestor; + this.isGroupDecendent = isGroupDecendent; this.states = ['opened', 'closed', 'merged', 'all']; this.page = page; diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js index a19bb882410..600024c21c3 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js +++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js @@ -1,5 +1,6 @@ import _ from 'underscore'; -import AjaxCache from '../lib/utils/ajax_cache'; +import AjaxCache from '~/lib/utils/ajax_cache'; +import { objectToQueryString } from '~/lib/utils/common_utils'; import Flash from '../flash'; import FilteredSearchContainer from './container'; import UsersCache from '../lib/utils/users_cache'; @@ -16,6 +17,21 @@ export default class FilteredSearchVisualTokens { }; } + /** + * Returns a computed API endpoint + * and query string composed of values from endpointQueryParams + * @param {String} endpoint + * @param {String} endpointQueryParams + */ + static getEndpointWithQueryParams(endpoint, endpointQueryParams) { + if (!endpointQueryParams) { + return endpoint; + } + + const queryString = objectToQueryString(JSON.parse(endpointQueryParams)); + return `${endpoint}?${queryString}`; + } + static unselectTokens() { const otherTokens = FilteredSearchContainer.container.querySelectorAll('.js-visual-token .selectable.selected'); [].forEach.call(otherTokens, t => t.classList.remove('selected')); @@ -86,7 +102,10 @@ export default class FilteredSearchVisualTokens { static updateLabelTokenColor(tokenValueContainer, tokenValue) { const filteredSearchInput = FilteredSearchContainer.container.querySelector('.filtered-search'); const baseEndpoint = filteredSearchInput.dataset.baseEndpoint; - const labelsEndpoint = `${baseEndpoint}/labels.json`; + const labelsEndpoint = FilteredSearchVisualTokens.getEndpointWithQueryParams( + `${baseEndpoint}/labels.json`, + filteredSearchInput.dataset.endpointQueryParams, + ); return AjaxCache.retrieve(labelsEndpoint) .then(FilteredSearchVisualTokens.preprocessLabel.bind(null, labelsEndpoint)) diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index 57a1fa107e5..8259133c95b 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import _ from 'underscore'; import glRegexp from './lib/utils/regexp'; import AjaxCache from './lib/utils/ajax_cache'; @@ -131,9 +132,8 @@ class GfmAutoComplete { callbacks: { ...this.getDefaultCallbacks(), matcher(flag, subtext) { - const relevantText = subtext.trim().split(/\s/).pop(); const regexp = new RegExp(`(?:[^${glRegexp.unicodeLetters}0-9:]|\n|^):([^:]*)$`, 'gi'); - const match = regexp.exec(relevantText); + const match = regexp.exec(subtext); return match && match.length ? match[1] : null; }, diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 6cf78bab6ad..86b34a6e360 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -1,5 +1,7 @@ /* eslint-disable func-names, no-underscore-dangle, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, prefer-rest-params, max-len, vars-on-top, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func, no-mixed-operators */ /* global fuzzaldrinPlus */ + +import $ from 'jquery'; import _ from 'underscore'; import fuzzaldrinPlus from 'fuzzaldrin-plus'; import axios from './lib/utils/axios_utils'; @@ -576,7 +578,7 @@ GitLabDropdown = (function() { for (var i = 0; i < html.length; i += 1) { var el = html[i]; - if (el instanceof jQuery) { + if (el instanceof $) { el = el.get(0); } diff --git a/app/assets/javascripts/gl_field_error.js b/app/assets/javascripts/gl_field_error.js index bd63f6f16f0..972b2252acb 100644 --- a/app/assets/javascripts/gl_field_error.js +++ b/app/assets/javascripts/gl_field_error.js @@ -1,3 +1,5 @@ +import $ from 'jquery'; + /** * This class overrides the browser's validation error bubbles, displaying custom * error messages for invalid fields instead. To begin validating any form, add the diff --git a/app/assets/javascripts/gl_field_errors.js b/app/assets/javascripts/gl_field_errors.js index 73bcbd93565..b9c51045b1d 100644 --- a/app/assets/javascripts/gl_field_errors.js +++ b/app/assets/javascripts/gl_field_errors.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import GlFieldError from './gl_field_error'; const customValidationFlag = 'gl-field-error-ignore'; diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js index 2d40856e038..9f5eba353d7 100644 --- a/app/assets/javascripts/gl_form.js +++ b/app/assets/javascripts/gl_form.js @@ -1,7 +1,8 @@ +import $ from 'jquery'; import autosize from 'autosize'; import GfmAutoComplete from './gfm_auto_complete'; import dropzoneInput from './dropzone_input'; -import textUtils from './lib/utils/text_markdown'; +import { addMarkdownListeners, removeMarkdownListeners } from './lib/utils/text_markdown'; export default class GLForm { constructor(form, enableGFM = false) { @@ -46,7 +47,7 @@ export default class GLForm { } // form and textarea event listeners this.addEventListeners(); - textUtils.init(this.form); + addMarkdownListeners(this.form); // hide discard button this.form.find('.js-note-discard').hide(); this.form.show(); @@ -85,7 +86,7 @@ export default class GLForm { clearEventListeners() { this.textarea.off('focus'); this.textarea.off('blur'); - textUtils.removeListeners(this.form); + removeMarkdownListeners(this.form); } addEventListeners() { diff --git a/app/assets/javascripts/gpg_badges.js b/app/assets/javascripts/gpg_badges.js index 6bf21f4f27d..502e3569321 100644 --- a/app/assets/javascripts/gpg_badges.js +++ b/app/assets/javascripts/gpg_badges.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import { parseQueryStringIntoObject } from '~/lib/utils/common_utils'; import axios from '~/lib/utils/axios_utils'; import flash from '~/flash'; diff --git a/app/assets/javascripts/group.js b/app/assets/javascripts/group.js index 7732edde1e7..4365305c168 100644 --- a/app/assets/javascripts/group.js +++ b/app/assets/javascripts/group.js @@ -1,3 +1,5 @@ +import $ from 'jquery'; + export default class Group { constructor() { this.groupPath = $('#group_path'); diff --git a/app/assets/javascripts/group_avatar.js b/app/assets/javascripts/group_avatar.js index 2168ff3a8ba..beaac61e887 100644 --- a/app/assets/javascripts/group_avatar.js +++ b/app/assets/javascripts/group_avatar.js @@ -1,3 +1,5 @@ +import $ from 'jquery'; + export default function groupAvatar() { $('.js-choose-group-avatar-button').on('click', function onClickGroupAvatar() { const form = $(this).closest('form'); diff --git a/app/assets/javascripts/group_label_subscription.js b/app/assets/javascripts/group_label_subscription.js index df9429b1e02..5648cb9a888 100644 --- a/app/assets/javascripts/group_label_subscription.js +++ b/app/assets/javascripts/group_label_subscription.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import axios from './lib/utils/axios_utils'; import flash from './flash'; import { __ } from './locale'; diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue index b8f0566f48c..63bb5832bd0 100644 --- a/app/assets/javascripts/groups/components/app.vue +++ b/app/assets/javascripts/groups/components/app.vue @@ -1,6 +1,7 @@ <script> /* global Flash */ +import $ from 'jquery'; import { s__ } from '~/locale'; import loadingIcon from '~/vue_shared/components/loading_icon.vue'; import modal from '~/vue_shared/components/modal.vue'; @@ -152,14 +153,14 @@ export default { showLeaveGroupModal(group, parentGroup) { this.targetGroup = group; this.targetParentGroup = parentGroup; - this.updateModal = true; + this.showModal = true; this.groupLeaveConfirmationMessage = s__(`GroupsTree|Are you sure you want to leave the "${group.fullName}" group?`); }, hideLeaveGroupModal() { - this.updateModal = false; + this.showModal = false; }, leaveGroup() { - this.updateModal = false; + this.showModal = false; this.targetGroup.isBeingRemoved = true; this.service.leaveGroup(this.targetGroup.leavePath) .then(res => res.json()) @@ -208,9 +209,9 @@ export default { :page-info="pageInfo" /> <modal - v-show="showModal" - :primary-button-label="__('Leave')" + v-if="showModal" kind="warning" + :primary-button-label="__('Leave')" :title="__('Are you sure?')" :text="groupLeaveConfirmationMessage" @cancel="hideLeaveGroupModal" diff --git a/app/assets/javascripts/groups/groups_filterable_list.js b/app/assets/javascripts/groups/groups_filterable_list.js index 31d56d15c23..e6db1746487 100644 --- a/app/assets/javascripts/groups/groups_filterable_list.js +++ b/app/assets/javascripts/groups/groups_filterable_list.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import FilterableList from '~/filterable_list'; import eventHub from './event_hub'; import { normalizeHeaders, getParameterByName } from '../lib/utils/common_utils'; diff --git a/app/assets/javascripts/groups/transfer_dropdown.js b/app/assets/javascripts/groups/transfer_dropdown.js index 85b7b08db4d..e0eb118ddf7 100644 --- a/app/assets/javascripts/groups/transfer_dropdown.js +++ b/app/assets/javascripts/groups/transfer_dropdown.js @@ -1,3 +1,5 @@ +import $ from 'jquery'; + export default class TransferDropdown { constructor() { this.groupDropdown = $('.js-groups-dropdown'); diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js index 12fc5f9b5c9..310f6fe06cf 100644 --- a/app/assets/javascripts/groups_select.js +++ b/app/assets/javascripts/groups_select.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import axios from './lib/utils/axios_utils'; import Api from './api'; import { normalizeHeaders } from './lib/utils/common_utils'; diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js index 33a352e158a..4ae3a714bee 100644 --- a/app/assets/javascripts/header.js +++ b/app/assets/javascripts/header.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import { highCountTrim } from '~/lib/utils/text_utility'; /** diff --git a/app/assets/javascripts/help/help.js b/app/assets/javascripts/help/help.js index d02477b19a2..f5333042bb8 100644 --- a/app/assets/javascripts/help/help.js +++ b/app/assets/javascripts/help/help.js @@ -1,4 +1,7 @@ // We will render the icons list here + +import $ from 'jquery'; + export default () => { if ($('#user-content-gitlab-icons').length > 0) { const $iconsHeader = $('#user-content-gitlab-icons'); diff --git a/app/assets/javascripts/how_to_merge.js b/app/assets/javascripts/how_to_merge.js index 12e6f24595a..bb734246584 100644 --- a/app/assets/javascripts/how_to_merge.js +++ b/app/assets/javascripts/how_to_merge.js @@ -1,3 +1,5 @@ +import $ from 'jquery'; + export default () => { const modal = $('#modal_merge_info'); diff --git a/app/assets/javascripts/ide/components/changed_file_icon.vue b/app/assets/javascripts/ide/components/changed_file_icon.vue new file mode 100644 index 00000000000..0c54c992e51 --- /dev/null +++ b/app/assets/javascripts/ide/components/changed_file_icon.vue @@ -0,0 +1,31 @@ +<script> + import icon from '~/vue_shared/components/icon.vue'; + + export default { + components: { + icon, + }, + props: { + file: { + type: Object, + required: true, + }, + }, + computed: { + changedIcon() { + return this.file.tempFile ? 'file-addition' : 'file-modified'; + }, + changedIconClass() { + return `multi-${this.changedIcon}`; + }, + }, + }; +</script> + +<template> + <icon + :name="changedIcon" + :size="12" + :css-classes="`ide-file-changed-icon ${changedIconClass}`" + /> +</template> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue new file mode 100644 index 00000000000..2cbd982af19 --- /dev/null +++ b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue @@ -0,0 +1,65 @@ +<script> + import { mapState } from 'vuex'; + import { sprintf, __ } from '~/locale'; + import * as consts from '../../stores/modules/commit/constants'; + import RadioGroup from './radio_group.vue'; + + export default { + components: { + RadioGroup, + }, + computed: { + ...mapState([ + 'currentBranchId', + ]), + newMergeRequestHelpText() { + return sprintf( + __('Creates a new branch from %{branchName} and re-directs to create a new merge request'), + { branchName: this.currentBranchId }, + ); + }, + commitToCurrentBranchText() { + return sprintf( + __('Commit to %{branchName} branch'), + { branchName: `<strong>${this.currentBranchId}</strong>` }, + false, + ); + }, + commitToNewBranchText() { + return sprintf( + __('Creates a new branch from %{branchName}'), + { branchName: this.currentBranchId }, + ); + }, + }, + commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH, + commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH, + commitToNewBranchMR: consts.COMMIT_TO_NEW_BRANCH_MR, + }; +</script> + +<template> + <div class="append-bottom-15 ide-commit-radios"> + <radio-group + :value="$options.commitToCurrentBranch" + :checked="true" + > + <span + v-html="commitToCurrentBranchText" + > + </span> + </radio-group> + <radio-group + :value="$options.commitToNewBranch" + :label="__('Create a new branch')" + :show-input="true" + :help-text="commitToNewBranchText" + /> + <radio-group + :value="$options.commitToNewBranchMR" + :label="__('Create a new branch and merge request')" + :show-input="true" + :help-text="newMergeRequestHelpText" + /> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue index a8459b011df..453208f3f19 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/list.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/list.vue @@ -1,6 +1,6 @@ <script> import { mapState } from 'vuex'; - import icon from '../../../vue_shared/components/icon.vue'; + import icon from '~/vue_shared/components/icon.vue'; import listItem from './list_item.vue'; import listCollapsed from './list_collapsed.vue'; @@ -26,6 +26,9 @@ 'currentBranchId', 'rightPanelCollapsed', ]), + isCommitInfoShown() { + return this.rightPanelCollapsed || this.fileList.length; + }, }, methods: { toggleCollapsed() { @@ -36,7 +39,11 @@ </script> <template> - <div class="multi-file-commit-list"> + <div + :class="{ + 'multi-file-commit-list': isCommitInfoShown + }" + > <list-collapsed v-if="rightPanelCollapsed" /> @@ -54,12 +61,6 @@ /> </li> </ul> - <div - v-else - class="help-block prepend-top-0" - > - No changes - </div> </template> </div> </template> 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 6a0262f271b..15918ac9631 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue @@ -1,6 +1,6 @@ <script> import { mapGetters } from 'vuex'; - import icon from '../../../vue_shared/components/icon.vue'; + import icon from '~/vue_shared/components/icon.vue'; export default { components: { 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 742f746e02f..18934af004a 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue @@ -1,5 +1,7 @@ <script> - import icon from '../../../vue_shared/components/icon.vue'; + import { mapActions } from 'vuex'; + import icon from '~/vue_shared/components/icon.vue'; + import router from '../../ide_router'; export default { components: { @@ -19,18 +21,40 @@ return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`; }, }, + methods: { + ...mapActions([ + 'discardFileChanges', + 'updateViewer', + ]), + openFileInEditor(file) { + this.updateViewer('diff'); + + router.push(`/project${file.url}`); + }, + }, }; </script> <template> <div class="multi-file-commit-list-item"> - <icon - :name="iconName" - :size="16" - :css-classes="iconClass" - /> - <span class="multi-file-commit-list-path"> - {{ file.path }} - </span> + <button + type="button" + class="multi-file-commit-list-path" + @click="openFileInEditor(file)"> + <span class="multi-file-commit-list-file-path"> + <icon + :name="iconName" + :size="16" + :css-classes="iconClass" + />{{ file.path }} + </span> + </button> + <button + type="button" + class="btn btn-blank multi-file-discard-btn" + @click="discardFileChanges(file.path)" + > + Discard + </button> </div> </template> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue new file mode 100644 index 00000000000..4310d762c78 --- /dev/null +++ b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue @@ -0,0 +1,94 @@ +<script> + import { mapActions, mapState, mapGetters } from 'vuex'; + import tooltip from '~/vue_shared/directives/tooltip'; + + export default { + directives: { + tooltip, + }, + props: { + value: { + type: String, + required: true, + }, + label: { + type: String, + required: false, + default: null, + }, + checked: { + type: Boolean, + required: false, + default: false, + }, + showInput: { + type: Boolean, + required: false, + default: false, + }, + helpText: { + type: String, + required: false, + default: null, + }, + }, + computed: { + ...mapState('commit', [ + 'commitAction', + ]), + ...mapGetters('commit', [ + 'newBranchName', + ]), + }, + methods: { + ...mapActions('commit', [ + 'updateCommitAction', + 'updateBranchName', + ]), + }, + }; +</script> + +<template> + <fieldset> + <label> + <input + type="radio" + name="commit-action" + :value="value" + @change="updateCommitAction($event.target.value)" + :checked="checked" + v-once + /> + <span class="prepend-left-10"> + <template v-if="label"> + {{ label }} + </template> + <slot v-else></slot> + <span + v-if="helpText" + v-tooltip + class="help-block inline" + :title="helpText" + > + <i + class="fa fa-question-circle" + aria-hidden="true" + > + </i> + </span> + </span> + </label> + <div + v-if="commitAction === value && showInput" + class="ide-commit-new-branch" + > + <input + type="text" + class="form-control" + :placeholder="newBranchName" + @input="updateBranchName($event.target.value)" + /> + </div> + </fieldset> +</template> diff --git a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue new file mode 100644 index 00000000000..170347881e0 --- /dev/null +++ b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue @@ -0,0 +1,91 @@ +<script> + import Icon from '~/vue_shared/components/icon.vue'; + + export default { + components: { + Icon, + }, + props: { + hasChanges: { + type: Boolean, + required: false, + default: false, + }, + viewer: { + type: String, + required: true, + }, + showShadow: { + type: Boolean, + required: true, + }, + }, + methods: { + changeMode(mode) { + this.$emit('click', mode); + }, + }, + }; +</script> + +<template> + <div + class="dropdown" + :class="{ + shadow: showShadow, + }" + > + <button + type="button" + class="btn btn-primary btn-sm" + :class="{ + 'btn-inverted': hasChanges, + }" + data-toggle="dropdown" + > + <template v-if="viewer === 'editor'"> + {{ __('Editing') }} + </template> + <template v-else> + {{ __('Reviewing') }} + </template> + <icon + name="angle-down" + :size="12" + css-classes="caret-down" + /> + </button> + <div class="dropdown-menu dropdown-menu-selectable dropdown-open-left"> + <ul> + <li> + <a + href="#" + @click.prevent="changeMode('editor')" + :class="{ + 'is-active': viewer === 'editor', + }" + > + <strong class="dropdown-menu-inner-title">{{ __('Editing') }}</strong> + <span class="dropdown-menu-inner-content"> + {{ __('View and edit lines') }} + </span> + </a> + </li> + <li> + <a + href="#" + @click.prevent="changeMode('diff')" + :class="{ + 'is-active': viewer === 'diff', + }" + > + <strong class="dropdown-menu-inner-title">{{ __('Reviewing') }}</strong> + <span class="dropdown-menu-inner-content"> + {{ __('Compare changes with the last commit') }} + </span> + </a> + </li> + </ul> + </div> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue index 89981ab2c65..015e750525a 100644 --- a/app/assets/javascripts/ide/components/ide.vue +++ b/app/assets/javascripts/ide/components/ide.vue @@ -5,7 +5,6 @@ import repoTabs from './repo_tabs.vue'; import repoFileButtons from './repo_file_buttons.vue'; import ideStatusBar from './ide_status_bar.vue'; - import repoPreview from './repo_preview.vue'; import repoEditor from './repo_editor.vue'; export default { @@ -16,27 +15,28 @@ repoFileButtons, ideStatusBar, repoEditor, - repoPreview, }, props: { emptyStateSvgPath: { type: String, required: true, }, + noChangesStateSvgPath: { + type: String, + required: true, + }, + committedStateSvgPath: { + type: String, + required: true, + }, }, computed: { - ...mapState([ - 'currentBlobView', - 'selectedFile', - ]), - ...mapGetters([ - 'changedFiles', - 'activeFile', - ]), + ...mapState(['changedFiles', 'openFiles', 'viewer']), + ...mapGetters(['activeFile', 'hasChanges']), }, mounted() { const returnValue = 'Are you sure you want to lose unsaved changes?'; - window.onbeforeunload = (e) => { + window.onbeforeunload = e => { if (!this.changedFiles.length) return undefined; Object.assign(e, { @@ -59,20 +59,29 @@ <template v-if="activeFile" > - <repo-tabs/> - <component + <repo-tabs + :files="openFiles" + :viewer="viewer" + :has-changes="hasChanges" + /> + <repo-editor class="multi-file-edit-pane-content" - :is="currentBlobView" + :file="activeFile" + /> + <repo-file-buttons + :file="activeFile" /> - <repo-file-buttons /> <ide-status-bar - :file="selectedFile" + :file="activeFile" /> </template> <template v-else > - <div class="ide-empty-state"> + <div + v-once + class="ide-empty-state" + > <div class="row js-empty-state"> <div class="col-xs-12"> <div class="svg-content svg-250"> @@ -94,6 +103,9 @@ </div> </template> </div> - <ide-contextbar/> + <ide-contextbar + :no-changes-state-svg-path="noChangesStateSvgPath" + :committed-state-svg-path="committedStateSvgPath" + /> </div> </template> diff --git a/app/assets/javascripts/ide/components/ide_context_bar.vue b/app/assets/javascripts/ide/components/ide_context_bar.vue index 9d933b8891d..79a83b47994 100644 --- a/app/assets/javascripts/ide/components/ide_context_bar.vue +++ b/app/assets/javascripts/ide/components/ide_context_bar.vue @@ -1,70 +1,46 @@ <script> - import { mapGetters, mapState, mapActions } from 'vuex'; - import icon from '~/vue_shared/components/icon.vue'; - import panelResizer from '~/vue_shared/components/panel_resizer.vue'; - import repoCommitSection from './repo_commit_section.vue'; +import { mapActions, mapGetters, mapState } from 'vuex'; +import icon from '~/vue_shared/components/icon.vue'; +import panelResizer from '~/vue_shared/components/panel_resizer.vue'; +import repoCommitSection from './repo_commit_section.vue'; +import ResizablePanel from './resizable_panel.vue'; - export default { - components: { - repoCommitSection, - icon, - panelResizer, +export default { + components: { + repoCommitSection, + icon, + panelResizer, + ResizablePanel, + }, + props: { + noChangesStateSvgPath: { + type: String, + required: true, }, - data() { - return { - width: 290, - }; + committedStateSvgPath: { + type: String, + required: true, }, - computed: { - ...mapState([ - 'rightPanelCollapsed', - ]), - ...mapGetters([ - 'changedFiles', - ]), - currentIcon() { - return this.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right'; - }, - maxSize() { - return window.innerWidth / 2; - }, - panelStyle() { - if (!this.rightPanelCollapsed) { - return { width: `${this.width}px` }; - } - return {}; - }, - }, - methods: { - ...mapActions([ - 'setPanelCollapsedStatus', - 'setResizingStatus', - ]), - toggleCollapsed() { - this.setPanelCollapsedStatus({ - side: 'right', - collapsed: !this.rightPanelCollapsed, - }); - }, - resizingStarted() { - this.setResizingStatus(true); - }, - resizingEnded() { - this.setResizingStatus(false); - }, - }, - }; + }, + computed: { + ...mapState(['changedFiles', 'rightPanelCollapsed']), + ...mapGetters(['currentIcon']), + }, + methods: { + ...mapActions(['setPanelCollapsedStatus']), + }, +}; </script> <template> - <div - class="multi-file-commit-panel" - :class="{ - 'is-collapsed': rightPanelCollapsed, - }" - :style="panelStyle" + <resizable-panel + :collapsible="true" + :initial-width="340" + side="right" > - <div class="multi-file-commit-panel-section"> + <div + class="multi-file-commit-panel-section" + > <header class="multi-file-commit-panel-header" :class="{ @@ -75,16 +51,23 @@ class="multi-file-commit-panel-header-title" v-if="!rightPanelCollapsed" > - <icon - name="list-bulleted" - :size="18" - /> - Staged + <div + v-if="changedFiles.length" + > + <icon + name="list-bulleted" + :size="18" + /> + Staged + </div> </div> <button type="button" class="btn btn-transparent multi-file-commit-panel-collapse-btn" - @click="toggleCollapsed" + @click.stop="setPanelCollapsedStatus({ + side: 'right', + collapsed: !rightPanelCollapsed, + })" > <icon :name="currentIcon" @@ -92,17 +75,10 @@ /> </button> </header> - <repo-commit-section /> + <repo-commit-section + :no-changes-state-svg-path="noChangesStateSvgPath" + :committed-state-svg-path="committedStateSvgPath" + /> </div> - <panel-resizer - :size.sync="width" - :enabled="!rightPanelCollapsed" - :start-size="290" - :min-size="200" - :max-size="maxSize" - @resize-start="resizingStarted" - @resize-end="resizingEnded" - side="left" - /> - </div> + </resizable-panel> </template> diff --git a/app/assets/javascripts/ide/components/ide_external_links.vue b/app/assets/javascripts/ide/components/ide_external_links.vue new file mode 100644 index 00000000000..c6f6e0d2348 --- /dev/null +++ b/app/assets/javascripts/ide/components/ide_external_links.vue @@ -0,0 +1,43 @@ +<script> +import icon from '~/vue_shared/components/icon.vue'; + +export default { + components: { + icon, + }, + props: { + projectUrl: { + type: String, + required: true, + }, + }, + computed: { + goBackUrl() { + return document.referrer || this.projectUrl; + }, + }, +}; +</script> + +<template> + <nav + class="ide-external-links" + v-once + > + <p> + <a + :href="goBackUrl" + class="ide-sidebar-link" + > + <icon + :size="16" + class="append-right-8" + name="go-back" + /> + <span class="ide-external-links-text"> + {{ s__('Go back') }} + </span> + </a> + </p> + </nav> +</template> diff --git a/app/assets/javascripts/ide/components/ide_project_branches_tree.vue b/app/assets/javascripts/ide/components/ide_project_branches_tree.vue index 2fbff2bd789..eb2749e6151 100644 --- a/app/assets/javascripts/ide/components/ide_project_branches_tree.vue +++ b/app/assets/javascripts/ide/components/ide_project_branches_tree.vue @@ -1,31 +1,31 @@ <script> -import icon from '~/vue_shared/components/icon.vue'; -import repoTree from './ide_repo_tree.vue'; -import newDropdown from './new_dropdown/index.vue'; + import icon from '~/vue_shared/components/icon.vue'; + import repoTree from './ide_repo_tree.vue'; + import newDropdown from './new_dropdown/index.vue'; -export default { - components: { - repoTree, - icon, - newDropdown, - }, - props: { - projectId: { - type: String, - required: true, + export default { + components: { + repoTree, + icon, + newDropdown, }, - branch: { - type: Object, - required: true, + props: { + projectId: { + type: String, + required: true, + }, + branch: { + type: Object, + required: true, + }, }, - }, -}; + }; </script> <template> <div class="branch-container"> <div class="branch-header"> - <div class="branch-header-title"> + <div class="branch-header-title str-truncated ref-name"> <icon name="branch" :size="12" @@ -40,8 +40,8 @@ export default { /> </div> </div> - <div> - <repo-tree :tree-id="branch.treeId" /> - </div> + <repo-tree + :tree="branch.tree" + /> </div> </template> diff --git a/app/assets/javascripts/ide/components/ide_project_tree.vue b/app/assets/javascripts/ide/components/ide_project_tree.vue index 32bf7175c88..a6f40286ac1 100644 --- a/app/assets/javascripts/ide/components/ide_project_tree.vue +++ b/app/assets/javascripts/ide/components/ide_project_tree.vue @@ -1,11 +1,15 @@ <script> -import projectAvatarImage from '~/vue_shared/components/project_avatar/image.vue'; -import branchesTree from './ide_project_branches_tree.vue'; +import ProjectAvatarImage from '~/vue_shared/components/project_avatar/image.vue'; +import Identicon from '../../vue_shared/components/identicon.vue'; +import BranchesTree from './ide_project_branches_tree.vue'; +import ExternalLinks from './ide_external_links.vue'; export default { components: { - branchesTree, - projectAvatarImage, + BranchesTree, + ExternalLinks, + ProjectAvatarImage, + Identicon, }, props: { project: { @@ -23,7 +27,10 @@ export default { :title="project.name" :href="project.web_url" > - <div class="avatar-container s40 project-avatar"> + <div + v-if="project.avatar_url" + class="avatar-container s40 project-avatar" + > <project-avatar-image class="avatar-container project-avatar" :link-href="project.path" @@ -32,11 +39,20 @@ export default { :img-size="40" /> </div> + <identicon + v-else + size-class="s40" + :entity-id="project.id" + :entity-name="project.name" + /> <div class="sidebar-context-title"> {{ project.name }} </div> </a> </div> + <external-links + :project-url="project.web_url" + /> <div class="multi-file-commit-panel-inner-scroll"> <branches-tree v-for="branch in project.branches" diff --git a/app/assets/javascripts/ide/components/ide_repo_tree.vue b/app/assets/javascripts/ide/components/ide_repo_tree.vue index 4a324264992..e6af88e04bc 100644 --- a/app/assets/javascripts/ide/components/ide_repo_tree.vue +++ b/app/assets/javascripts/ide/components/ide_repo_tree.vue @@ -1,74 +1,41 @@ <script> -import { mapState } from 'vuex'; -import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; -import repoPreviousDirectory from './repo_prev_directory.vue'; -import repoFile from './repo_file.vue'; -import { treeList } from '../stores/utils'; +import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; +import RepoFile from './repo_file.vue'; export default { components: { - repoPreviousDirectory, - repoFile, - skeletonLoadingContainer, + RepoFile, + SkeletonLoadingContainer, }, props: { - treeId: { - type: String, + tree: { + type: Object, required: true, }, }, - computed: { - ...mapState([ - 'trees', - 'isRoot', - ]), - ...mapState({ - projectName(state) { - return state.project.name; - }, - }), - fetchedList() { - return treeList(this.$store.state, this.treeId); - }, - hasPreviousDirectory() { - return !this.isRoot && this.fetchedList.length; - }, - showLoading() { - if (this.trees[this.treeId]) { - return this.trees[this.treeId].loading; - } - return true; - }, - }, }; </script> <template> - <div> - <div class="ide-file-list"> - <table class="table"> - <tbody - v-if="treeId" - > - <repo-previous-directory - v-if="hasPreviousDirectory" - /> - <template v-if="showLoading"> - <div - class="multi-file-loading-container" - v-for="n in 3" - :key="n" - > - <skeleton-loading-container /> - </div> - </template> - <repo-file - v-for="file in fetchedList" - :key="file.key" - :file="file" - /> - </tbody> - </table> - </div> + <div + class="ide-file-list" + > + <template v-if="tree.loading"> + <div + class="multi-file-loading-container" + v-for="n in 3" + :key="n" + > + <skeleton-loading-container /> + </div> + </template> + <template v-else> + <repo-file + v-for="file in tree.tree" + :key="file.key" + :file="file" + :level="0" + /> + </template> </div> </template> diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue index 18b5059a17f..8cf1ccb4fce 100644 --- a/app/assets/javascripts/ide/components/ide_side_bar.vue +++ b/app/assets/javascripts/ide/components/ide_side_bar.vue @@ -1,9 +1,10 @@ <script> - import { mapState, mapActions } from 'vuex'; + import { mapState, mapGetters } from 'vuex'; import icon from '~/vue_shared/components/icon.vue'; import panelResizer from '~/vue_shared/components/panel_resizer.vue'; import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; import projectTree from './ide_project_tree.vue'; + import ResizablePanel from './resizable_panel.vue'; export default { components: { @@ -11,65 +12,27 @@ icon, panelResizer, skeletonLoadingContainer, - }, - data() { - return { - width: 290, - }; + ResizablePanel, }, computed: { ...mapState([ 'loading', - 'projects', - 'leftPanelCollapsed', ]), - currentIcon() { - return this.leftPanelCollapsed ? 'angle-double-right' : 'angle-double-left'; - }, - maxSize() { - return window.innerWidth / 2; - }, - panelStyle() { - if (!this.leftPanelCollapsed) { - return { width: `${this.width}px` }; - } - return {}; - }, - showLoading() { - return this.loading; - }, - }, - methods: { - ...mapActions([ - 'setPanelCollapsedStatus', - 'setResizingStatus', + ...mapGetters([ + 'projectsWithTrees', ]), - toggleCollapsed() { - this.setPanelCollapsedStatus({ - side: 'left', - collapsed: !this.leftPanelCollapsed, - }); - }, - resizingStarted() { - this.setResizingStatus(true); - }, - resizingEnded() { - this.setResizingStatus(false); - }, }, }; </script> <template> - <div - class="multi-file-commit-panel" - :class="{ - 'is-collapsed': leftPanelCollapsed, - }" - :style="panelStyle" + <resizable-panel + :collapsible="false" + :initial-width="290" + side="left" > <div class="multi-file-commit-panel-inner"> - <template v-if="showLoading"> + <template v-if="loading"> <div class="multi-file-loading-container" v-for="n in 3" @@ -79,36 +42,10 @@ </div> </template> <project-tree - v-for="project in projects" + v-for="project in projectsWithTrees" :key="project.id" :project="project" /> </div> - <button - type="button" - class="btn btn-transparent left-collapse-btn" - @click="toggleCollapsed" - > - <icon - :name="currentIcon" - :size="18" - /> - <span - v-if="!leftPanelCollapsed" - class="collapse-text" - > - Collapse sidebar - </span> - </button> - <panel-resizer - :size.sync="width" - :enabled="!leftPanelCollapsed" - :start-size="290" - :min-size="200" - :max-size="maxSize" - @resize-start="resizingStarted" - @resize-end="resizingEnded" - side="right" - /> - </div> + </resizable-panel> </template> diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue index 97ae64b206d..9c386896448 100644 --- a/app/assets/javascripts/ide/components/ide_status_bar.vue +++ b/app/assets/javascripts/ide/components/ide_status_bar.vue @@ -1,5 +1,4 @@ <script> - import { mapState } from 'vuex'; import icon from '~/vue_shared/components/icon.vue'; import tooltip from '~/vue_shared/directives/tooltip'; import timeAgoMixin from '~/vue_shared/mixins/timeago'; @@ -20,47 +19,42 @@ required: true, }, }, - computed: { - ...mapState([ - 'selectedFile', - ]), - }, }; </script> <template> <div class="ide-status-bar"> - <div> + <div class="ref-name"> <icon name="branch" :size="12" /> - {{ selectedFile.branchId }} + {{ file.branchId }} </div> <div> - <div v-if="selectedFile.lastCommit && selectedFile.lastCommit.id"> + <div v-if="file.lastCommit && file.lastCommit.id"> Last commit: <a v-tooltip - :title="selectedFile.lastCommit.message" - :href="selectedFile.lastCommit.url" + :title="file.lastCommit.message" + :href="file.lastCommit.url" > - {{ timeFormated(selectedFile.lastCommit.updatedAt) }} by - {{ selectedFile.lastCommit.author }} + {{ timeFormated(file.lastCommit.updatedAt) }} by + {{ file.lastCommit.author }} </a> </div> </div> <div class="text-right"> - {{ selectedFile.name }} + {{ file.name }} </div> <div class="text-right"> - {{ selectedFile.eol }} + {{ file.eol }} </div> <div class="text-right"> {{ file.editorRow }}:{{ file.editorColumn }} </div> <div class="text-right"> - {{ selectedFile.fileLanguage }} + {{ file.fileLanguage }} </div> </div> </template> diff --git a/app/assets/javascripts/ide/components/new_branch_form.vue b/app/assets/javascripts/ide/components/new_branch_form.vue deleted file mode 100644 index 1e8d5bb6453..00000000000 --- a/app/assets/javascripts/ide/components/new_branch_form.vue +++ /dev/null @@ -1,108 +0,0 @@ -<script> - import { mapState, mapActions } from 'vuex'; - import flash, { hideFlash } from '~/flash'; - import loadingIcon from '~/vue_shared/components/loading_icon.vue'; - - export default { - components: { - loadingIcon, - }, - data() { - return { - branchName: '', - loading: false, - }; - }, - computed: { - ...mapState([ - 'currentBranch', - ]), - btnDisabled() { - return this.loading || this.branchName === ''; - }, - }, - created() { - // Dropdown is outside of Vue instance & is controlled by Bootstrap - this.$dropdown = $('.git-revision-dropdown'); - - // text element is outside Vue app - this.dropdownText = document.querySelector('.project-refs-form .dropdown-toggle-text'); - }, - methods: { - ...mapActions([ - 'createNewBranch', - ]), - toggleDropdown() { - this.$dropdown.dropdown('toggle'); - }, - submitNewBranch() { - // need to query as the element is appended outside of Vue - const flashEl = this.$refs.flashContainer.querySelector('.flash-alert'); - - this.loading = true; - - if (flashEl) { - hideFlash(flashEl, false); - } - - this.createNewBranch(this.branchName) - .then(() => { - this.loading = false; - this.branchName = ''; - - if (this.dropdownText) { - this.dropdownText.textContent = this.currentBranchId; - } - - this.toggleDropdown(); - }) - .catch(res => res.json().then((data) => { - this.loading = false; - flash(data.message, 'alert', this.$el); - })); - }, - }, - }; -</script> - -<template> - <div> - <div - class="flash-container" - ref="flashContainer" - > - </div> - <p> - Create from: - <code>{{ currentBranch }}</code> - </p> - <input - class="form-control js-new-branch-name" - type="text" - placeholder="Name new branch" - v-model="branchName" - @keyup.enter.stop.prevent="submitNewBranch" - /> - <div class="prepend-top-default clearfix"> - <button - type="button" - class="btn btn-primary pull-left" - :disabled="btnDisabled" - @click.stop.prevent="submitNewBranch" - > - <loading-icon - v-if="loading" - :inline="true" - /> - <span>Create</span> - </button> - <button - type="button" - class="btn btn-default pull-right" - @click.stop.prevent="toggleDropdown" - > - Cancel - </button> - </div> - </div> -</template> diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue index ef653357f5f..769e9b79cad 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/index.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue @@ -1,7 +1,8 @@ <script> + import { mapActions } from 'vuex'; + import icon from '~/vue_shared/components/icon.vue'; import newModal from './modal.vue'; import upload from './upload.vue'; - import icon from '../../../vue_shared/components/icon.vue'; export default { components: { @@ -18,37 +19,46 @@ type: String, required: true, }, - parent: { - type: Object, - default: null, - }, }, data() { return { openModal: false, modalType: '', + dropdownOpen: false, }; }, methods: { + ...mapActions([ + 'createTempEntry', + ]), createNewItem(type) { this.modalType = type; this.openModal = true; + this.dropdownOpen = false; }, hideModal() { this.openModal = false; }, + openDropdown() { + this.dropdownOpen = !this.dropdownOpen; + }, }, }; </script> <template> - <div class="repo-new-btn pull-right"> - <div class="dropdown"> + <div class="ide-new-btn"> + <div + class="dropdown" + :class="{ + open: dropdownOpen, + }" + > <button type="button" class="btn btn-sm btn-default dropdown-toggle add-to-tree" - data-toggle="dropdown" aria-label="Create new file or directory" + @click.stop="openDropdown()" > <icon name="plus" @@ -66,7 +76,7 @@ <a href="#" role="button" - @click.prevent="createNewItem('blob')" + @click.stop.prevent="createNewItem('blob')" > {{ __('New file') }} </a> @@ -75,14 +85,14 @@ <upload :branch-id="branch" :path="path" - :parent="parent" + @create="createTempEntry" /> </li> <li> <a href="#" role="button" - @click.prevent="createNewItem('tree')" + @click.stop.prevent="createNewItem('tree')" > {{ __('New directory') }} </a> @@ -94,8 +104,8 @@ :type="modalType" :branch-id="branch" :path="path" - :parent="parent" @hide="hideModal" + @create="createTempEntry" /> </div> </template> diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue index 36cd825c6dd..5723891d130 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue @@ -1,7 +1,6 @@ <script> - import { mapActions, mapState } from 'vuex'; - import { __ } from '../../../locale'; - import modal from '../../../vue_shared/components/modal.vue'; + import { __ } from '~/locale'; + import modal from '~/vue_shared/components/modal.vue'; export default { components: { @@ -12,10 +11,6 @@ type: String, required: true, }, - parent: { - type: Object, - default: null, - }, type: { type: String, required: true, @@ -31,9 +26,6 @@ }; }, computed: { - ...mapState([ - 'currentProjectId', - ]), modalTitle() { if (this.type === 'tree') { return __('Create new directory'); @@ -60,15 +52,10 @@ this.$refs.fieldName.focus(); }, methods: { - ...mapActions([ - 'createTempEntry', - ]), createEntryInStore() { - this.createTempEntry({ - projectId: this.currentProjectId, + this.$emit('create', { branchId: this.branchId, - parent: this.parent, - name: this.entryName.replace(new RegExp(`^${this.path}/`), ''), + name: this.entryName, type: this.type, }); diff --git a/app/assets/javascripts/ide/components/new_dropdown/upload.vue b/app/assets/javascripts/ide/components/new_dropdown/upload.vue index 6244737fa43..c165af5ce52 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/upload.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/upload.vue @@ -1,23 +1,16 @@ <script> - import { mapActions, mapState } from 'vuex'; - export default { props: { branchId: { type: String, required: true, }, - parent: { - type: Object, - default: null, + path: { + type: String, + required: false, + default: '', }, }, - computed: { - ...mapState([ - 'trees', - 'currentProjectId', - ]), - }, mounted() { this.$refs.fileUpload.addEventListener('change', this.openFile); }, @@ -25,9 +18,6 @@ this.$refs.fileUpload.removeEventListener('change', this.openFile); }, methods: { - ...mapActions([ - 'createTempEntry', - ]), createFile(target, file, isText) { const { name } = file; let { result } = target; @@ -36,11 +26,9 @@ result = result.split('base64,')[1]; } - this.createTempEntry({ - name, - projectId: this.currentProjectId, + this.$emit('create', { + name: `${(this.path ? `${this.path}/` : '')}${name}`, branchId: this.branchId, - parent: this.parent, type: 'blob', content: result, base64: !isText, @@ -73,7 +61,7 @@ <a href="#" role="button" - @click.prevent="startFileUpload" + @click.stop.prevent="startFileUpload" > {{ __('Upload file') }} </a> diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue index 37f2cf30a29..d772cab2d0e 100644 --- a/app/assets/javascripts/ide/components/repo_commit_section.vue +++ b/app/assets/javascripts/ide/components/repo_commit_section.vue @@ -1,171 +1,174 @@ <script> -import { mapGetters, mapState, mapActions } from 'vuex'; +import { mapState, mapActions, mapGetters } from 'vuex'; import tooltip from '~/vue_shared/directives/tooltip'; import icon from '~/vue_shared/components/icon.vue'; import modal from '~/vue_shared/components/modal.vue'; +import LoadingButton from '~/vue_shared/components/loading_button.vue'; import commitFilesList from './commit_sidebar/list.vue'; +import * as consts from '../stores/modules/commit/constants'; +import Actions from './commit_sidebar/actions.vue'; export default { components: { modal, icon, commitFilesList, + Actions, + LoadingButton, }, directives: { tooltip, }, - data() { - return { - showNewBranchModal: false, - submitCommitsLoading: false, - startNewMR: false, - commitMessage: '', - }; + props: { + noChangesStateSvgPath: { + type: String, + required: true, + }, + committedStateSvgPath: { + type: String, + required: true, + }, }, computed: { ...mapState([ 'currentProjectId', 'currentBranchId', 'rightPanelCollapsed', - ]), - ...mapGetters([ + 'lastCommitMsg', 'changedFiles', ]), - commitButtonDisabled() { - return this.commitMessage === '' || this.submitCommitsLoading || !this.changedFiles.length; - }, - commitMessageCount() { - return this.commitMessage.length; + ...mapState('commit', [ + 'commitMessage', + 'submitCommitLoading', + ]), + ...mapGetters('commit', [ + 'commitButtonDisabled', + 'discardDraftButtonDisabled', + 'branchName', + ]), + statusSvg() { + return this.lastCommitMsg ? this.committedStateSvgPath : this.noChangesStateSvgPath; }, }, methods: { ...mapActions([ - 'checkCommitStatus', - 'commitChanges', - 'getTreeData', 'setPanelCollapsedStatus', ]), - makeCommit(newBranch = false) { - const createNewBranch = newBranch || this.startNewMR; - - const payload = { - branch: createNewBranch ? - `${this.currentBranchId}-${new Date().getTime().toString()}` : - this.currentBranchId, - commit_message: this.commitMessage, - actions: this.changedFiles.map(f => ({ - action: f.tempFile ? 'create' : 'update', - file_path: f.path, - content: f.content, - encoding: f.base64 ? 'base64' : 'text', - })), - start_branch: createNewBranch ? this.currentBranchId : undefined, - }; - - this.showNewBranchModal = false; - this.submitCommitsLoading = true; - - this.commitChanges({ payload, newMr: this.startNewMR }) - .then(() => { - this.submitCommitsLoading = false; - this.commitMessage = ''; - this.startNewMR = false; - }) - .catch(() => { - this.submitCommitsLoading = false; - }); - }, - tryCommit() { - this.submitCommitsLoading = true; - - this.checkCommitStatus() - .then((branchChanged) => { - if (branchChanged) { - this.showNewBranchModal = true; - } else { - this.makeCommit(); - } - }) - .catch(() => { - this.submitCommitsLoading = false; - }); - }, + ...mapActions('commit', [ + 'updateCommitMessage', + 'discardDraft', + 'commitChanges', + 'updateCommitAction', + ]), toggleCollapsed() { this.setPanelCollapsedStatus({ side: 'right', collapsed: !this.rightPanelCollapsed, }); }, + forceCreateNewBranch() { + return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH) + .then(() => this.commitChanges()); + }, }, }; </script> <template> - <div class="multi-file-commit-panel-section"> + <div + class="multi-file-commit-panel-section" + :class="{ + 'multi-file-commit-empty-state-container': !changedFiles.length + }" + > <modal - v-if="showNewBranchModal" + id="ide-create-branch-modal" :primary-button-label="__('Create new branch')" - kind="primary" + kind="success" :title="__('Branch has changed')" - :text="__(`This branch has changed since -you started editing. Would you like to create a new branch?`)" - @cancel="showNewBranchModal = false" - @submit="makeCommit(true)" - /> + @submit="forceCreateNewBranch" + > + <template slot="body"> + {{ __(`This branch has changed since you started editing. + Would you like to create a new branch?`) }} + </template> + </modal> <commit-files-list title="Staged" :file-list="changedFiles" :collapsed="rightPanelCollapsed" @toggleCollapsed="toggleCollapsed" /> - <form - class="form-horizontal multi-file-commit-form" - @submit.prevent="tryCommit" - v-if="!rightPanelCollapsed" + <template + v-if="changedFiles.length" > - <div class="multi-file-commit-fieldset"> - <textarea - class="form-control multi-file-commit-message" - name="commit-message" - v-model="commitMessage" - placeholder="Commit message" - > - </textarea> - </div> - <div class="multi-file-commit-fieldset"> - <label - v-tooltip - title="Create a new merge request with these changes" - data-container="body" - data-placement="top" - > - <input - type="checkbox" - v-model="startNewMR" + <form + class="form-horizontal multi-file-commit-form" + @submit.prevent.stop="commitChanges" + v-if="!rightPanelCollapsed" + > + <div class="multi-file-commit-fieldset"> + <textarea + class="form-control multi-file-commit-message" + name="commit-message" + :value="commitMessage" + :placeholder="__('Write a commit message...')" + @input="updateCommitMessage($event.target.value)" + > + </textarea> + </div> + <div class="clearfix prepend-top-15"> + <actions /> + <loading-button + :loading="submitCommitLoading" + :disabled="commitButtonDisabled" + container-class="btn btn-success btn-sm pull-left" + :label="__('Commit')" + @click="commitChanges" /> - Merge Request - </label> - <button - type="submit" - :disabled="commitButtonDisabled" - class="btn btn-default btn-sm append-right-10 prepend-left-10" - :class="{ disabled: submitCommitsLoading }" - > - <i - v-if="submitCommitsLoading" - class="js-commit-loading-icon fa fa-spinner fa-spin" - aria-hidden="true" - aria-label="loading" + <button + v-if="!discardDraftButtonDisabled" + type="button" + class="btn btn-default btn-sm pull-right" + @click="discardDraft" > - </i> - Commit - </button> + {{ __('Discard draft') }} + </button> + </div> + </form> + </template> + <div + v-else-if="!rightPanelCollapsed" + class="row js-empty-state" + > + <div class="col-xs-10 col-xs-offset-1"> + <div class="svg-content svg-80"> + <img :src="statusSvg" /> + </div> + </div> + <div class="col-xs-10 col-xs-offset-1"> + <div + class="text-content text-center" + v-if="!lastCommitMsg" + > + <h4> + {{ __('No changes') }} + </h4> + <p> + {{ __('Edit files in the editor and commit changes here') }} + </p> + </div> <div - class="multi-file-commit-message-count" + class="text-content text-center" + v-else > - {{ commitMessageCount }} + <h4> + {{ __('All changes are committed') }} + </h4> + <p v-html="lastCommitMsg"> + </p> </div> </div> - </form> + </div> </div> </template> diff --git a/app/assets/javascripts/ide/components/repo_edit_button.vue b/app/assets/javascripts/ide/components/repo_edit_button.vue deleted file mode 100644 index fe4320731d9..00000000000 --- a/app/assets/javascripts/ide/components/repo_edit_button.vue +++ /dev/null @@ -1,57 +0,0 @@ -<script> -import { mapGetters, mapActions, mapState } from 'vuex'; -import modal from '~/vue_shared/components/modal.vue'; - -export default { - components: { - modal, - }, - computed: { - ...mapState([ - 'editMode', - 'discardPopupOpen', - ]), - ...mapGetters([ - 'canEditFile', - ]), - buttonLabel() { - return this.editMode ? this.__('Cancel edit') : this.__('Edit'); - }, - }, - methods: { - ...mapActions([ - 'toggleEditMode', - 'closeDiscardPopup', - ]), - }, -}; -</script> - -<template> - <div class="editable-mode"> - <button - v-if="canEditFile" - class="btn btn-default" - type="button" - @click.prevent="toggleEditMode()"> - <i - v-if="!editMode" - class="fa fa-pencil" - aria-hidden="true"> - </i> - <span> - {{ buttonLabel }} - </span> - </button> - <modal - v-if="discardPopupOpen" - class="text-left" - :primary-button-label="__('Discard changes')" - kind="warning" - :title="__('Are you sure?')" - :text="__('Are you sure you want to discard your changes?')" - @cancel="closeDiscardPopup" - @submit="toggleEditMode(true)" - /> - </div> -</template> diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index f31cc12339b..e73d1ce839f 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -1,28 +1,31 @@ <script> /* global monaco */ -import { mapState, mapGetters, mapActions } from 'vuex'; +import { mapState, mapActions } from 'vuex'; import flash from '~/flash'; import monacoLoader from '../monaco_loader'; import Editor from '../lib/editor'; export default { + props: { + file: { + type: Object, + required: true, + }, + }, computed: { - ...mapGetters([ - 'activeFile', - 'activeFileExtension', - ]), ...mapState([ 'leftPanelCollapsed', 'rightPanelCollapsed', - 'panelResizing', + 'viewer', + 'delayViewerUpdated', ]), shouldHideEditor() { - return this.activeFile.binary && !this.activeFile.raw; + return this.file && this.file.binary && !this.file.raw; }, }, watch: { - activeFile(oldVal, newVal) { - if (newVal && !newVal.active) { + file(oldVal, newVal) { + if (newVal.path !== this.file.path) { this.initMonaco(); } }, @@ -32,10 +35,8 @@ export default { rightPanelCollapsed() { this.editor.updateDimensions(); }, - panelResizing(isResizing) { - if (isResizing === false) { - this.editor.updateDimensions(); - } + viewer() { + this.createEditorInstance(); }, }, beforeDestroy() { @@ -59,34 +60,58 @@ export default { 'setFileLanguage', 'setEditorPosition', 'setFileEOL', + 'updateViewer', + 'updateDelayViewerUpdated', ]), initMonaco() { if (this.shouldHideEditor) return; this.editor.clearEditor(); - this.getRawFileData(this.activeFile) + this.getRawFileData(this.file) .then(() => { - this.editor.createInstance(this.$refs.editor); + const viewerPromise = this.delayViewerUpdated ? this.updateViewer('editor') : Promise.resolve(); + + return viewerPromise; + }) + .then(() => { + this.updateDelayViewerUpdated(false); + this.createEditorInstance(); }) - .then(() => this.setupEditor()) .catch((err) => { flash('Error setting up monaco. Please try again.', 'alert', document, null, false, true); throw err; }); }, + createEditorInstance() { + this.editor.dispose(); + + this.$nextTick(() => { + if (this.viewer === 'editor') { + this.editor.createInstance(this.$refs.editor); + } else { + this.editor.createDiffInstance(this.$refs.editor); + } + + this.setupEditor(); + }); + }, setupEditor() { - if (!this.activeFile) return; + if (!this.file || !this.editor.instance) return; - const model = this.editor.createModel(this.activeFile); + this.model = this.editor.createModel(this.file); - this.editor.attachModel(model); + this.editor.attachModel(this.model); - model.onChange((m) => { - this.changeFileContent({ - file: this.activeFile, - content: m.getValue(), - }); + this.model.onChange((model) => { + const { file } = model; + + if (file.active) { + this.changeFileContent({ + path: file.path, + content: model.getModel().getValue(), + }); + } }); // Handle Cursor Position @@ -98,18 +123,18 @@ export default { }); this.editor.setPosition({ - lineNumber: this.activeFile.editorRow, - column: this.activeFile.editorColumn, + lineNumber: this.file.editorRow, + column: this.file.editorColumn, }); // Handle File Language this.setFileLanguage({ - fileLanguage: model.language, + fileLanguage: this.model.language, }); // Get File eol this.setFileEOL({ - eol: model.eol, + eol: this.model.eol, }); }, }, @@ -123,7 +148,7 @@ export default { > <div v-if="shouldHideEditor" - v-html="activeFile.html" + v-html="file.html" > </div> <div diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue index cbbab765e1c..297b9c2628f 100644 --- a/app/assets/javascripts/ide/components/repo_file.vue +++ b/app/assets/javascripts/ide/components/repo_file.vue @@ -1,165 +1,128 @@ <script> - import { mapState } from 'vuex'; - import timeAgoMixin from '~/vue_shared/mixins/timeago'; - import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; - import fileIcon from '~/vue_shared/components/file_icon.vue'; - import newDropdown from './new_dropdown/index.vue'; +import { mapActions } from 'vuex'; +import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; +import fileIcon from '~/vue_shared/components/file_icon.vue'; +import router from '../ide_router'; +import newDropdown from './new_dropdown/index.vue'; +import fileStatusIcon from './repo_file_status_icon.vue'; +import changedFileIcon from './changed_file_icon.vue'; - export default { - components: { - skeletonLoadingContainer, - newDropdown, - fileIcon, +export default { + name: 'RepoFile', + components: { + skeletonLoadingContainer, + newDropdown, + fileStatusIcon, + fileIcon, + changedFileIcon, + }, + props: { + file: { + type: Object, + required: true, }, - mixins: [ - timeAgoMixin, - ], - props: { - file: { - type: Object, - required: true, - }, - showExtraColumns: { - type: Boolean, - default: false, - }, + level: { + type: Number, + required: true, }, - computed: { - ...mapState([ - 'leftPanelCollapsed', - ]), - isSubmodule() { - return this.file.type === 'submodule'; - }, - isTree() { - return this.file.type === 'tree'; - }, - levelIndentation() { - if (this.file.level > 0) { - return { - marginLeft: `${this.file.level * 16}px`, - }; - } - return {}; - }, - shortId() { - return this.file.id.substr(0, 8); - }, - submoduleColSpan() { - return !this.leftPanelCollapsed && this.isSubmodule ? 3 : 1; - }, - fileClass() { - if (this.file.type === 'blob') { - if (this.file.active) { - return 'file-open file-active'; - } - return this.file.opened ? 'file-open' : ''; - } - return ''; - }, - changedClass() { - return { - 'fa-circle unsaved-icon': this.file.changed || this.file.tempFile, - }; - }, + }, + computed: { + isTree() { + return this.file.type === 'tree'; }, - updated() { - if (this.file.type === 'blob' && this.file.active) { - this.$el.scrollIntoView(); - } + isBlob() { + return this.file.type === 'blob'; + }, + levelIndentation() { + return { + marginLeft: `${this.level * 16}px`, + }; }, - methods: { - clickFile(row) { - // Manual Action if a tree is selected/opened - if (this.file.type === 'tree' && this.$router.currentRoute.path === `/project${row.url}`) { - this.$store.dispatch('toggleTreeOpen', { - endpoint: this.file.url, - tree: this.file, - }); - } - this.$router.push(`/project${row.url}`); - }, + fileClass() { + return { + 'file-open': this.isBlob && this.file.opened, + 'file-active': this.isBlob && this.file.active, + folder: this.isTree, + 'is-open': this.file.opened, + }; }, - }; + }, + updated() { + if (this.file.type === 'blob' && this.file.active) { + this.$el.scrollIntoView(); + } + }, + methods: { + ...mapActions(['toggleTreeOpen', 'updateDelayViewerUpdated']), + clickFile() { + // Manual Action if a tree is selected/opened + if ( + this.isTree && + this.$router.currentRoute.path === `/project${this.file.url}` + ) { + this.toggleTreeOpen(this.file.path); + } + + const delayPromise = this.file.changed + ? Promise.resolve() + : this.updateDelayViewerUpdated(true); + + return delayPromise.then(() => { + router.push(`/project${this.file.url}`); + }); + }, + }, +}; </script> <template> - <tr - class="file" - :class="fileClass" - @click="clickFile(file)"> - <td - class="multi-file-table-name" - :colspan="submoduleColSpan" + <div> + <div + class="file" + :class="fileClass" > - <a - class="repo-file-name" - > - <file-icon - :file-name="file.name" - :loading="file.loading" - :folder="file.type === 'tree'" - :opened="file.opened" - :style="levelIndentation" - :size="16" - /> - {{ file.name }} - </a> - <new-dropdown - v-if="isTree" - :project-id="file.projectId" - :branch="file.branchId" - :path="file.path" - :parent="file" - /> - <i - class="fa" - v-if="file.changed || file.tempFile" - :class="changedClass" - aria-hidden="true" + <div + class="file-name" + @click="clickFile" + role="button" > - </i> - <template v-if="isSubmodule && file.id"> - @ - <span class="commit-sha"> - <a - @click.stop - :href="file.tree_url" - > - {{ shortId }} - </a> - </span> - </template> - </td> - - <template v-if="showExtraColumns && !isSubmodule"> - <td class="multi-file-table-col-commit-message hidden-sm hidden-xs"> - <a - v-if="file.lastCommit.message" - @click.stop - :href="file.lastCommit.url" - > - {{ file.lastCommit.message }} - </a> - <skeleton-loading-container - v-else - :small="true" - /> - </td> - - <td class="commit-update hidden-xs text-right"> <span - v-if="file.lastCommit.updatedAt" - :title="tooltipTitle(file.lastCommit.updatedAt)" + class="ide-file-name str-truncated" + :style="levelIndentation" > - {{ timeFormated(file.lastCommit.updatedAt) }} + <file-icon + :file-name="file.name" + :loading="file.loading" + :folder="isTree" + :opened="file.opened" + :size="16" + /> + {{ file.name }} + <file-status-icon + :file="file" + /> </span> - <skeleton-loading-container - v-else - class="animation-container-right" - :small="true" + <changed-file-icon + :file="file" + v-if="file.changed || file.tempFile" + class="prepend-top-5 pull-right" /> - </td> + <new-dropdown + v-if="isTree" + :project-id="file.projectId" + :branch="file.branchId" + :path="file.path" + class="pull-right prepend-left-8" + /> + </div> + </div> + <template v-if="file.opened"> + <repo-file + v-for="childFile in file.tree" + :key="childFile.key" + :file="childFile" + :level="level + 1" + /> </template> - </tr> + </div> </template> diff --git a/app/assets/javascripts/ide/components/repo_file_buttons.vue b/app/assets/javascripts/ide/components/repo_file_buttons.vue index aabc0d8eada..4ea8cf7504b 100644 --- a/app/assets/javascripts/ide/components/repo_file_buttons.vue +++ b/app/assets/javascripts/ide/components/repo_file_buttons.vue @@ -1,19 +1,20 @@ <script> -import { mapGetters } from 'vuex'; - export default { + props: { + file: { + type: Object, + required: true, + }, + }, computed: { - ...mapGetters([ - 'activeFile', - ]), showButtons() { - return this.activeFile.rawPath || - this.activeFile.blamePath || - this.activeFile.commitsPath || - this.activeFile.permalink; + return this.file.rawPath || + this.file.blamePath || + this.file.commitsPath || + this.file.permalink; }, rawDownloadButtonLabel() { - return this.activeFile.binary ? 'Download' : 'Raw'; + return this.file.binary ? 'Download' : 'Raw'; }, }, }; @@ -25,7 +26,7 @@ export default { class="multi-file-editor-btn-group" > <a - :href="activeFile.rawPath" + :href="file.rawPath" target="_blank" class="btn btn-default btn-sm raw" rel="noopener noreferrer"> @@ -38,19 +39,19 @@ export default { aria-label="File actions" > <a - :href="activeFile.blamePath" + :href="file.blamePath" class="btn btn-default btn-sm blame" > Blame </a> <a - :href="activeFile.commitsPath" + :href="file.commitsPath" class="btn btn-default btn-sm history" > History </a> <a - :href="activeFile.permalink" + :href="file.permalink" class="btn btn-default btn-sm permalink" > Permalink diff --git a/app/assets/javascripts/ide/components/repo_file_status_icon.vue b/app/assets/javascripts/ide/components/repo_file_status_icon.vue new file mode 100644 index 00000000000..25d311142d5 --- /dev/null +++ b/app/assets/javascripts/ide/components/repo_file_status_icon.vue @@ -0,0 +1,39 @@ +<script> + import icon from '~/vue_shared/components/icon.vue'; + import tooltip from '~/vue_shared/directives/tooltip'; + import '~/lib/utils/datetime_utility'; + + export default { + components: { + icon, + }, + directives: { + tooltip, + }, + props: { + file: { + type: Object, + required: true, + }, + }, + computed: { + lockTooltip() { + return `Locked by ${this.file.file_lock.user.name}`; + }, + }, + }; +</script> + +<template> + <span + v-if="file.file_lock" + v-tooltip + :title="lockTooltip" + data-container="body" + > + <icon + name="lock" + css-classes="file-status-icon" + /> + </span> +</template> diff --git a/app/assets/javascripts/ide/components/repo_prev_directory.vue b/app/assets/javascripts/ide/components/repo_prev_directory.vue deleted file mode 100644 index 7cd359ea4ed..00000000000 --- a/app/assets/javascripts/ide/components/repo_prev_directory.vue +++ /dev/null @@ -1,32 +0,0 @@ -<script> - import { mapState, mapActions } from 'vuex'; - - export default { - computed: { - ...mapState([ - 'parentTreeUrl', - 'leftPanelCollapsed', - ]), - colSpanCondition() { - return this.leftPanelCollapsed ? undefined : 3; - }, - }, - methods: { - ...mapActions([ - 'getTreeData', - ]), - }, - }; -</script> - -<template> - <tr class="file prev-directory"> - <td - :colspan="colSpanCondition" - class="table-cell" - @click.prevent="getTreeData({ endpoint: parentTreeUrl })" - > - <a :href="parentTreeUrl">...</a> - </td> - </tr> -</template> diff --git a/app/assets/javascripts/ide/components/repo_preview.vue b/app/assets/javascripts/ide/components/repo_preview.vue deleted file mode 100644 index a216269e292..00000000000 --- a/app/assets/javascripts/ide/components/repo_preview.vue +++ /dev/null @@ -1,71 +0,0 @@ -<script> - import { mapGetters } from 'vuex'; - import LineHighlighter from '~/line_highlighter'; - import syntaxHighlight from '~/syntax_highlight'; - - export default { - computed: { - ...mapGetters([ - 'activeFile', - ]), - renderErrorTooLarge() { - return this.activeFile.renderError === 'too_large'; - }, - }, - mounted() { - this.highlightFile(); - this.lineHighlighter = new LineHighlighter({ - fileHolderSelector: '.blob-viewer-container', - scrollFileHolder: true, - }); - }, - updated() { - this.$nextTick(() => { - this.highlightFile(); - }); - }, - methods: { - highlightFile() { - syntaxHighlight($(this.$el).find('.file-content')); - }, - }, - }; -</script> - -<template> - <div> - <div - v-if="!activeFile.renderError" - v-html="activeFile.html" - class="multi-file-preview-holder" - > - </div> - <div - v-else-if="activeFile.tempFile" - class="vertical-center render-error"> - <p class="text-center"> - The source could not be displayed for this temporary file. - </p> - </div> - <div - v-else-if="renderErrorTooLarge" - class="vertical-center render-error"> - <p class="text-center"> - The source could not be displayed because it is too large. - You can <a - :href="activeFile.rawPath" - download>download</a> it instead. - </p> - </div> - <div - v-else - class="vertical-center render-error"> - <p class="text-center"> - The source could not be displayed because a rendering error occurred. - You can <a - :href="activeFile.rawPath" - download>download</a> it instead. - </p> - </div> - </div> -</template> diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue index 5656081c598..c337bc813e6 100644 --- a/app/assets/javascripts/ide/components/repo_tab.vue +++ b/app/assets/javascripts/ide/components/repo_tab.vue @@ -1,10 +1,17 @@ <script> import { mapActions } from 'vuex'; + import fileIcon from '~/vue_shared/components/file_icon.vue'; + import icon from '~/vue_shared/components/icon.vue'; + import fileStatusIcon from './repo_file_status_icon.vue'; + import changedFileIcon from './changed_file_icon.vue'; export default { components: { + fileStatusIcon, fileIcon, + icon, + changedFileIcon, }, props: { tab: { @@ -12,6 +19,11 @@ required: true, }, }, + data() { + return { + tabMouseOver: false, + }; + }, computed: { closeLabel() { if (this.tab.changed || this.tab.tempFile) { @@ -19,12 +31,8 @@ } return `Close ${this.tab.name}`; }, - changedClass() { - const tabChangedObj = { - 'fa-times close-icon': !this.tab.changed && !this.tab.tempFile, - 'fa-circle unsaved-icon': this.tab.changed || this.tab.tempFile, - }; - return tabChangedObj; + showChangedIcon() { + return this.tab.changed ? !this.tabMouseOver : false; }, }, @@ -35,28 +43,41 @@ clickFile(tab) { this.$router.push(`/project${tab.url}`); }, + mouseOverTab() { + if (this.tab.changed) { + this.tabMouseOver = true; + } + }, + mouseOutTab() { + if (this.tab.changed) { + this.tabMouseOver = false; + } + }, }, }; </script> <template> - <li @click="clickFile(tab)"> + <li + @click="clickFile(tab)" + @mouseover="mouseOverTab" + @mouseout="mouseOutTab" + > <button type="button" class="multi-file-tab-close" - @click.stop.prevent="closeFile({ file: tab })" + @click.stop.prevent="closeFile(tab.path)" :aria-label="closeLabel" - :class="{ - 'modified': tab.changed, - }" - :disabled="tab.changed" > - <i - class="fa" - :class="changedClass" - aria-hidden="true" - > - </i> + <icon + v-if="!showChangedIcon" + name="close" + :size="12" + /> + <changed-file-icon + v-else + :file="tab" + /> </button> <div @@ -69,6 +90,9 @@ :size="16" /> {{ tab.name }} + <file-status-icon + :file="tab" + /> </div> </li> </template> diff --git a/app/assets/javascripts/ide/components/repo_tabs.vue b/app/assets/javascripts/ide/components/repo_tabs.vue index ca363bba0ef..8ea64ddf84a 100644 --- a/app/assets/javascripts/ide/components/repo_tabs.vue +++ b/app/assets/javascripts/ide/components/repo_tabs.vue @@ -1,27 +1,61 @@ <script> - import { mapState } from 'vuex'; + import { mapActions } from 'vuex'; import RepoTab from './repo_tab.vue'; + import EditorMode from './editor_mode_dropdown.vue'; export default { components: { - 'repo-tab': RepoTab, + RepoTab, + EditorMode, }, - computed: { - ...mapState([ - 'openFiles', - ]), + props: { + files: { + type: Array, + required: true, + }, + viewer: { + type: String, + required: true, + }, + hasChanges: { + type: Boolean, + required: true, + }, + }, + data() { + return { + showShadow: false, + }; + }, + updated() { + if (!this.$refs.tabsScroller) return; + + this.showShadow = + this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth; + }, + methods: { + ...mapActions(['updateViewer']), }, }; </script> <template> - <ul - class="multi-file-tabs list-unstyled append-bottom-0" - > - <repo-tab - v-for="tab in openFiles" - :key="tab.key" - :tab="tab" + <div class="multi-file-tabs"> + <ul + class="list-unstyled append-bottom-0" + ref="tabsScroller" + > + <repo-tab + v-for="tab in files" + :key="tab.key" + :tab="tab" + /> + </ul> + <editor-mode + :viewer="viewer" + :show-shadow="showShadow" + :has-changes="hasChanges" + @click="updateViewer" /> - </ul> + </div> </template> diff --git a/app/assets/javascripts/ide/components/resizable_panel.vue b/app/assets/javascripts/ide/components/resizable_panel.vue new file mode 100644 index 00000000000..faa690ecba0 --- /dev/null +++ b/app/assets/javascripts/ide/components/resizable_panel.vue @@ -0,0 +1,88 @@ +<script> + import { mapActions, mapState } from 'vuex'; + import PanelResizer from '~/vue_shared/components/panel_resizer.vue'; + + export default { + components: { + PanelResizer, + }, + props: { + collapsible: { + type: Boolean, + required: true, + }, + initialWidth: { + type: Number, + required: true, + }, + minSize: { + type: Number, + required: false, + default: 200, + }, + side: { + type: String, + required: true, + }, + }, + data() { + return { + width: this.initialWidth, + }; + }, + computed: { + ...mapState({ + collapsed(state) { + return state[`${this.side}PanelCollapsed`]; + }, + }), + panelStyle() { + if (!this.collapsed) { + return { + width: `${this.width}px`, + }; + } + + return {}; + }, + }, + methods: { + ...mapActions([ + 'setPanelCollapsedStatus', + 'setResizingStatus', + ]), + toggleFullbarCollapsed() { + if (this.collapsed && this.collapsible) { + this.setPanelCollapsedStatus({ + side: this.side, + collapsed: !this.collapsed, + }); + } + }, + }, + maxSize: (window.innerWidth / 2), + }; +</script> + +<template> + <div + class="multi-file-commit-panel" + :class="{ + 'is-collapsed': collapsed && collapsible, + }" + :style="panelStyle" + @click="toggleFullbarCollapsed" + > + <slot></slot> + <panel-resizer + :size.sync="width" + :enabled="!collapsed" + :start-size="initialWidth" + :min-size="minSize" + :max-size="$options.maxSize" + @resize-start="setResizingStatus(true)" + @resize-end="setResizingStatus(false)" + :side="side === 'right' ? 'left' : 'right'" + /> + </div> +</template> diff --git a/app/assets/javascripts/ide/eventhub.js b/app/assets/javascripts/ide/eventhub.js new file mode 100644 index 00000000000..0948c2e5352 --- /dev/null +++ b/app/assets/javascripts/ide/eventhub.js @@ -0,0 +1,3 @@ +import Vue from 'vue'; + +export default new Vue(); diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js index a7fb9e0588a..db89c1d44db 100644 --- a/app/assets/javascripts/ide/ide_router.js +++ b/app/assets/javascripts/ide/ide_router.js @@ -1,10 +1,7 @@ import Vue from 'vue'; import VueRouter from 'vue-router'; +import flash from '~/flash'; import store from './stores'; -import flash from '../flash'; -import { - getTreeEntry, -} from './stores/utils'; Vue.use(VueRouter); @@ -57,42 +54,61 @@ const router = new VueRouter({ router.beforeEach((to, from, next) => { if (to.params.namespace && to.params.project) { - store.dispatch('getProjectData', { - namespace: to.params.namespace, - projectId: to.params.project, - }) - .then(() => { - const fullProjectId = `${to.params.namespace}/${to.params.project}`; + store + .dispatch('getProjectData', { + namespace: to.params.namespace, + projectId: to.params.project, + }) + .then(() => { + const fullProjectId = `${to.params.namespace}/${to.params.project}`; - if (to.params.branch) { - store.dispatch('getBranchData', { - projectId: fullProjectId, - branchId: to.params.branch, - }); + if (to.params.branch) { + store.dispatch('getBranchData', { + projectId: fullProjectId, + branchId: to.params.branch, + }); - store.dispatch('getTreeData', { - projectId: fullProjectId, - branch: to.params.branch, - endpoint: `/tree/${to.params.branch}`, - }) - .then(() => { - if (to.params[0]) { - const treeEntry = getTreeEntry(store, `${to.params.namespace}/${to.params.project}/${to.params.branch}`, to.params[0]); - if (treeEntry) { - store.dispatch('handleTreeEntryAction', treeEntry); - } - } - }) - .catch((e) => { - flash('Error while loading the branch files. Please try again.', 'alert', document, null, false, true); - throw e; - }); - } - }) - .catch((e) => { - flash('Error while loading the project data. Please try again.', 'alert', document, null, false, true); - throw e; - }); + store + .dispatch('getFiles', { + projectId: fullProjectId, + branchId: to.params.branch, + }) + .then(() => { + if (to.params[0]) { + const path = + to.params[0].slice(-1) === '/' + ? to.params[0].slice(0, -1) + : to.params[0]; + const treeEntry = store.state.entries[path]; + if (treeEntry) { + store.dispatch('handleTreeEntryAction', treeEntry); + } + } + }) + .catch(e => { + flash( + 'Error while loading the branch files. Please try again.', + 'alert', + document, + null, + false, + true, + ); + throw e; + }); + } + }) + .catch(e => { + flash( + 'Error while loading the project data. Please try again.', + 'alert', + document, + null, + false, + true, + ); + throw e; + }); } next(); diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js index e8a19f47cee..cbfb3dc54f2 100644 --- a/app/assets/javascripts/ide/index.js +++ b/app/assets/javascripts/ide/index.js @@ -1,8 +1,8 @@ import Vue from 'vue'; +import Translate from '~/vue_shared/translate'; import ide from './components/ide.vue'; import store from './stores'; import router from './ide_router'; -import Translate from '../vue_shared/translate'; function initIde(el) { if (!el) return null; @@ -18,6 +18,8 @@ function initIde(el) { return createElement('ide', { props: { emptyStateSvgPath: el.dataset.emptyStateSvgPath, + noChangesStateSvgPath: el.dataset.noChangesStateSvgPath, + committedStateSvgPath: el.dataset.committedStateSvgPath, }, }); }, diff --git a/app/assets/javascripts/ide/lib/common/model.js b/app/assets/javascripts/ide/lib/common/model.js index 14d9fe4771e..73cd684351c 100644 --- a/app/assets/javascripts/ide/lib/common/model.js +++ b/app/assets/javascripts/ide/lib/common/model.js @@ -1,5 +1,6 @@ /* global monaco */ import Disposable from './disposable'; +import eventHub from '../../eventhub'; export default class Model { constructor(monaco, file) { @@ -9,19 +10,28 @@ export default class Model { this.content = file.content !== '' ? file.content : file.raw; this.disposable.add( - this.originalModel = this.monaco.editor.createModel( + (this.originalModel = this.monaco.editor.createModel( this.file.raw, undefined, new this.monaco.Uri(null, null, `original/${this.file.path}`), - ), - this.model = this.monaco.editor.createModel( + )), + (this.model = this.monaco.editor.createModel( this.content, undefined, new this.monaco.Uri(null, null, this.file.path), - ), + )), ); this.events = new Map(); + + this.updateContent = this.updateContent.bind(this); + this.dispose = this.dispose.bind(this); + + eventHub.$on(`editor.update.model.dispose.${this.file.path}`, this.dispose); + eventHub.$on( + `editor.update.model.content.${this.file.path}`, + this.updateContent, + ); } get url() { @@ -48,17 +58,33 @@ export default class Model { return this.originalModel; } + setValue(value) { + this.getModel().setValue(value); + } + onChange(cb) { this.events.set( this.path, - this.disposable.add( - this.model.onDidChangeContent(e => cb(this.model, e)), - ), + this.disposable.add(this.model.onDidChangeContent(e => cb(this, e))), ); } + updateContent(content) { + this.getOriginalModel().setValue(content); + this.getModel().setValue(content); + } + dispose() { this.disposable.dispose(); this.events.clear(); + + eventHub.$off( + `editor.update.model.dispose.${this.file.path}`, + this.dispose, + ); + eventHub.$off( + `editor.update.model.content.${this.file.path}`, + this.updateContent, + ); } } diff --git a/app/assets/javascripts/ide/lib/common/model_manager.js b/app/assets/javascripts/ide/lib/common/model_manager.js index fd462252795..57d5e59a88b 100644 --- a/app/assets/javascripts/ide/lib/common/model_manager.js +++ b/app/assets/javascripts/ide/lib/common/model_manager.js @@ -1,3 +1,4 @@ +import eventHub from '../../eventhub'; import Disposable from './disposable'; import Model from './model'; @@ -12,18 +13,36 @@ export default class ModelManager { return this.models.has(path); } + getModel(path) { + return this.models.get(path); + } + addModel(file) { if (this.hasCachedModel(file.path)) { - return this.models.get(file.path); + return this.getModel(file.path); } const model = new Model(this.monaco, file); this.models.set(model.path, model); this.disposable.add(model); + eventHub.$on( + `editor.update.model.dispose.${file.path}`, + this.removeCachedModel.bind(this, file), + ); + return model; } + removeCachedModel(file) { + this.models.delete(file.path); + + eventHub.$off( + `editor.update.model.dispose.${file.path}`, + this.removeCachedModel, + ); + } + dispose() { // dispose of all the models this.disposable.dispose(); diff --git a/app/assets/javascripts/ide/lib/decorations/controller.js b/app/assets/javascripts/ide/lib/decorations/controller.js index 0954b7973c4..42904774747 100644 --- a/app/assets/javascripts/ide/lib/decorations/controller.js +++ b/app/assets/javascripts/ide/lib/decorations/controller.js @@ -27,6 +27,8 @@ export default class DecorationsController { } decorate(model) { + if (!this.editor.instance) return; + const decorations = this.getAllDecorationsForModel(model); const oldDecorations = this.editorDecorations.get(model.url) || []; diff --git a/app/assets/javascripts/ide/lib/diff/controller.js b/app/assets/javascripts/ide/lib/diff/controller.js index dc0b1c95e59..b136545ad11 100644 --- a/app/assets/javascripts/ide/lib/diff/controller.js +++ b/app/assets/javascripts/ide/lib/diff/controller.js @@ -59,7 +59,8 @@ export default class DirtyDiffController { decorate({ data }) { const decorations = data.changes.map(change => getDecorator(change)); - this.decorationsController.addDecorations(data.path, 'dirtyDiff', decorations); + const model = this.modelManager.getModel(data.path); + this.decorationsController.addDecorations(model, 'dirtyDiff', decorations); } dispose() { diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js index 51255f15658..38de2fe2b27 100644 --- a/app/assets/javascripts/ide/lib/editor.js +++ b/app/assets/javascripts/ide/lib/editor.js @@ -3,10 +3,21 @@ import DecorationsController from './decorations/controller'; import DirtyDiffController from './diff/controller'; import Disposable from './common/disposable'; import ModelManager from './common/model_manager'; -import editorOptions from './editor_options'; +import editorOptions, { defaultEditorOptions } from './editor_options'; +import gitlabTheme from './themes/gl_theme'; + +export const clearDomElement = el => { + if (!el || !el.firstChild) return; + + while (el.firstChild) { + el.removeChild(el.firstChild); + } +}; export default class Editor { static create(monaco) { + if (this.editorInstance) return this.editorInstance; + this.editorInstance = new Editor(monaco); return this.editorInstance; @@ -18,34 +29,46 @@ export default class Editor { this.instance = null; this.dirtyDiffController = null; this.disposable = new Disposable(); + this.modelManager = new ModelManager(this.monaco); + this.decorationsController = new DecorationsController(this); - this.disposable.add( - this.modelManager = new ModelManager(this.monaco), - this.decorationsController = new DecorationsController(this), - ); + this.setupMonacoTheme(); this.debouncedUpdate = _.debounce(() => { this.updateDimensions(); }, 200); - window.addEventListener('resize', this.debouncedUpdate, false); } createInstance(domElement) { if (!this.instance) { + clearDomElement(domElement); + + this.disposable.add( + (this.instance = this.monaco.editor.create(domElement, { + ...defaultEditorOptions, + })), + (this.dirtyDiffController = new DirtyDiffController( + this.modelManager, + this.decorationsController, + )), + ); + + window.addEventListener('resize', this.debouncedUpdate, false); + } + } + + createDiffInstance(domElement) { + if (!this.instance) { + clearDomElement(domElement); + this.disposable.add( - this.instance = this.monaco.editor.create(domElement, { - model: null, - readOnly: false, - contextmenu: true, - scrollBeyondLastLine: false, - minimap: { - enabled: false, - }, - }), - this.dirtyDiffController = new DirtyDiffController( - this.modelManager, this.decorationsController, - ), + (this.instance = this.monaco.editor.createDiffEditor(domElement, { + ...defaultEditorOptions, + readOnly: true, + })), ); + + window.addEventListener('resize', this.debouncedUpdate, false); } } @@ -54,23 +77,43 @@ export default class Editor { } attachModel(model) { + if (this.instance.getEditorType() === 'vs.editor.IDiffEditor') { + this.instance.setModel({ + original: model.getOriginalModel(), + modified: model.getModel(), + }); + + return; + } + this.instance.setModel(model.getModel()); if (this.dirtyDiffController) this.dirtyDiffController.attachModel(model); this.currentModel = model; - this.instance.updateOptions(editorOptions.reduce((acc, obj) => { - Object.keys(obj).forEach((key) => { - Object.assign(acc, { - [key]: obj[key](model), + this.instance.updateOptions( + editorOptions.reduce((acc, obj) => { + Object.keys(obj).forEach(key => { + Object.assign(acc, { + [key]: obj[key](model), + }); }); - }); - return acc; - }, {})); + return acc; + }, {}), + ); if (this.dirtyDiffController) this.dirtyDiffController.reDecorate(model); } + setupMonacoTheme() { + this.monaco.editor.defineTheme( + gitlabTheme.themeName, + gitlabTheme.monacoTheme, + ); + + this.monaco.editor.setTheme('gitlab'); + } + clearEditor() { if (this.instance) { this.instance.setModel(null); @@ -78,12 +121,21 @@ export default class Editor { } dispose() { - this.disposable.dispose(); window.removeEventListener('resize', this.debouncedUpdate); - // dispose main monaco instance - if (this.instance) { + // catch any potential errors with disposing the error + // this is mainly for tests caused by elements not existing + try { + this.disposable.dispose(); + this.instance = null; + } catch (e) { + this.instance = null; + + if (process.env.NODE_ENV !== 'test') { + // eslint-disable-next-line no-console + console.error(e); + } } } @@ -103,6 +155,8 @@ export default class Editor { } onPositionChange(cb) { + if (!this.instance.onDidChangeCursorPosition) return; + this.disposable.add( this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)), ); diff --git a/app/assets/javascripts/ide/lib/editor_options.js b/app/assets/javascripts/ide/lib/editor_options.js index 701affc466e..d69d4b8c615 100644 --- a/app/assets/javascripts/ide/lib/editor_options.js +++ b/app/assets/javascripts/ide/lib/editor_options.js @@ -1,2 +1,15 @@ -export default [{ -}]; +export const defaultEditorOptions = { + model: null, + readOnly: false, + contextmenu: true, + scrollBeyondLastLine: false, + minimap: { + enabled: false, + }, +}; + +export default [ + { + readOnly: model => !!model.file.file_lock, + }, +]; diff --git a/app/assets/javascripts/ide/lib/themes/gl_theme.js b/app/assets/javascripts/ide/lib/themes/gl_theme.js new file mode 100644 index 00000000000..2fc96250c7d --- /dev/null +++ b/app/assets/javascripts/ide/lib/themes/gl_theme.js @@ -0,0 +1,14 @@ +export default { + themeName: 'gitlab', + monacoTheme: { + base: 'vs', + inherit: true, + rules: [], + colors: { + 'editorLineNumber.foreground': '#CCCCCC', + 'diffEditor.insertedTextBackground': '#ddfbe6', + 'diffEditor.removedTextBackground': '#f9d7dc', + 'editor.selectionBackground': '#aad6f8', + }, + }, +}; diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js index 1fb24e93f2e..5f1fb6cf843 100644 --- a/app/assets/javascripts/ide/services/index.js +++ b/app/assets/javascripts/ide/services/index.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import VueResource from 'vue-resource'; -import Api from '../../api'; +import Api from '~/api'; Vue.use(VueResource); @@ -44,4 +44,12 @@ export default { }, }); }, + getFiles(projectUrl, branchId) { + const url = `${projectUrl}/files/${branchId}`; + return Vue.http.get(url, { + params: { + format: 'json', + }, + }); + }, }; diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js index 2c690b1f635..7e920aa9f30 100644 --- a/app/assets/javascripts/ide/stores/actions.js +++ b/app/assets/javascripts/ide/stores/actions.js @@ -1,59 +1,28 @@ import Vue from 'vue'; import { visitUrl } from '~/lib/utils/url_utility'; import flash from '~/flash'; -import service from '../services'; import * as types from './mutation_types'; -import { stripHtml } from '../../lib/utils/text_utility'; +import FilesDecoratorWorker from './workers/files_decorator_worker'; export const redirectToUrl = (_, url) => visitUrl(url); export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data); -export const closeDiscardPopup = ({ commit }) => - commit(types.TOGGLE_DISCARD_POPUP, false); - -export const discardAllChanges = ({ commit, getters, dispatch }) => { - const changedFiles = getters.changedFiles; - - changedFiles.forEach((file) => { - commit(types.DISCARD_FILE_CHANGES, file); +export const discardAllChanges = ({ state, commit, dispatch }) => { + state.changedFiles.forEach(file => { + commit(types.DISCARD_FILE_CHANGES, file.path); if (file.tempFile) { - dispatch('closeFile', { file, force: true }); + dispatch('closeFile', file.path); } }); -}; - -export const closeAllFiles = ({ state, dispatch }) => { - state.openFiles.forEach(file => dispatch('closeFile', { file })); -}; -export const toggleEditMode = ( - { state, commit, getters, dispatch }, - force = false, -) => { - const changedFiles = getters.changedFiles; - - if (changedFiles.length && !force) { - commit(types.TOGGLE_DISCARD_POPUP, true); - } else { - commit(types.TOGGLE_EDIT_MODE); - commit(types.TOGGLE_DISCARD_POPUP, false); - dispatch('toggleBlobView'); - - if (!state.editMode) { - dispatch('discardAllChanges'); - } - } + commit(types.REMOVE_ALL_CHANGES_FILES); }; -export const toggleBlobView = ({ commit, state }) => { - if (state.editMode) { - commit(types.SET_EDIT_MODE); - } else { - commit(types.SET_PREVIEW_MODE); - } +export const closeAllFiles = ({ state, dispatch }) => { + state.openFiles.forEach(file => dispatch('closeFile', file.path)); }; export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => { @@ -68,115 +37,64 @@ export const setResizingStatus = ({ commit }, resizing) => { commit(types.SET_RESIZING_STATUS, resizing); }; -export const checkCommitStatus = ({ state }) => - service - .getBranchData(state.currentProjectId, state.currentBranchId) - .then(({ data }) => { - const { id } = data.commit; - const selectedBranch = - state.projects[state.currentProjectId].branches[state.currentBranchId]; - - if (selectedBranch.workingReference !== id) { - return true; - } - - return false; - }) - .catch(() => flash('Error checking branch data. Please try again.', 'alert', document, null, false, true)); - -export const commitChanges = ( - { commit, state, dispatch, getters }, - { payload, newMr }, +export const createTempEntry = ( + { state, commit, dispatch }, + { branchId, name, type, content = '', base64 = false }, ) => - service - .commit(state.currentProjectId, payload) - .then(({ data }) => { - const { branch } = payload; - if (!data.short_id) { - flash(data.message, 'alert', document, null, false, true); - return; - } - - const selectedProject = state.projects[state.currentProjectId]; - const lastCommit = { - commit_path: `${selectedProject.web_url}/commit/${data.id}`, - commit: { - message: data.message, - authored_date: data.committed_date, - }, - }; - - let commitMsg = `Your changes have been committed. Commit ${data.short_id}`; - if (data.stats) { - commitMsg += ` with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`; - } + new Promise(resolve => { + const worker = new FilesDecoratorWorker(); + const fullName = + name.slice(-1) !== '/' && type === 'tree' ? `${name}/` : name; + if (state.entries[name]) { flash( - commitMsg, - 'notice', + `The name "${name + .split('/') + .pop()}" is already taken in this directory.`, + 'alert', document, null, false, - true); - window.dispatchEvent(new Event('resize')); - - if (newMr) { - dispatch('discardAllChanges'); - dispatch( - 'redirectToUrl', - `${selectedProject.web_url}/merge_requests/new?merge_request%5Bsource_branch%5D=${branch}`, - ); - } else { - commit(types.SET_BRANCH_WORKING_REFERENCE, { - projectId: state.currentProjectId, - branchId: state.currentBranchId, - reference: data.id, - }); - - getters.changedFiles.forEach((entry) => { - commit(types.SET_LAST_COMMIT_DATA, { - entry, - lastCommit, - }); - }); - - dispatch('discardAllChanges'); - - window.scrollTo(0, 0); - } - }) - .catch((err) => { - let errMsg = 'Error committing changes. Please try again.'; - if (err.response.data && err.response.data.message) { - errMsg += ` (${stripHtml(err.response.data.message)})`; + true, + ); + + resolve(); + + return null; + } + + worker.addEventListener('message', ({ data }) => { + const { file } = data; + + worker.terminate(); + + commit(types.CREATE_TMP_ENTRY, { + data, + projectId: state.currentProjectId, + branchId, + }); + + if (type === 'blob') { + commit(types.TOGGLE_FILE_OPEN, file.path); + commit(types.ADD_FILE_TO_CHANGED, file.path); + dispatch('setFileActive', file.path); } - flash(errMsg, 'alert', document, null, false, true); - window.dispatchEvent(new Event('resize')); - }); -export const createTempEntry = ( - { state, dispatch }, - { projectId, branchId, parent, name, type, content = '', base64 = false }, -) => { - const selectedParent = parent || state.trees[`${projectId}/${branchId}`]; - if (type === 'tree') { - dispatch('createTempTree', { - projectId, - branchId, - parent: selectedParent, - name, + resolve(file); }); - } else if (type === 'blob') { - dispatch('createTempFile', { - projectId, + + worker.postMessage({ + data: [fullName], + projectId: state.currentProjectId, branchId, - parent: selectedParent, - name, + type, + tempFile: true, base64, content, }); - } -}; + + return null; + }); export const scrollToTab = () => { Vue.nextTick(() => { @@ -190,7 +108,14 @@ export const scrollToTab = () => { }); }; +export const updateViewer = ({ commit }, viewer) => { + commit(types.UPDATE_VIEWER, viewer); +}; + +export const updateDelayViewerUpdated = ({ commit }, delay) => { + commit(types.UPDATE_DELAY_VIEWER_CHANGE, delay); +}; + export * from './actions/tree'; export * from './actions/file'; export * from './actions/project'; -export * from './actions/branch'; diff --git a/app/assets/javascripts/ide/stores/actions/branch.js b/app/assets/javascripts/ide/stores/actions/branch.js deleted file mode 100644 index bc6fd2d4163..00000000000 --- a/app/assets/javascripts/ide/stores/actions/branch.js +++ /dev/null @@ -1,43 +0,0 @@ -import service from '../../services'; -import flash from '../../../flash'; -import * as types from '../mutation_types'; - -export const getBranchData = ( - { commit, state, dispatch }, - { projectId, branchId, force = false } = {}, -) => new Promise((resolve, reject) => { - if ((typeof state.projects[`${projectId}`] === 'undefined' || - !state.projects[`${projectId}`].branches[branchId]) - || force) { - service.getBranchData(`${projectId}`, branchId) - .then(({ data }) => { - const { id } = data.commit; - commit(types.SET_BRANCH, { projectPath: `${projectId}`, branchName: branchId, branch: data }); - commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id }); - resolve(data); - }) - .catch(() => { - flash('Error loading branch data. Please try again.', 'alert', document, null, false, true); - reject(new Error(`Branch not loaded - ${projectId}/${branchId}`)); - }); - } else { - resolve(state.projects[`${projectId}`].branches[branchId]); - } -}); - -export const createNewBranch = ({ state, commit }, branch) => service.createBranch( - state.currentProjectId, - { - branch, - ref: state.currentBranchId, - }, -) -.then(res => res.json()) -.then((data) => { - const branchName = data.name; - const url = location.href.replace(state.currentBranchId, branchName); - - if (this.$router) this.$router.push(url); - - commit(types.SET_CURRENT_BRANCH, branchName); -}); diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js index 670af2fb89e..ddc4b757bf9 100644 --- a/app/assets/javascripts/ide/stores/actions/file.js +++ b/app/assets/javascripts/ide/stores/actions/file.js @@ -1,137 +1,146 @@ -import { normalizeHeaders } from '../../../lib/utils/common_utils'; -import flash from '../../../flash'; +import { normalizeHeaders } from '~/lib/utils/common_utils'; +import flash from '~/flash'; +import eventHub from '../../eventhub'; import service from '../../services'; import * as types from '../mutation_types'; import router from '../../ide_router'; -import { - findEntry, - setPageTitle, - createTemp, - findIndexOfFile, -} from '../utils'; +import { setPageTitle } from '../utils'; -export const closeFile = ({ commit, state, dispatch }, { file, force = false }) => { - if ((file.changed || file.tempFile) && !force) return; - - const indexOfClosedFile = findIndexOfFile(state.openFiles, file); +export const closeFile = ({ commit, state, getters, dispatch }, path) => { + const indexOfClosedFile = state.openFiles.findIndex(f => f.path === path); + const file = state.entries[path]; const fileWasActive = file.active; - commit(types.TOGGLE_FILE_OPEN, file); - commit(types.SET_FILE_ACTIVE, { file, active: false }); + commit(types.TOGGLE_FILE_OPEN, path); + commit(types.SET_FILE_ACTIVE, { path, active: false }); if (state.openFiles.length > 0 && fileWasActive) { const nextIndexToOpen = indexOfClosedFile === 0 ? 0 : indexOfClosedFile - 1; - const nextFileToOpen = state.openFiles[nextIndexToOpen]; + const nextFileToOpen = state.entries[state.openFiles[nextIndexToOpen].path]; - dispatch('setFileActive', nextFileToOpen); + router.push(`/project${nextFileToOpen.url}`); } else if (!state.openFiles.length) { router.push(`/project/${file.projectId}/tree/${file.branchId}/`); } - dispatch('getLastCommitData'); + eventHub.$emit(`editor.update.model.dispose.${file.path}`); }; -export const setFileActive = ({ commit, state, getters, dispatch }, file) => { +export const setFileActive = ({ commit, state, getters, dispatch }, path) => { + const file = state.entries[path]; const currentActiveFile = getters.activeFile; if (file.active) return; if (currentActiveFile) { - commit(types.SET_FILE_ACTIVE, { file: currentActiveFile, active: false }); + commit(types.SET_FILE_ACTIVE, { + path: currentActiveFile.path, + active: false, + }); } - commit(types.SET_FILE_ACTIVE, { file, active: true }); + commit(types.SET_FILE_ACTIVE, { path, active: true }); dispatch('scrollToTab'); - // reset hash for line highlighting - location.hash = ''; - commit(types.SET_CURRENT_PROJECT, file.projectId); commit(types.SET_CURRENT_BRANCH, file.branchId); }; export const getFileData = ({ state, commit, dispatch }, file) => { - commit(types.TOGGLE_LOADING, file); + commit(types.TOGGLE_LOADING, { entry: file }); - service.getFileData(file.url) - .then((res) => { + return service + .getFileData(file.url) + .then(res => { const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']); setPageTitle(pageTitle); return res.json(); }) - .then((data) => { + .then(data => { commit(types.SET_FILE_DATA, { data, file }); - commit(types.TOGGLE_FILE_OPEN, file); - dispatch('setFileActive', file); - commit(types.TOGGLE_LOADING, file); + commit(types.TOGGLE_FILE_OPEN, file.path); + dispatch('setFileActive', file.path); + commit(types.TOGGLE_LOADING, { entry: file }); }) .catch(() => { - commit(types.TOGGLE_LOADING, file); - flash('Error loading file data. Please try again.', 'alert', document, null, false, true); + commit(types.TOGGLE_LOADING, { entry: file }); + flash( + 'Error loading file data. Please try again.', + 'alert', + document, + null, + false, + true, + ); }); }; -export const getRawFileData = ({ commit, dispatch }, file) => service.getRawFileData(file) - .then((raw) => { - commit(types.SET_FILE_RAW_DATA, { file, raw }); - }) - .catch(() => flash('Error loading file content. Please try again.', 'alert', document, null, false, true)); - -export const changeFileContent = ({ commit }, { file, content }) => { - commit(types.UPDATE_FILE_CONTENT, { file, content }); +export const getRawFileData = ({ commit, dispatch }, file) => + service + .getRawFileData(file) + .then(raw => { + commit(types.SET_FILE_RAW_DATA, { file, raw }); + }) + .catch(() => + flash( + 'Error loading file content. Please try again.', + 'alert', + document, + null, + false, + true, + ), + ); + +export const changeFileContent = ({ state, commit }, { path, content }) => { + const file = state.entries[path]; + commit(types.UPDATE_FILE_CONTENT, { path, content }); + + const indexOfChangedFile = state.changedFiles.findIndex(f => f.path === path); + + if (file.changed && indexOfChangedFile === -1) { + commit(types.ADD_FILE_TO_CHANGED, path); + } else if (!file.changed && indexOfChangedFile !== -1) { + commit(types.REMOVE_FILE_FROM_CHANGED, path); + } }; -export const setFileLanguage = ({ state, commit }, { fileLanguage }) => { - if (state.selectedFile) { - commit(types.SET_FILE_LANGUAGE, { file: state.selectedFile, fileLanguage }); +export const setFileLanguage = ({ getters, commit }, { fileLanguage }) => { + if (getters.activeFile) { + commit(types.SET_FILE_LANGUAGE, { file: getters.activeFile, fileLanguage }); } }; -export const setFileEOL = ({ state, commit }, { eol }) => { - if (state.selectedFile) { - commit(types.SET_FILE_EOL, { file: state.selectedFile, eol }); +export const setFileEOL = ({ getters, commit }, { eol }) => { + if (getters.activeFile) { + commit(types.SET_FILE_EOL, { file: getters.activeFile, eol }); } }; -export const setEditorPosition = ({ state, commit }, { editorRow, editorColumn }) => { - if (state.selectedFile) { - commit(types.SET_FILE_POSITION, { file: state.selectedFile, editorRow, editorColumn }); +export const setEditorPosition = ( + { getters, commit }, + { editorRow, editorColumn }, +) => { + if (getters.activeFile) { + commit(types.SET_FILE_POSITION, { + file: getters.activeFile, + editorRow, + editorColumn, + }); } }; -export const createTempFile = ({ state, commit, dispatch }, { projectId, branchId, parent, name, content = '', base64 = '' }) => { - const path = parent.path !== undefined ? parent.path : ''; - // We need to do the replacement otherwise the web_url + file.url duplicate - const newUrl = `/${projectId}/blob/${branchId}/${path}${path ? '/' : ''}${name}`; - const file = createTemp({ - projectId, - branchId, - name: name.replace(`${path}/`, ''), - path, - type: 'blob', - level: parent.level !== undefined ? parent.level + 1 : 0, - changed: true, - content, - base64, - url: newUrl, - }); - - if (findEntry(parent.tree, 'blob', file.name)) return flash(`The name "${file.name}" is already taken in this directory.`, 'alert', document, null, false, true); - - commit(types.CREATE_TMP_FILE, { - parent, - file, - }); - commit(types.TOGGLE_FILE_OPEN, file); - dispatch('setFileActive', file); - - if (!state.editMode && !file.base64) { - dispatch('toggleEditMode', true); - } +export const discardFileChanges = ({ state, commit }, path) => { + const file = state.entries[path]; - router.push(`/project${file.url}`); + commit(types.DISCARD_FILE_CHANGES, path); + commit(types.REMOVE_FILE_FROM_CHANGED, path); + + if (file.tempFile && file.opened) { + commit(types.TOGGLE_FILE_OPEN, path); + } - return Promise.resolve(file); + eventHub.$emit(`editor.update.model.content.${file.path}`, file.raw); }; diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js index faeceb430a2..b3882cb8d21 100644 --- a/app/assets/javascripts/ide/stores/actions/project.js +++ b/app/assets/javascripts/ide/stores/actions/project.js @@ -1,18 +1,17 @@ +import flash from '~/flash'; import service from '../../services'; -import flash from '../../../flash'; import * as types from '../mutation_types'; -// eslint-disable-next-line import/prefer-default-export export const getProjectData = ( { commit, state, dispatch }, { namespace, projectId, force = false } = {}, ) => new Promise((resolve, reject) => { if (!state.projects[`${namespace}/${projectId}`] || force) { - commit(types.TOGGLE_LOADING, state); + commit(types.TOGGLE_LOADING, { entry: state }); service.getProjectData(namespace, projectId) .then(res => res.data) .then((data) => { - commit(types.TOGGLE_LOADING, state); + commit(types.TOGGLE_LOADING, { entry: state }); commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data }); if (!state.currentProjectId) commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`); resolve(data); @@ -25,3 +24,26 @@ export const getProjectData = ( resolve(state.projects[`${namespace}/${projectId}`]); } }); + +export const getBranchData = ( + { commit, state, dispatch }, + { projectId, branchId, force = false } = {}, +) => new Promise((resolve, reject) => { + if ((typeof state.projects[`${projectId}`] === 'undefined' || + !state.projects[`${projectId}`].branches[branchId]) + || force) { + service.getBranchData(`${projectId}`, branchId) + .then(({ data }) => { + const { id } = data.commit; + commit(types.SET_BRANCH, { projectPath: `${projectId}`, branchName: branchId, branch: data }); + commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id }); + resolve(data); + }) + .catch(() => { + flash('Error loading branch data. Please try again.', 'alert', document, null, false, true); + reject(new Error(`Branch not loaded - ${projectId}/${branchId}`)); + }); + } else { + resolve(state.projects[`${projectId}`].branches[branchId]); + } +}); diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js index 302ba45edee..70a969a0325 100644 --- a/app/assets/javascripts/ide/stores/actions/tree.js +++ b/app/assets/javascripts/ide/stores/actions/tree.js @@ -1,134 +1,30 @@ -import { visitUrl } from '../../../lib/utils/url_utility'; -import { normalizeHeaders } from '../../../lib/utils/common_utils'; -import flash from '../../../flash'; +import { normalizeHeaders } from '~/lib/utils/common_utils'; +import flash from '~/flash'; import service from '../../services'; import * as types from '../mutation_types'; -import router from '../../ide_router'; import { - setPageTitle, findEntry, - createTemp, - createOrMergeEntry, } from '../utils'; +import FilesDecoratorWorker from '../workers/files_decorator_worker'; -export const getTreeData = ( - { commit, state, dispatch }, - { endpoint, tree = null, projectId, branch, force = false } = {}, -) => new Promise((resolve, reject) => { - // We already have the base tree so we resolve immediately - if (!tree && state.trees[`${projectId}/${branch}`] && !force) { - resolve(); - } else { - if (tree) commit(types.TOGGLE_LOADING, tree); - const selectedProject = state.projects[projectId]; - // We are merging the web_url that we got on the project info with the endpoint - // we got on the tree entry, as both contain the projectId, we replace it in the tree endpoint - const completeEndpoint = selectedProject.web_url + (endpoint).replace(projectId, ''); - if (completeEndpoint && (!tree || !tree.tempFile)) { - service.getTreeData(completeEndpoint) - .then((res) => { - const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']); - - setPageTitle(pageTitle); - - return res.json(); - }) - .then((data) => { - if (!state.isInitialRoot) { - commit(types.SET_ROOT, data.path === '/'); - } - - dispatch('updateDirectoryData', { data, tree, projectId, branch }); - const selectedTree = tree || state.trees[`${projectId}/${branch}`]; - - commit(types.SET_PARENT_TREE_URL, data.parent_tree_url); - commit(types.SET_LAST_COMMIT_URL, { tree: selectedTree, url: data.last_commit_path }); - if (tree) commit(types.TOGGLE_LOADING, selectedTree); - - const prevLastCommitPath = selectedTree.lastCommitPath; - if (prevLastCommitPath !== null) { - dispatch('getLastCommitData', selectedTree); - } - resolve(data); - }) - .catch((e) => { - flash('Error loading tree data. Please try again.', 'alert', document, null, false, true); - if (tree) commit(types.TOGGLE_LOADING, tree); - reject(e); - }); - } else { - resolve(); - } - } -}); - -export const toggleTreeOpen = ({ commit, dispatch }, { endpoint, tree }) => { - if (tree.opened) { - // send empty data to clear the tree - const data = { trees: [], blobs: [], submodules: [] }; - - dispatch('updateDirectoryData', { data, tree, projectId: tree.projectId, branchId: tree.branchId }); - } else { - dispatch('getTreeData', { endpoint, tree, projectId: tree.projectId, branch: tree.branchId }); - } - - commit(types.TOGGLE_TREE_OPEN, tree); +export const toggleTreeOpen = ({ commit, dispatch }, path) => { + commit(types.TOGGLE_TREE_OPEN, path); }; export const handleTreeEntryAction = ({ commit, dispatch }, row) => { if (row.type === 'tree') { - dispatch('toggleTreeOpen', { - endpoint: row.url, - tree: row, - }); - } else if (row.type === 'submodule') { - commit(types.TOGGLE_LOADING, row); - visitUrl(row.url); - } else if (row.type === 'blob' && row.opened) { - dispatch('setFileActive', row); + dispatch('toggleTreeOpen', row.path); + } else if (row.type === 'blob' && (row.opened || row.changed)) { + if (row.changed && !row.opened) { + commit(types.TOGGLE_FILE_OPEN, row.path); + } + + dispatch('setFileActive', row.path); } else { dispatch('getFileData', row); } }; -export const createTempTree = ( - { state, commit, dispatch }, - { projectId, branchId, parent, name }, -) => { - let selectedTree = parent; - const dirNames = name.replace(new RegExp(`^${state.path}/`), '').split('/'); - - dirNames.forEach((dirName) => { - const foundEntry = findEntry(selectedTree.tree, 'tree', dirName); - - if (!foundEntry) { - const path = selectedTree.path !== undefined ? selectedTree.path : ''; - const tmpEntry = createTemp({ - projectId, - branchId, - name: dirName, - path, - type: 'tree', - level: selectedTree.level !== undefined ? selectedTree.level + 1 : 0, - tree: [], - url: `/${projectId}/blob/${branchId}/${path}${path ? '/' : ''}${dirName}`, - }); - - commit(types.CREATE_TMP_TREE, { - parent: selectedTree, - tmpEntry, - }); - commit(types.TOGGLE_TREE_OPEN, tmpEntry); - - router.push(`/project${tmpEntry.url}`); - - selectedTree = tmpEntry; - } else { - selectedTree = foundEntry; - } - }); -}; - export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => { if (!tree || tree.lastCommitPath === null || !tree.lastCommitPath) return; @@ -154,35 +50,44 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s .catch(() => flash('Error fetching log data.', 'alert', document, null, false, true)); }; -export const updateDirectoryData = ( - { commit, state }, - { data, tree, projectId, branch }, -) => { - if (!tree) { - const existingTree = state.trees[`${projectId}/${branch}`]; - if (!existingTree) { - commit(types.CREATE_TREE, { treePath: `${projectId}/${branch}` }); - } +export const getFiles = ( + { state, commit, dispatch }, + { projectId, branchId } = {}, +) => new Promise((resolve, reject) => { + if (!state.trees[`${projectId}/${branchId}`]) { + const selectedProject = state.projects[projectId]; + commit(types.CREATE_TREE, { treePath: `${projectId}/${branchId}` }); + + service + .getFiles(selectedProject.web_url, branchId) + .then(res => res.json()) + .then((data) => { + const worker = new FilesDecoratorWorker(); + worker.addEventListener('message', (e) => { + const { entries, treeList } = e.data; + const selectedTree = state.trees[`${projectId}/${branchId}`]; + + commit(types.SET_ENTRIES, entries); + commit(types.SET_DIRECTORY_DATA, { treePath: `${projectId}/${branchId}`, data: treeList }); + commit(types.TOGGLE_LOADING, { entry: selectedTree, forceValue: false }); + + worker.terminate(); + + resolve(); + }); + + worker.postMessage({ + data, + projectId, + branchId, + }); + }) + .catch((e) => { + flash('Error loading tree data. Please try again.', 'alert', document, null, false, true); + reject(e); + }); + } else { + resolve(); } +}); - const selectedTree = tree || state.trees[`${projectId}/${branch}`]; - const level = selectedTree.level !== undefined ? selectedTree.level + 1 : 0; - const parentTreeUrl = data.parent_tree_url ? `${data.parent_tree_url}${data.path}` : state.endpoints.rootUrl; - const createEntry = (entry, type) => createOrMergeEntry({ - tree: selectedTree, - projectId: `${projectId}`, - branchId: branch, - entry, - level, - type, - parentTreeUrl, - }); - - const formattedData = [ - ...data.trees.map(t => createEntry(t, 'tree')), - ...data.submodules.map(m => createEntry(m, 'submodule')), - ...data.blobs.map(b => createEntry(b, 'blob')), - ]; - - commit(types.SET_DIRECTORY_DATA, { tree: selectedTree, data: formattedData }); -}; diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js index 6b51ccff817..eba325a31df 100644 --- a/app/assets/javascripts/ide/stores/getters.js +++ b/app/assets/javascripts/ide/stores/getters.js @@ -1,19 +1,30 @@ -export const changedFiles = state => state.openFiles.filter(file => file.changed); +export const activeFile = state => + state.openFiles.find(file => file.active) || null; -export const activeFile = state => state.openFiles.find(file => file.active) || null; +export const addedFiles = state => state.changedFiles.filter(f => f.tempFile); -export const activeFileExtension = (state) => { - const file = activeFile(state); - return file ? `.${file.path.split('.').pop()}` : ''; -}; +export const modifiedFiles = state => + state.changedFiles.filter(f => !f.tempFile); -export const canEditFile = (state) => { - const currentActiveFile = activeFile(state); +export const projectsWithTrees = state => + Object.keys(state.projects).map(projectId => { + const project = state.projects[projectId]; - return state.canCommit && - (currentActiveFile && !currentActiveFile.renderError && !currentActiveFile.binary); -}; + return { + ...project, + branches: Object.keys(project.branches).map(branchId => { + const branch = project.branches[branchId]; -export const addedFiles = state => changedFiles(state).filter(f => f.tempFile); + return { + ...branch, + tree: state.trees[branch.treeId], + }; + }), + }; + }); -export const modifiedFiles = state => changedFiles(state).filter(f => !f.tempFile); +// eslint-disable-next-line no-confusing-arrow +export const currentIcon = state => + state.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right'; + +export const hasChanges = state => !!state.changedFiles.length; diff --git a/app/assets/javascripts/ide/stores/index.js b/app/assets/javascripts/ide/stores/index.js index 6ac9bfd8189..7c82ce7976b 100644 --- a/app/assets/javascripts/ide/stores/index.js +++ b/app/assets/javascripts/ide/stores/index.js @@ -4,6 +4,7 @@ import state from './state'; import * as actions from './actions'; import * as getters from './getters'; import mutations from './mutations'; +import commitModule from './modules/commit'; Vue.use(Vuex); @@ -12,4 +13,7 @@ export default new Vuex.Store({ actions, mutations, getters, + modules: { + commit: commitModule, + }, }); diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js new file mode 100644 index 00000000000..f536ce6344b --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js @@ -0,0 +1,218 @@ +import $ from 'jquery'; +import { sprintf, __ } from '~/locale'; +import flash from '~/flash'; +import { stripHtml } from '~/lib/utils/text_utility'; +import * as rootTypes from '../../mutation_types'; +import { createCommitPayload, createNewMergeRequestUrl } from '../../utils'; +import router from '../../../ide_router'; +import service from '../../../services'; +import * as types from './mutation_types'; +import * as consts from './constants'; +import eventHub from '../../../eventhub'; + +export const updateCommitMessage = ({ commit }, message) => { + commit(types.UPDATE_COMMIT_MESSAGE, message); +}; + +export const discardDraft = ({ commit }) => { + commit(types.UPDATE_COMMIT_MESSAGE, ''); +}; + +export const updateCommitAction = ({ commit }, commitAction) => { + commit(types.UPDATE_COMMIT_ACTION, commitAction); +}; + +export const updateBranchName = ({ commit }, branchName) => { + commit(types.UPDATE_NEW_BRANCH_NAME, branchName); +}; + +export const setLastCommitMessage = ({ rootState, commit }, data) => { + const currentProject = rootState.projects[rootState.currentProjectId]; + const commitStats = data.stats + ? sprintf(__('with %{additions} additions, %{deletions} deletions.'), { + additions: data.stats.additions, // eslint-disable-line indent + deletions: data.stats.deletions, // eslint-disable-line indent + }) // eslint-disable-line indent + : ''; + const commitMsg = sprintf( + __('Your changes have been committed. Commit %{commitId} %{commitStats}'), + { + commitId: `<a href="${currentProject.web_url}/commit/${ + data.short_id + }" class="commit-sha">${data.short_id}</a>`, + commitStats, + }, + false, + ); + + commit(rootTypes.SET_LAST_COMMIT_MSG, commitMsg, { root: true }); +}; + +export const checkCommitStatus = ({ rootState }) => + service + .getBranchData(rootState.currentProjectId, rootState.currentBranchId) + .then(({ data }) => { + const { id } = data.commit; + const selectedBranch = + rootState.projects[rootState.currentProjectId].branches[ + rootState.currentBranchId + ]; + + if (selectedBranch.workingReference !== id) { + return true; + } + + return false; + }) + .catch(() => + flash( + __('Error checking branch data. Please try again.'), + 'alert', + document, + null, + false, + true, + ), + ); + +export const updateFilesAfterCommit = ( + { commit, dispatch, state, rootState, rootGetters }, + { data, branch }, +) => { + const selectedProject = rootState.projects[rootState.currentProjectId]; + const lastCommit = { + commit_path: `${selectedProject.web_url}/commit/${data.id}`, + commit: { + id: data.id, + message: data.message, + authored_date: data.committed_date, + author_name: data.committer_name, + }, + }; + + commit( + rootTypes.SET_BRANCH_WORKING_REFERENCE, + { + projectId: rootState.currentProjectId, + branchId: rootState.currentBranchId, + reference: data.id, + }, + { root: true }, + ); + + rootState.changedFiles.forEach(entry => { + commit( + rootTypes.SET_LAST_COMMIT_DATA, + { + entry, + lastCommit, + }, + { root: true }, + ); + + eventHub.$emit(`editor.update.model.content.${entry.path}`, entry.content); + + commit( + rootTypes.SET_FILE_RAW_DATA, + { + file: entry, + raw: entry.content, + }, + { root: true }, + ); + + commit( + rootTypes.TOGGLE_FILE_CHANGED, + { + file: entry, + changed: false, + }, + { root: true }, + ); + }); + + commit(rootTypes.REMOVE_ALL_CHANGES_FILES, null, { root: true }); + + if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH) { + router.push( + `/project/${rootState.currentProjectId}/blob/${branch}/${ + rootGetters.activeFile.path + }`, + ); + } + + dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH); +}; + +export const commitChanges = ({ + commit, + state, + getters, + dispatch, + rootState, +}) => { + const newBranch = state.commitAction !== consts.COMMIT_TO_CURRENT_BRANCH; + const payload = createCommitPayload( + getters.branchName, + newBranch, + state, + rootState, + ); + const getCommitStatus = newBranch + ? Promise.resolve(false) + : dispatch('checkCommitStatus'); + + commit(types.UPDATE_LOADING, true); + + return getCommitStatus + .then( + branchChanged => + new Promise(resolve => { + if (branchChanged) { + // show the modal with a Bootstrap call + $('#ide-create-branch-modal').modal('show'); + } else { + resolve(); + } + }), + ) + .then(() => service.commit(rootState.currentProjectId, payload)) + .then(({ data }) => { + commit(types.UPDATE_LOADING, false); + + if (!data.short_id) { + flash(data.message, 'alert', document, null, false, true); + return; + } + + dispatch('setLastCommitMessage', data); + dispatch('updateCommitMessage', ''); + + if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH_MR) { + dispatch( + 'redirectToUrl', + createNewMergeRequestUrl( + rootState.projects[rootState.currentProjectId].web_url, + getters.branchName, + rootState.currentBranchId, + ), + { root: true }, + ); + } else { + dispatch('updateFilesAfterCommit', { + data, + branch: getters.branchName, + }); + } + }) + .catch(err => { + let errMsg = __('Error committing changes. Please try again.'); + if (err.response.data && err.response.data.message) { + errMsg += ` (${stripHtml(err.response.data.message)})`; + } + flash(errMsg, 'alert', document, null, false, true); + window.dispatchEvent(new Event('resize')); + + commit(types.UPDATE_LOADING, false); + }); +}; diff --git a/app/assets/javascripts/ide/stores/modules/commit/constants.js b/app/assets/javascripts/ide/stores/modules/commit/constants.js new file mode 100644 index 00000000000..230b0a3d9b5 --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/commit/constants.js @@ -0,0 +1,3 @@ +export const COMMIT_TO_CURRENT_BRANCH = '1'; +export const COMMIT_TO_NEW_BRANCH = '2'; +export const COMMIT_TO_NEW_BRANCH_MR = '3'; diff --git a/app/assets/javascripts/ide/stores/modules/commit/getters.js b/app/assets/javascripts/ide/stores/modules/commit/getters.js new file mode 100644 index 00000000000..f7cdd6adb0c --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/commit/getters.js @@ -0,0 +1,24 @@ +import * as consts from './constants'; + +export const discardDraftButtonDisabled = state => state.commitMessage === '' || state.submitCommitLoading; + +export const commitButtonDisabled = (state, getters, rootState) => + getters.discardDraftButtonDisabled || !rootState.changedFiles.length; + +export const newBranchName = (state, _, rootState) => + `${gon.current_username}-${rootState.currentBranchId}-patch-${`${new Date().getTime()}`.substr(-5)}`; + +export const branchName = (state, getters, rootState) => { + if ( + state.commitAction === consts.COMMIT_TO_NEW_BRANCH || + state.commitAction === consts.COMMIT_TO_NEW_BRANCH_MR + ) { + if (state.newBranchName === '') { + return getters.newBranchName; + } + + return state.newBranchName; + } + + return rootState.currentBranchId; +}; diff --git a/app/assets/javascripts/ide/stores/modules/commit/index.js b/app/assets/javascripts/ide/stores/modules/commit/index.js new file mode 100644 index 00000000000..3bf65b02847 --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/commit/index.js @@ -0,0 +1,12 @@ +import state from './state'; +import mutations from './mutations'; +import * as actions from './actions'; +import * as getters from './getters'; + +export default { + namespaced: true, + state: state(), + mutations, + actions, + getters, +}; diff --git a/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js b/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js new file mode 100644 index 00000000000..9221f054e9f --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js @@ -0,0 +1,4 @@ +export const UPDATE_COMMIT_MESSAGE = 'UPDATE_COMMIT_MESSAGE'; +export const UPDATE_COMMIT_ACTION = 'UPDATE_COMMIT_ACTION'; +export const UPDATE_NEW_BRANCH_NAME = 'UPDATE_NEW_BRANCH_NAME'; +export const UPDATE_LOADING = 'UPDATE_LOADING'; diff --git a/app/assets/javascripts/ide/stores/modules/commit/mutations.js b/app/assets/javascripts/ide/stores/modules/commit/mutations.js new file mode 100644 index 00000000000..797357e3df9 --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/commit/mutations.js @@ -0,0 +1,24 @@ +import * as types from './mutation_types'; + +export default { + [types.UPDATE_COMMIT_MESSAGE](state, commitMessage) { + Object.assign(state, { + commitMessage, + }); + }, + [types.UPDATE_COMMIT_ACTION](state, commitAction) { + Object.assign(state, { + commitAction, + }); + }, + [types.UPDATE_NEW_BRANCH_NAME](state, newBranchName) { + Object.assign(state, { + newBranchName, + }); + }, + [types.UPDATE_LOADING](state, submitCommitLoading) { + Object.assign(state, { + submitCommitLoading, + }); + }, +}; diff --git a/app/assets/javascripts/ide/stores/modules/commit/state.js b/app/assets/javascripts/ide/stores/modules/commit/state.js new file mode 100644 index 00000000000..8dae50961b0 --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/commit/state.js @@ -0,0 +1,6 @@ +export default () => ({ + commitMessage: '', + commitAction: '1', + newBranchName: '', + submitCommitLoading: false, +}); diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js index 69b218a5e7d..e28f190897c 100644 --- a/app/assets/javascripts/ide/stores/mutation_types.js +++ b/app/assets/javascripts/ide/stores/mutation_types.js @@ -1,8 +1,7 @@ export const SET_INITIAL_DATA = 'SET_INITIAL_DATA'; export const TOGGLE_LOADING = 'TOGGLE_LOADING'; -export const SET_PARENT_TREE_URL = 'SET_PARENT_TREE_URL'; -export const SET_ROOT = 'SET_ROOT'; export const SET_LAST_COMMIT_DATA = 'SET_LAST_COMMIT_DATA'; +export const SET_LAST_COMMIT_MSG = 'SET_LAST_COMMIT_MSG'; export const SET_LEFT_PANEL_COLLAPSED = 'SET_LEFT_PANEL_COLLAPSED'; export const SET_RIGHT_PANEL_COLLAPSED = 'SET_RIGHT_PANEL_COLLAPSED'; export const SET_RESIZING_STATUS = 'SET_RESIZING_STATUS'; @@ -20,9 +19,9 @@ export const TOGGLE_BRANCH_OPEN = 'TOGGLE_BRANCH_OPEN'; // Tree mutation types export const SET_DIRECTORY_DATA = 'SET_DIRECTORY_DATA'; export const TOGGLE_TREE_OPEN = 'TOGGLE_TREE_OPEN'; -export const CREATE_TMP_TREE = 'CREATE_TMP_TREE'; export const SET_LAST_COMMIT_URL = 'SET_LAST_COMMIT_URL'; export const CREATE_TREE = 'CREATE_TREE'; +export const REMOVE_ALL_CHANGES_FILES = 'REMOVE_ALL_CHANGES_FILES'; // File mutation types export const SET_FILE_DATA = 'SET_FILE_DATA'; @@ -34,13 +33,11 @@ export const SET_FILE_LANGUAGE = 'SET_FILE_LANGUAGE'; export const SET_FILE_POSITION = 'SET_FILE_POSITION'; export const SET_FILE_EOL = 'SET_FILE_EOL'; export const DISCARD_FILE_CHANGES = 'DISCARD_FILE_CHANGES'; -export const CREATE_TMP_FILE = 'CREATE_TMP_FILE'; - -// Viewer mutation types -export const SET_PREVIEW_MODE = 'SET_PREVIEW_MODE'; -export const SET_EDIT_MODE = 'SET_EDIT_MODE'; -export const TOGGLE_EDIT_MODE = 'TOGGLE_EDIT_MODE'; -export const TOGGLE_DISCARD_POPUP = 'TOGGLE_DISCARD_POPUP'; - +export const ADD_FILE_TO_CHANGED = 'ADD_FILE_TO_CHANGED'; +export const REMOVE_FILE_FROM_CHANGED = 'REMOVE_FILE_FROM_CHANGED'; +export const TOGGLE_FILE_CHANGED = 'TOGGLE_FILE_CHANGED'; export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH'; - +export const SET_ENTRIES = 'SET_ENTRIES'; +export const CREATE_TMP_ENTRY = 'CREATE_TMP_ENTRY'; +export const UPDATE_VIEWER = 'UPDATE_VIEWER'; +export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE'; diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js index 03d81be10a1..da41fc9285c 100644 --- a/app/assets/javascripts/ide/stores/mutations.js +++ b/app/assets/javascripts/ide/stores/mutations.js @@ -8,36 +8,19 @@ export default { [types.SET_INITIAL_DATA](state, data) { Object.assign(state, data); }, - [types.SET_PREVIEW_MODE](state) { - Object.assign(state, { - currentBlobView: 'repo-preview', - }); - }, - [types.SET_EDIT_MODE](state) { - Object.assign(state, { - currentBlobView: 'repo-editor', - }); - }, - [types.TOGGLE_LOADING](state, entry) { - Object.assign(entry, { - loading: !entry.loading, - }); - }, - [types.TOGGLE_EDIT_MODE](state) { - Object.assign(state, { - editMode: !state.editMode, - }); - }, - [types.TOGGLE_DISCARD_POPUP](state, discardPopupOpen) { - Object.assign(state, { - discardPopupOpen, - }); - }, - [types.SET_ROOT](state, isRoot) { - Object.assign(state, { - isRoot, - isInitialRoot: isRoot, - }); + [types.TOGGLE_LOADING](state, { entry, forceValue = undefined }) { + if (entry.path) { + Object.assign(state.entries[entry.path], { + loading: + forceValue !== undefined + ? forceValue + : !state.entries[entry.path].loading, + }); + } else { + Object.assign(entry, { + loading: forceValue !== undefined ? forceValue : !entry.loading, + }); + } }, [types.SET_LEFT_PANEL_COLLAPSED](state, collapsed) { Object.assign(state, { @@ -63,6 +46,59 @@ export default { updatedAt: lastCommit.commit.authored_date, }); }, + [types.SET_LAST_COMMIT_MSG](state, lastCommitMsg) { + Object.assign(state, { + lastCommitMsg, + }); + }, + [types.SET_ENTRIES](state, entries) { + Object.assign(state, { + entries, + }); + }, + [types.CREATE_TMP_ENTRY](state, { data, projectId, branchId }) { + Object.keys(data.entries).reduce((acc, key) => { + const entry = data.entries[key]; + const foundEntry = state.entries[key]; + + if (!foundEntry) { + Object.assign(state.entries, { + [key]: entry, + }); + } else { + const tree = entry.tree.filter( + f => foundEntry.tree.find(e => e.path === f.path) === undefined, + ); + Object.assign(foundEntry, { + tree: foundEntry.tree.concat(tree), + }); + } + + return acc.concat(key); + }, []); + + const foundEntry = state.trees[`${projectId}/${branchId}`].tree.find( + e => e.path === data.treeList[0].path, + ); + + if (!foundEntry) { + Object.assign(state.trees[`${projectId}/${branchId}`], { + tree: state.trees[`${projectId}/${branchId}`].tree.concat( + data.treeList, + ), + }); + } + }, + [types.UPDATE_VIEWER](state, viewer) { + Object.assign(state, { + viewer, + }); + }, + [types.UPDATE_DELAY_VIEWER_CHANGE](state, delayViewerUpdated) { + Object.assign(state, { + delayViewerUpdated, + }); + }, ...projectMutations, ...fileMutations, ...treeMutations, diff --git a/app/assets/javascripts/ide/stores/mutations/branch.js b/app/assets/javascripts/ide/stores/mutations/branch.js index 04b9582c5bb..2972ba5e38e 100644 --- a/app/assets/javascripts/ide/stores/mutations/branch.js +++ b/app/assets/javascripts/ide/stores/mutations/branch.js @@ -7,16 +7,14 @@ export default { }); }, [types.SET_BRANCH](state, { projectPath, branchName, branch }) { - // Add client side properties - Object.assign(branch, { - treeId: `${projectPath}/${branchName}`, - active: true, - workingReference: '', - }); - Object.assign(state.projects[projectPath], { branches: { - [branchName]: branch, + [branchName]: { + ...branch, + treeId: `${projectPath}/${branchName}`, + active: true, + workingReference: '', + }, }, }); }, diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js index 72db1c180c9..2500f13db7c 100644 --- a/app/assets/javascripts/ide/stores/mutations/file.js +++ b/app/assets/javascripts/ide/stores/mutations/file.js @@ -1,74 +1,83 @@ import * as types from '../mutation_types'; -import { findIndexOfFile } from '../utils'; export default { - [types.SET_FILE_ACTIVE](state, { file, active }) { - Object.assign(file, { + [types.SET_FILE_ACTIVE](state, { path, active }) { + Object.assign(state.entries[path], { active, }); - - Object.assign(state, { - selectedFile: file, - }); }, - [types.TOGGLE_FILE_OPEN](state, file) { - Object.assign(file, { - opened: !file.opened, + [types.TOGGLE_FILE_OPEN](state, path) { + Object.assign(state.entries[path], { + opened: !state.entries[path].opened, }); - if (file.opened) { - state.openFiles.push(file); + if (state.entries[path].opened) { + state.openFiles.push(state.entries[path]); } else { - state.openFiles.splice(findIndexOfFile(state.openFiles, file), 1); + Object.assign(state, { + openFiles: state.openFiles.filter(f => f.path !== path), + }); } }, [types.SET_FILE_DATA](state, { data, file }) { - Object.assign(file, { + Object.assign(state.entries[file.path], { + id: data.id, blamePath: data.blame_path, commitsPath: data.commits_path, permalink: data.permalink, rawPath: data.raw_path, binary: data.binary, - html: data.html, renderError: data.render_error, }); }, [types.SET_FILE_RAW_DATA](state, { file, raw }) { - Object.assign(file, { + Object.assign(state.entries[file.path], { raw, }); }, - [types.UPDATE_FILE_CONTENT](state, { file, content }) { - const changed = content !== file.raw; + [types.UPDATE_FILE_CONTENT](state, { path, content }) { + const changed = content !== state.entries[path].raw; - Object.assign(file, { + Object.assign(state.entries[path], { content, changed, }); }, [types.SET_FILE_LANGUAGE](state, { file, fileLanguage }) { - Object.assign(file, { + Object.assign(state.entries[file.path], { fileLanguage, }); }, [types.SET_FILE_EOL](state, { file, eol }) { - Object.assign(file, { + Object.assign(state.entries[file.path], { eol, }); }, [types.SET_FILE_POSITION](state, { file, editorRow, editorColumn }) { - Object.assign(file, { + Object.assign(state.entries[file.path], { editorRow, editorColumn, }); }, - [types.DISCARD_FILE_CHANGES](state, file) { - Object.assign(file, { - content: file.raw, + [types.DISCARD_FILE_CHANGES](state, path) { + Object.assign(state.entries[path], { + content: state.entries[path].raw, changed: false, }); }, - [types.CREATE_TMP_FILE](state, { file, parent }) { - parent.tree.push(file); + [types.ADD_FILE_TO_CHANGED](state, path) { + Object.assign(state, { + changedFiles: state.changedFiles.concat(state.entries[path]), + }); + }, + [types.REMOVE_FILE_FROM_CHANGED](state, path) { + Object.assign(state, { + changedFiles: state.changedFiles.filter(f => f.path !== path), + }); + }, + [types.TOGGLE_FILE_CHANGED](state, { file, changed }) { + Object.assign(state.entries[file.path], { + changed, + }); }, }; diff --git a/app/assets/javascripts/ide/stores/mutations/tree.js b/app/assets/javascripts/ide/stores/mutations/tree.js index 4fe438ab465..7f7e470c9bb 100644 --- a/app/assets/javascripts/ide/stores/mutations/tree.js +++ b/app/assets/javascripts/ide/stores/mutations/tree.js @@ -1,9 +1,9 @@ import * as types from '../mutation_types'; export default { - [types.TOGGLE_TREE_OPEN](state, tree) { - Object.assign(tree, { - opened: !tree.opened, + [types.TOGGLE_TREE_OPEN](state, path) { + Object.assign(state.entries[path], { + opened: !state.entries[path].opened, }); }, [types.CREATE_TREE](state, { treePath }) { @@ -11,18 +11,18 @@ export default { trees: Object.assign({}, state.trees, { [treePath]: { tree: [], + loading: true, }, }), }); }, - [types.SET_DIRECTORY_DATA](state, { data, tree }) { - Object.assign(tree, { - tree: data, - }); - }, - [types.SET_PARENT_TREE_URL](state, url) { + [types.SET_DIRECTORY_DATA](state, { data, treePath }) { Object.assign(state, { - parentTreeUrl: url, + trees: Object.assign(state.trees, { + [treePath]: { + tree: data, + }, + }), }); }, [types.SET_LAST_COMMIT_URL](state, { tree = state, url }) { @@ -30,7 +30,9 @@ export default { lastCommitPath: url, }); }, - [types.CREATE_TMP_TREE](state, { parent, tmpEntry }) { - parent.tree.push(tmpEntry); + [types.REMOVE_ALL_CHANGES_FILES](state) { + Object.assign(state, { + changedFiles: [], + }); }, }; diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js index 61d12096946..6110f54951c 100644 --- a/app/assets/javascripts/ide/stores/state.js +++ b/app/assets/javascripts/ide/stores/state.js @@ -1,23 +1,19 @@ export default () => ({ - canCommit: false, currentProjectId: '', currentBranchId: '', - currentBlobView: 'repo-editor', - discardPopupOpen: false, - editMode: true, + changedFiles: [], endpoints: {}, - isRoot: false, - isInitialRoot: false, + lastCommitMsg: '', lastCommitPath: '', loading: false, - onTopOfBranch: false, openFiles: [], - selectedFile: null, - path: '', parentTreeUrl: '', trees: {}, projects: {}, leftPanelCollapsed: false, - rightPanelCollapsed: true, + rightPanelCollapsed: false, panelResizing: false, + entries: {}, + viewer: 'editor', + delayViewerUpdated: false, }); diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js index d556404faa5..487ea1ead8e 100644 --- a/app/assets/javascripts/ide/stores/utils.js +++ b/app/assets/javascripts/ide/stores/utils.js @@ -1,5 +1,3 @@ -import _ from 'underscore'; - export const dataStructure = () => ({ id: '', key: '', @@ -9,9 +7,7 @@ export const dataStructure = () => ({ name: '', url: '', path: '', - level: 0, tempFile: false, - icon: '', tree: [], loading: false, opened: false, @@ -25,7 +21,6 @@ export const dataStructure = () => ({ updatedAt: '', author: '', }, - tree_url: '', blamePath: '', commitsPath: '', permalink: '', @@ -51,8 +46,6 @@ export const decorateData = (entity) => { type, url, name, - icon, - tree_url, path, renderError, content = '', @@ -61,8 +54,10 @@ export const decorateData = (entity) => { opened = false, changed = false, parentTreeUrl = '', - level = 0, base64 = false, + + file_lock, + } = entity; return { @@ -74,11 +69,8 @@ export const decorateData = (entity) => { type, name, url, - tree_url, path, - level, tempFile, - icon: `fa-${icon}`, opened, active, parentTreeUrl, @@ -86,37 +78,14 @@ export const decorateData = (entity) => { renderError, content, base64, - }; -}; - -/* - Takes the multi-dimensional tree and returns a flattened array. - This allows for the table to recursively render the table rows but keeps the data - structure nested to make it easier to add new files/directories. -*/ -export const treeList = (state, treeId) => { - const baseTree = state.trees[treeId]; - if (baseTree) { - const mapTree = arr => (!arr.tree || !arr.tree.length ? - [] : _.map(arr.tree, a => [a, mapTree(a)])); - - return _.chain(baseTree.tree) - .map(arr => [arr, mapTree(arr)]) - .flatten() - .value(); - } - return []; -}; -export const getTree = state => (namespace, projectId, branch) => state.trees[`${namespace}/${projectId}/${branch}`]; + file_lock, -export const getTreeEntry = (store, treeId, path) => { - const fileList = treeList(store.state, treeId); - return fileList ? fileList.find(file => file.path === path) : null; + }; }; -export const findEntry = (tree, type, name) => tree.find( - f => f.type === type && f.name === name, +export const findEntry = (tree, type, name, prop = 'name') => tree.find( + f => f.type === type && f[prop] === name, ); export const findIndexOfFile = (state, file) => state.findIndex(f => f.path === file.path); @@ -125,53 +94,32 @@ export const setPageTitle = (title) => { document.title = title; }; -export const createTemp = ({ - projectId, branchId, name, path, type, level, changed, content, base64, url, -}) => { - const treePath = path ? `${path}/${name}` : name; - - return decorateData({ - id: new Date().getTime().toString(), - projectId, - branchId, - name, - type, - tempFile: true, - path: treePath, - icon: type === 'tree' ? 'folder' : 'file-text-o', - changed, - content, - parentTreeUrl: '', - level, - base64, - renderError: base64, - url, - }); -}; +export const createCommitPayload = (branch, newBranch, state, rootState) => ({ + branch, + commit_message: state.commitMessage, + actions: rootState.changedFiles.map(f => ({ + action: f.tempFile ? 'create' : 'update', + file_path: f.path, + content: f.content, + encoding: f.base64 ? 'base64' : 'text', + })), + start_branch: newBranch ? rootState.currentBranchId : undefined, +}); -export const createOrMergeEntry = ({ tree, - projectId, - branchId, - entry, - type, - parentTreeUrl, - level }) => { - const found = findEntry(tree.tree || tree, type, entry.name); +export const createNewMergeRequestUrl = (projectUrl, source, target) => + `${projectUrl}/merge_requests/new?merge_request[source_branch]=${source}&merge_request[target_branch]=${target}`; - if (found) { - return Object.assign({}, found, { - id: entry.id, - url: entry.url, - tempFile: false, - }); +const sortTreesByTypeAndName = (a, b) => { + if (a.type === 'tree' && b.type === 'blob') { + return -1; + } else if (a.type === 'blob' && b.type === 'tree') { + return 1; } - - return decorateData({ - ...entry, - projectId, - branchId, - type, - parentTreeUrl, - level, - }); + if (a.name.toLowerCase() < b.name.toLowerCase()) return -1; + if (a.name.toLowerCase() > b.name.toLowerCase()) return 1; + return 0; }; + +export const sortTree = sortedTree => sortedTree.map(entity => Object.assign(entity, { + tree: entity.tree.length ? sortTree(entity.tree) : [], +})).sort(sortTreesByTypeAndName); diff --git a/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js b/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js new file mode 100644 index 00000000000..a4cd1ab099f --- /dev/null +++ b/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js @@ -0,0 +1,101 @@ +import { decorateData, sortTree } from '../utils'; + +self.addEventListener('message', e => { + const { + data, + projectId, + branchId, + tempFile = false, + content = '', + base64 = false, + } = e.data; + + const treeList = []; + let file; + const entries = data.reduce((acc, path) => { + const pathSplit = path.split('/'); + const blobName = pathSplit.pop().trim(); + + if (pathSplit.length > 0) { + pathSplit.reduce((pathAcc, folderName) => { + const parentFolder = acc[pathAcc[pathAcc.length - 1]]; + const folderPath = `${ + parentFolder ? `${parentFolder.path}/` : '' + }${folderName}`; + const foundEntry = acc[folderPath]; + + if (!foundEntry) { + const tree = decorateData({ + projectId, + branchId, + id: folderPath, + name: folderName, + path: folderPath, + url: `/${projectId}/tree/${branchId}/${folderPath}/`, + type: 'tree', + parentTreeUrl: parentFolder + ? parentFolder.url + : `/${projectId}/tree/${branchId}/`, + tempFile, + changed: tempFile, + opened: tempFile, + }); + + Object.assign(acc, { + [folderPath]: tree, + }); + + if (parentFolder) { + parentFolder.tree.push(tree); + } else { + treeList.push(tree); + } + + pathAcc.push(tree.path); + } else { + pathAcc.push(foundEntry.path); + } + + return pathAcc; + }, []); + } + + if (blobName !== '') { + const fileFolder = acc[pathSplit.join('/')]; + file = decorateData({ + projectId, + branchId, + id: path, + name: blobName, + path, + url: `/${projectId}/blob/${branchId}/${path}`, + type: 'blob', + parentTreeUrl: fileFolder + ? fileFolder.url + : `/${projectId}/blob/${branchId}`, + tempFile, + changed: tempFile, + content, + base64, + }); + + Object.assign(acc, { + [path]: file, + }); + + if (fileFolder) { + fileFolder.tree.push(file); + } else { + treeList.push(file); + } + } + + return acc; + }, {}); + + self.postMessage({ + entries, + treeList: sortTree(treeList), + file, + }); +}); diff --git a/app/assets/javascripts/image_diff/image_diff.js b/app/assets/javascripts/image_diff/image_diff.js index f3af92cf2b0..fab0255c378 100644 --- a/app/assets/javascripts/image_diff/image_diff.js +++ b/app/assets/javascripts/image_diff/image_diff.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import imageDiffHelper from './helpers/index'; import ImageBadge from './image_badge'; import { isImageLoaded } from '../lib/utils/image_utility'; diff --git a/app/assets/javascripts/importer_status.js b/app/assets/javascripts/importer_status.js index 35094f8e73b..b469e1e2adc 100644 --- a/app/assets/javascripts/importer_status.js +++ b/app/assets/javascripts/importer_status.js @@ -1,11 +1,15 @@ -import { __ } from './locale'; +import $ from 'jquery'; +import _ from 'underscore'; +import { __, sprintf } from './locale'; import axios from './lib/utils/axios_utils'; import flash from './flash'; +import { convertPermissionToBoolean } from './lib/utils/common_utils'; class ImporterStatus { - constructor(jobsUrl, importUrl) { + constructor({ jobsUrl, importUrl, ciCdOnly }) { this.jobsUrl = jobsUrl; this.importUrl = importUrl; + this.ciCdOnly = ciCdOnly; this.initStatusPage(); this.setAutoUpdate(); } @@ -45,6 +49,7 @@ class ImporterStatus { repo_id: id, target_namespace: targetNamespace, new_name: newName, + ci_cd_only: this.ciCdOnly, }) .then(({ data }) => { const job = $(`tr#repo_${id}`); @@ -54,7 +59,13 @@ class ImporterStatus { $('table.import-jobs tbody').prepend(job); job.addClass('active'); - job.find('.import-actions').html('<i class="fa fa-spinner fa-spin" aria-label="importing"></i> started'); + const connectingVerb = this.ciCdOnly ? __('connecting') : __('importing'); + job.find('.import-actions').html(sprintf( + _.escape(__('%{loadingIcon} Started')), { + loadingIcon: `<i class="fa fa-spinner fa-spin" aria-label="${_.escape(connectingVerb)}"></i>`, + }, + false, + )); }) .catch(() => flash(__('An error occurred while importing project'))); } @@ -71,13 +82,16 @@ class ImporterStatus { switch (job.import_status) { case 'finished': jobItem.removeClass('active').addClass('success'); - statusField.html('<span><i class="fa fa-check"></i> done</span>'); + statusField.html(`<span><i class="fa fa-check"></i> ${__('Done')}</span>`); break; case 'scheduled': - statusField.html(`${spinner} scheduled`); + statusField.html(`${spinner} ${__('Scheduled')}`); break; case 'started': - statusField.html(`${spinner} started`); + statusField.html(`${spinner} ${__('Started')}`); + break; + case 'failed': + statusField.html(__('Failed')); break; default: statusField.html(job.import_status); @@ -98,7 +112,11 @@ function initImporterStatus() { if (importerStatus) { const data = importerStatus.dataset; - return new ImporterStatus(data.jobsImportPath, data.importPath); + return new ImporterStatus({ + jobsUrl: data.jobsImportPath, + importUrl: data.importPath, + ciCdOnly: convertPermissionToBoolean(data.ciCdOnly), + }); } } diff --git a/app/assets/javascripts/init_changes_dropdown.js b/app/assets/javascripts/init_changes_dropdown.js index 1bab7965c19..09cca1dc7d9 100644 --- a/app/assets/javascripts/init_changes_dropdown.js +++ b/app/assets/javascripts/init_changes_dropdown.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import stickyMonitor from './lib/utils/sticky'; export default (stickyTop) => { diff --git a/app/assets/javascripts/init_labels.js b/app/assets/javascripts/init_labels.js index 5f20055510f..15da5d5cceb 100644 --- a/app/assets/javascripts/init_labels.js +++ b/app/assets/javascripts/init_labels.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import LabelManager from './label_manager'; import GroupLabelSubscription from './group_label_subscription'; import ProjectLabelSubscription from './project_label_subscription'; diff --git a/app/assets/javascripts/integrations/integration_settings_form.js b/app/assets/javascripts/integrations/integration_settings_form.js index 2848fe003cb..741894b5e6c 100644 --- a/app/assets/javascripts/integrations/integration_settings_form.js +++ b/app/assets/javascripts/integrations/integration_settings_form.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import axios from '../lib/utils/axios_utils'; import flash from '../flash'; diff --git a/app/assets/javascripts/issuable/auto_width_dropdown_select.js b/app/assets/javascripts/issuable/auto_width_dropdown_select.js index 14a2bfbe4e0..b2c2de9e5de 100644 --- a/app/assets/javascripts/issuable/auto_width_dropdown_select.js +++ b/app/assets/javascripts/issuable/auto_width_dropdown_select.js @@ -1,3 +1,5 @@ +import $ from 'jquery'; + let instanceCount = 0; class AutoWidthDropdownSelect { diff --git a/app/assets/javascripts/issuable_bulk_update_actions.js b/app/assets/javascripts/issuable_bulk_update_actions.js index 8c1b2e78ca4..e003fb1d127 100644 --- a/app/assets/javascripts/issuable_bulk_update_actions.js +++ b/app/assets/javascripts/issuable_bulk_update_actions.js @@ -1,4 +1,6 @@ /* eslint-disable comma-dangle, quotes, consistent-return, func-names, array-callback-return, space-before-function-paren, prefer-arrow-callback, max-len, no-unused-expressions, no-sequences, no-underscore-dangle, no-unused-vars, no-param-reassign */ + +import $ from 'jquery'; import _ from 'underscore'; import axios from './lib/utils/axios_utils'; import Flash from './flash'; diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable_bulk_update_sidebar.js index 2056efe701b..2307c8e0d85 100644 --- a/app/assets/javascripts/issuable_bulk_update_sidebar.js +++ b/app/assets/javascripts/issuable_bulk_update_sidebar.js @@ -1,5 +1,6 @@ /* eslint-disable class-methods-use-this, no-new */ +import $ from 'jquery'; import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; import MilestoneSelect from './milestone_select'; import issueStatusSelect from './issue_status_select'; diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js index da99394ff90..7470d634b99 100644 --- a/app/assets/javascripts/issuable_context.js +++ b/app/assets/javascripts/issuable_context.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import Cookies from 'js-cookie'; import bp from './breakpoints'; import UsersSelect from './users_select'; diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js index fdfad0b6a4f..bb8b3d91e40 100644 --- a/app/assets/javascripts/issuable_form.js +++ b/app/assets/javascripts/issuable_form.js @@ -1,6 +1,7 @@ /* eslint-disable func-names, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-new, object-shorthand, no-unused-vars, comma-dangle, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, max-len */ /* global GitLab */ +import $ from 'jquery'; import Pikaday from 'pikaday'; import Autosave from './autosave'; import UsersSelect from './users_select'; diff --git a/app/assets/javascripts/issuable_index.js b/app/assets/javascripts/issuable_index.js index 0683ca82a38..06ec4546164 100644 --- a/app/assets/javascripts/issuable_index.js +++ b/app/assets/javascripts/issuable_index.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import axios from './lib/utils/axios_utils'; import flash from './flash'; import { __ } from './locale'; diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js index 333bbd9e0ba..5113ac6775d 100644 --- a/app/assets/javascripts/issue.js +++ b/app/assets/javascripts/issue.js @@ -1,4 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, max-len */ + +import $ from 'jquery'; import axios from './lib/utils/axios_utils'; import { addDelimiter } from './lib/utils/text_utility'; import flash from './flash'; diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue index 1338be0ec4b..ae577e04a56 100644 --- a/app/assets/javascripts/issue_show/components/description.vue +++ b/app/assets/javascripts/issue_show/components/description.vue @@ -1,4 +1,5 @@ <script> + import $ from 'jquery'; import animateMixin from '../mixins/animate'; import TaskList from '../../task_list'; import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor'; 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 1ad0e59287e..7db0488e306 100644 --- a/app/assets/javascripts/issue_show/components/fields/description_template.vue +++ b/app/assets/javascripts/issue_show/components/fields/description_template.vue @@ -1,4 +1,5 @@ <script> + import $ from 'jquery'; import IssuableTemplateSelectors from '../../../templates/issuable_template_selectors'; export default { diff --git a/app/assets/javascripts/issue_status_select.js b/app/assets/javascripts/issue_status_select.js index 71c0f894389..c14803c80e7 100644 --- a/app/assets/javascripts/issue_status_select.js +++ b/app/assets/javascripts/issue_status_select.js @@ -1,3 +1,5 @@ +import $ from 'jquery'; + export default function issueStatusSelect() { $('.js-issue-status').each((i, el) => { const fieldName = $(el).data('fieldName'); diff --git a/app/assets/javascripts/job.js b/app/assets/javascripts/job.js index f39ae764d3c..ace45e9dd29 100644 --- a/app/assets/javascripts/job.js +++ b/app/assets/javascripts/job.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import _ from 'underscore'; import axios from './lib/utils/axios_utils'; import { visitUrl } from './lib/utils/url_utility'; diff --git a/app/assets/javascripts/label_manager.js b/app/assets/javascripts/label_manager.js index 61b40f79db1..e230dbbd4ac 100644 --- a/app/assets/javascripts/label_manager.js +++ b/app/assets/javascripts/label_manager.js @@ -1,4 +1,6 @@ /* eslint-disable comma-dangle, class-methods-use-this, no-underscore-dangle, no-param-reassign, no-unused-vars, consistent-return, func-names, space-before-function-paren, max-len */ + +import $ from 'jquery'; import Sortable from 'vendor/Sortable'; import flash from './flash'; diff --git a/app/assets/javascripts/labels.js b/app/assets/javascripts/labels.js index 7aab13ed9c6..d85ae851706 100644 --- a/app/assets/javascripts/labels.js +++ b/app/assets/javascripts/labels.js @@ -1,3 +1,5 @@ +import $ from 'jquery'; + export default class Labels { constructor() { this.setSuggestedColor = this.setSuggestedColor.bind(this); diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index 9b46bbf83da..824d3f7ca09 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -1,6 +1,8 @@ /* eslint-disable no-useless-return, func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, no-unused-vars, one-var-declaration-per-line, prefer-template, no-new, consistent-return, object-shorthand, comma-dangle, no-shadow, no-param-reassign, brace-style, vars-on-top, quotes, no-lonely-if, no-else-return, dot-notation, no-empty, no-return-assign, camelcase, prefer-spread */ /* global Issuable */ /* global ListLabel */ + +import $ from 'jquery'; import _ from 'underscore'; import { __ } from './locale'; import axios from './lib/utils/axios_utils'; diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js index 1b4900827b8..e3177188772 100644 --- a/app/assets/javascripts/layout_nav.js +++ b/app/assets/javascripts/layout_nav.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import ContextualSidebar from './contextual_sidebar'; import initFlyOutNav from './fly_out_nav'; diff --git a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js index 0bf2ba6acc2..3873f4528ce 100644 --- a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js +++ b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js @@ -1,3 +1,5 @@ +import $ from 'jquery'; + /** * Linked Tabs * diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index e741789fbb6..0830ebe9e4e 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -1,4 +1,4 @@ -import jQuery from 'jquery'; +import $ from 'jquery'; import Cookies from 'js-cookie'; import axios from './axios_utils'; import { getLocationHash } from './url_utility'; @@ -142,7 +142,7 @@ export const isMetaClick = e => e.metaKey || e.ctrlKey || e.which === 2; export const scrollToElement = (element) => { let $el = element; - if (!(element instanceof jQuery)) { + if (!(element instanceof $)) { $el = $(element); } const top = $el.offset().top; @@ -302,6 +302,14 @@ export const parseQueryStringIntoObject = (query = '') => { }, {}); }; +/** + * Converts object with key-value pairs + * into query-param string + * + * @param {Object} params + */ +export const objectToQueryString = (params = {}) => Object.keys(params).map(param => `${param}=${params[param]}`).join('&'); + export const buildUrlWithCurrentLocation = param => (param ? `${window.location.pathname}${param}` : window.location.pathname); /** diff --git a/app/assets/javascripts/lib/utils/csrf.js b/app/assets/javascripts/lib/utils/csrf.js index 0bdb547d31a..ca9828c4682 100644 --- a/app/assets/javascripts/lib/utils/csrf.js +++ b/app/assets/javascripts/lib/utils/csrf.js @@ -1,3 +1,5 @@ +import $ from 'jquery'; + /* This module provides easy access to the CSRF token and caches it for re-use. It also exposes some values commonly used in relation diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index d6cccbef42b..c3d94d63c13 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import timeago from 'timeago.js'; import dateFormat from 'vendor/date.format'; import { pluralize } from './text_utility'; diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js index 5dc98b4a920..5a16adea4dc 100644 --- a/app/assets/javascripts/lib/utils/text_markdown.js +++ b/app/assets/javascripts/lib/utils/text_markdown.js @@ -1,26 +1,25 @@ /* eslint-disable import/prefer-default-export, func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len, vars-on-top */ +import $ from 'jquery'; +import { insertText } from '~/lib/utils/common_utils'; -const textUtils = {}; - -textUtils.selectedText = function(text, textarea) { +function selectedText(text, textarea) { return text.substring(textarea.selectionStart, textarea.selectionEnd); -}; +} -textUtils.lineBefore = function(text, textarea) { +function lineBefore(text, textarea) { var split; split = text.substring(0, textarea.selectionStart).trim().split('\n'); return split[split.length - 1]; -}; +} -textUtils.lineAfter = function(text, textarea) { +function lineAfter(text, textarea) { return text.substring(textarea.selectionEnd).trim().split('\n')[0]; -}; +} -textUtils.blockTagText = function(text, textArea, blockTag, selected) { - var lineAfter, lineBefore; - lineBefore = this.lineBefore(text, textArea); - lineAfter = this.lineAfter(text, textArea); - if (lineBefore === blockTag && lineAfter === blockTag) { +function blockTagText(text, textArea, blockTag, selected) { + const before = lineBefore(text, textArea); + const after = lineAfter(text, textArea); + if (before === blockTag && after === blockTag) { // To remove the block tag we have to select the line before & after if (blockTag != null) { textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1); @@ -30,10 +29,30 @@ textUtils.blockTagText = function(text, textArea, blockTag, selected) { } else { return blockTag + "\n" + selected + "\n" + blockTag; } -}; +} + +function moveCursor(textArea, tag, wrapped, removedLastNewLine) { + var pos; + if (!textArea.setSelectionRange) { + return; + } + if (textArea.selectionStart === textArea.selectionEnd) { + if (wrapped) { + pos = textArea.selectionStart - tag.length; + } else { + pos = textArea.selectionStart; + } + + if (removedLastNewLine) { + pos -= 1; + } + + return textArea.setSelectionRange(pos, pos); + } +} -textUtils.insertText = function(textArea, text, tag, blockTag, selected, wrap) { - var insertText, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine, currentLineEmpty, lastNewLine; +export function insertMarkdownText(textArea, text, tag, blockTag, selected, wrap) { + var textToInsert, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine, currentLineEmpty, lastNewLine; removedLastNewLine = false; removedFirstNewLine = false; currentLineEmpty = false; @@ -65,9 +84,9 @@ textUtils.insertText = function(textArea, text, tag, blockTag, selected, wrap) { if (selectedSplit.length > 1 && (!wrap || (blockTag != null && blockTag !== ''))) { if (blockTag != null && blockTag !== '') { - insertText = this.blockTagText(text, textArea, blockTag, selected); + textToInsert = blockTagText(text, textArea, blockTag, selected); } else { - insertText = selectedSplit.map(function(val) { + textToInsert = selectedSplit.map(function(val) { if (val.indexOf(tag) === 0) { return "" + (val.replace(tag, '')); } else { @@ -76,78 +95,42 @@ textUtils.insertText = function(textArea, text, tag, blockTag, selected, wrap) { }).join('\n'); } } else { - insertText = "" + startChar + tag + selected + (wrap ? tag : ' '); + textToInsert = "" + startChar + tag + selected + (wrap ? tag : ' '); } if (removedFirstNewLine) { - insertText = '\n' + insertText; + textToInsert = '\n' + textToInsert; } if (removedLastNewLine) { - insertText += '\n'; - } - - if (document.queryCommandSupported('insertText')) { - inserted = document.execCommand('insertText', false, insertText); - } - if (!inserted) { - try { - document.execCommand("ms-beginUndoUnit"); - } catch (error) {} - textArea.value = this.replaceRange(text, textArea.selectionStart, textArea.selectionEnd, insertText); - try { - document.execCommand("ms-endUndoUnit"); - } catch (error) {} + textToInsert += '\n'; } - return this.moveCursor(textArea, tag, wrap, removedLastNewLine); -}; -textUtils.moveCursor = function(textArea, tag, wrapped, removedLastNewLine) { - var pos; - if (!textArea.setSelectionRange) { - return; - } - if (textArea.selectionStart === textArea.selectionEnd) { - if (wrapped) { - pos = textArea.selectionStart - tag.length; - } else { - pos = textArea.selectionStart; - } - - if (removedLastNewLine) { - pos -= 1; - } - - return textArea.setSelectionRange(pos, pos); - } -}; + insertText(textArea, textToInsert); + return moveCursor(textArea, tag, wrap, removedLastNewLine); +} -textUtils.updateText = function(textArea, tag, blockTag, wrap) { +function updateText(textArea, tag, blockTag, wrap) { var $textArea, selected, text; $textArea = $(textArea); textArea = $textArea.get(0); text = $textArea.val(); - selected = this.selectedText(text, textArea); + selected = selectedText(text, textArea); $textArea.focus(); - return this.insertText(textArea, text, tag, blockTag, selected, wrap); -}; + return insertMarkdownText(textArea, text, tag, blockTag, selected, wrap); +} -textUtils.init = function(form) { - var self; - self = this; +function replaceRange(s, start, end, substitute) { + return s.substring(0, start) + substitute + s.substring(end); +} + +export function addMarkdownListeners(form) { return $('.js-md', form).off('click').on('click', function() { - var $this; - $this = $(this); - return self.updateText($this.closest('.md-area').find('textarea'), $this.data('mdTag'), $this.data('mdBlock'), !$this.data('mdPrepend')); + const $this = $(this); + return updateText($this.closest('.md-area').find('textarea'), $this.data('mdTag'), $this.data('mdBlock'), !$this.data('mdPrepend')); }); -}; +} -textUtils.removeListeners = function(form) { +export function removeMarkdownListeners(form) { return $('.js-md', form).off('click'); -}; - -textUtils.replaceRange = function(s, start, end, substitute) { - return s.substring(0, start) + substitute + s.substring(end); -}; - -export default textUtils; +} diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index c0ce0786518..94d03621bff 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -65,20 +65,6 @@ export function capitalizeFirstCharacter(text) { return `${text[0].toUpperCase()}${text.slice(1)}`; } -export function camelCase(str) { - return str.replace(/_+([a-z])/gi, ($1, $2) => $2.toUpperCase()); -} - -export function camelCaseKeys(obj = {}) { - return Object.keys(obj).reduce((acc, key) => { - const camelKey = camelCase(key); - return { - ...acc, - [camelKey]: obj[key], - }; - }, {}); -} - /** * Replaces all html tags from a string with the given replacement. * diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js index e5c1fce3db9..f2323f57455 100644 --- a/app/assets/javascripts/line_highlighter.js +++ b/app/assets/javascripts/line_highlighter.js @@ -1,5 +1,7 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-underscore-dangle, no-param-reassign, prefer-template, quotes, comma-dangle, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, no-else-return, max-len */ +import $ from 'jquery'; + // LineHighlighter // // Handles single- and multi-line selection and highlight for blob views. diff --git a/app/assets/javascripts/logo.js b/app/assets/javascripts/logo.js index 3688a57937e..403e216e70f 100644 --- a/app/assets/javascripts/logo.js +++ b/app/assets/javascripts/logo.js @@ -1,3 +1,5 @@ +import $ from 'jquery'; + export default function initLogoAnimation() { window.addEventListener('beforeunload', () => { $('.tanuki-logo').addClass('animate'); diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 659dc9eaa1f..2c80baba10b 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -1,5 +1,5 @@ /* eslint-disable import/first */ -/* global ConfirmDangerModal */ +/* global $ */ import jQuery from 'jquery'; import Cookies from 'js-cookie'; @@ -20,7 +20,6 @@ import './behaviors/'; // everything else import loadAwardsHandler from './awards_handler'; import bp from './breakpoints'; -import './confirm_danger_modal'; import Flash, { removeFlashClickListener } from './flash'; import './gl_dropdown'; import initTodoToggle from './header'; @@ -31,13 +30,15 @@ import LazyLoader from './lazy_loader'; import initLogoAnimation from './logo'; import './milestone_select'; import './projects_dropdown'; -import './render_gfm'; import initBreadcrumbs from './breadcrumb'; import initDispatcher from './dispatcher'; -// eslint-disable-next-line global-require, import/no-commonjs -if (process.env.NODE_ENV !== 'production') require('./test_utils/'); +// inject test utilities if necessary +if (process.env.NODE_ENV !== 'production' && gon && gon.test_env) { + $.fx.off = true; + import(/* webpackMode: "eager" */ './test_utils/'); +} svg4everybody(); @@ -211,16 +212,6 @@ document.addEventListener('DOMContentLoaded', () => { $(document).trigger('toggle.comments'); }); - $document.on('click', '.js-confirm-danger', (e) => { - const btn = $(e.target); - const form = btn.closest('form'); - const text = btn.data('confirmDangerMessage'); - e.preventDefault(); - - // eslint-disable-next-line no-new - new ConfirmDangerModal(form, text); - }); - $document.on('breakpoint:change', (e, breakpoint) => { if (breakpoint === 'sm' || breakpoint === 'xs') { const $gutterIcon = $sidebarGutterToggle.find('i'); diff --git a/app/assets/javascripts/member_expiration_date.js b/app/assets/javascripts/member_expiration_date.js index 84e70e35bad..d27922a2099 100644 --- a/app/assets/javascripts/member_expiration_date.js +++ b/app/assets/javascripts/member_expiration_date.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import Pikaday from 'pikaday'; import { parsePikadayDate, pikadayToString } from './lib/utils/datefix'; diff --git a/app/assets/javascripts/members.js b/app/assets/javascripts/members.js index 330ebed5f73..7d0c701fd70 100644 --- a/app/assets/javascripts/members.js +++ b/app/assets/javascripts/members.js @@ -1,3 +1,5 @@ +import $ from 'jquery'; + export default class Members { constructor() { this.addListeners(); diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js index 8be7314ded8..db1d09eb2f2 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js +++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js @@ -1,5 +1,6 @@ /* eslint-disable comma-dangle, object-shorthand, no-param-reassign, camelcase, no-nested-ternary, no-continue, max-len */ +import $ from 'jquery'; import Vue from 'vue'; import Cookies from 'js-cookie'; diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js index 66b258839ae..4abd5433bb5 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js +++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js @@ -1,5 +1,6 @@ /* eslint-disable new-cap, comma-dangle, no-new */ +import $ from 'jquery'; import Vue from 'vue'; import Flash from '../flash'; import initIssuableSidebar from '../init_issuable_sidebar'; diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index a64093afcf4..d8222ebec63 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -1,4 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, no-underscore-dangle, one-var, one-var-declaration-per-line, consistent-return, dot-notation, quote-props, comma-dangle, object-shorthand, max-len, prefer-arrow-callback */ + +import $ from 'jquery'; import { __ } from '~/locale'; import TaskList from './task_list'; import MergeRequestTabs from './merge_request_tabs'; diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 46789e324c2..e77318fef46 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -1,5 +1,6 @@ /* eslint-disable no-new, class-methods-use-this */ +import $ from 'jquery'; import Cookies from 'js-cookie'; import axios from './lib/utils/axios_utils'; import flash from './flash'; @@ -72,6 +73,7 @@ export default class MergeRequestTabs { constructor({ action, setUrl, stubLocation } = {}) { const mergeRequestTabs = document.querySelector('.js-tabs-affix'); const navbar = document.querySelector('.navbar-gitlab'); + const peek = document.getElementById('peek'); const paddingTop = 16; this.diffsLoaded = false; @@ -85,6 +87,10 @@ export default class MergeRequestTabs { this.showTab = this.showTab.bind(this); this.stickyTop = navbar ? navbar.offsetHeight - paddingTop : 0; + if (peek) { + this.stickyTop += peek.offsetHeight; + } + if (mergeRequestTabs) { this.stickyTop += mergeRequestTabs.offsetHeight; } diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js index b1d74250dfd..e6e3a66aa20 100644 --- a/app/assets/javascripts/milestone.js +++ b/app/assets/javascripts/milestone.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import axios from './lib/utils/axios_utils'; import flash from './flash'; diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index 2841ecb558b..add07c156a4 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -1,6 +1,8 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, one-var-declaration-per-line, no-unused-vars, object-shorthand, comma-dangle, no-else-return, no-self-compare, consistent-return, no-param-reassign, no-shadow */ /* global Issuable */ /* global ListMilestone */ + +import $ from 'jquery'; import _ from 'underscore'; import axios from './lib/utils/axios_utils'; import { timeFor } from './lib/utils/datetime_utility'; @@ -216,6 +218,9 @@ export default class MilestoneSelect { $value.html(milestoneLinkNoneTemplate); return $sidebarCollapsedValue.find('span').text('No'); } + }) + .catch(() => { + $loading.fadeOut(); }); } } diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js b/app/assets/javascripts/mini_pipeline_graph_dropdown.js index c7bccd483ac..01399de4c62 100644 --- a/app/assets/javascripts/mini_pipeline_graph_dropdown.js +++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js @@ -1,4 +1,6 @@ /* eslint-disable no-new */ + +import $ from 'jquery'; import flash from './flash'; import axios from './lib/utils/axios_utils'; diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 031badc7026..10b3a4d2fee 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -7,34 +7,86 @@ import EmptyState from './empty_state.vue'; import MonitoringStore from '../stores/monitoring_store'; import eventHub from '../event_hub'; - import { convertPermissionToBoolean } from '../../lib/utils/common_utils'; export default { - components: { Graph, GraphGroup, EmptyState, }, - data() { - const metricsData = document.querySelector('#prometheus-graphs').dataset; - const store = new MonitoringStore(); + props: { + hasMetrics: { + type: Boolean, + required: false, + default: true, + }, + showLegend: { + type: Boolean, + required: false, + default: true, + }, + showPanels: { + type: Boolean, + required: false, + default: true, + }, + forceSmallGraph: { + type: Boolean, + required: false, + default: false, + }, + documentationPath: { + type: String, + required: true, + }, + settingsPath: { + type: String, + required: true, + }, + clustersPath: { + type: String, + required: true, + }, + tagsPath: { + type: String, + required: true, + }, + projectPath: { + type: String, + required: true, + }, + metricsEndpoint: { + type: String, + required: true, + }, + deploymentEndpoint: { + type: String, + required: false, + default: null, + }, + emptyGettingStartedSvgPath: { + type: String, + required: true, + }, + emptyLoadingSvgPath: { + type: String, + required: true, + }, + emptyNoDataSvgPath: { + type: String, + required: true, + }, + emptyUnableToConnectSvgPath: { + type: String, + required: true, + }, + }, + data() { return { - store, + store: new MonitoringStore(), state: 'gettingStarted', - hasMetrics: convertPermissionToBoolean(metricsData.hasMetrics), - documentationPath: metricsData.documentationPath, - settingsPath: metricsData.settingsPath, - clustersPath: metricsData.clustersPath, - tagsPath: metricsData.tagsPath, - projectPath: metricsData.projectPath, - metricsEndpoint: metricsData.additionalMetrics, - deploymentEndpoint: metricsData.deploymentEndpoint, - emptyGettingStartedSvgPath: metricsData.emptyGettingStartedSvgPath, - emptyLoadingSvgPath: metricsData.emptyLoadingSvgPath, - emptyUnableToConnectSvgPath: metricsData.emptyUnableToConnectSvgPath, showEmptyState: true, updateAspectRatio: false, updatedAspectRatios: 0, @@ -67,6 +119,7 @@ window.addEventListener('resize', this.resizeThrottled, false); } }, + methods: { getGraphsData() { this.state = 'loading'; @@ -115,6 +168,7 @@ v-for="(groupData, index) in store.groups" :key="index" :name="groupData.group" + :show-panels="showPanels" > <graph v-for="(graphData, index) in groupData.metrics" @@ -125,6 +179,8 @@ :deployment-data="store.deploymentData" :project-path="projectPath" :tags-path="tagsPath" + :show-legend="showLegend" + :small-graph="forceSmallGraph" /> </graph-group> </div> @@ -136,6 +192,7 @@ :clusters-path="clustersPath" :empty-getting-started-svg-path="emptyGettingStartedSvgPath" :empty-loading-svg-path="emptyLoadingSvgPath" + :empty-no-data-svg-path="emptyNoDataSvgPath" :empty-unable-to-connect-svg-path="emptyUnableToConnectSvgPath" /> </template> diff --git a/app/assets/javascripts/monitoring/components/empty_state.vue b/app/assets/javascripts/monitoring/components/empty_state.vue index 9517b8ccb67..fbf451fce68 100644 --- a/app/assets/javascripts/monitoring/components/empty_state.vue +++ b/app/assets/javascripts/monitoring/components/empty_state.vue @@ -27,6 +27,10 @@ type: String, required: true, }, + emptyNoDataSvgPath: { + type: String, + required: true, + }, emptyUnableToConnectSvgPath: { type: String, required: true, @@ -54,7 +58,7 @@ buttonPath: this.documentationPath, }, noData: { - svgUrl: this.emptyUnableToConnectSvgPath, + svgUrl: this.emptyNoDataSvgPath, title: 'No data found', description: `You are connected to the Prometheus server, but there is currently no data to display.`, diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue index ea5c24efaf9..42615d2bb8e 100644 --- a/app/assets/javascripts/monitoring/components/graph.vue +++ b/app/assets/javascripts/monitoring/components/graph.vue @@ -52,6 +52,16 @@ type: String, required: true, }, + showLegend: { + type: Boolean, + required: false, + default: true, + }, + smallGraph: { + type: Boolean, + required: false, + default: false, + }, }, data() { @@ -130,7 +140,7 @@ const breakpointSize = bp.getBreakpointSize(); const query = this.graphData.queries[0]; this.margin = measurements.large.margin; - if (breakpointSize === 'xs' || breakpointSize === 'sm') { + if (this.smallGraph || breakpointSize === 'xs' || breakpointSize === 'sm') { this.graphHeight = 300; this.margin = measurements.small.margin; this.measurements = measurements.small; @@ -182,7 +192,9 @@ this.graphHeightOffset, ); - if (this.timeSeries.length > 3) { + if (!this.showLegend) { + this.baseGraphHeight -= 50; + } else if (this.timeSeries.length > 3) { this.baseGraphHeight = this.baseGraphHeight += (this.timeSeries.length - 3) * 20; } @@ -197,6 +209,7 @@ const xAxis = d3.axisBottom() .scale(axisXScale) + .ticks(this.graphWidth / 120) .tickFormat(timeScaleFormat); const yAxis = d3.axisLeft() @@ -255,6 +268,7 @@ :time-series="timeSeries" :unit-of-display="unitOfDisplay" :current-data-index="currentDataIndex" + :show-legend-group="showLegend" /> <svg class="graph-data" diff --git a/app/assets/javascripts/monitoring/components/graph/legend.vue b/app/assets/javascripts/monitoring/components/graph/legend.vue index c6e8d726ffc..3149397b61f 100644 --- a/app/assets/javascripts/monitoring/components/graph/legend.vue +++ b/app/assets/javascripts/monitoring/components/graph/legend.vue @@ -39,6 +39,11 @@ type: Number, required: true, }, + showLegendGroup: { + type: Boolean, + required: false, + default: true, + }, }, data() { return { @@ -57,8 +62,9 @@ }, rectTransform() { - const yCoordinate = ((this.graphHeight - this.margin.top) / 2) - + (this.yLabelWidth / 2) + 10 || 0; + const yCoordinate = (((this.graphHeight - this.margin.top) + + this.measurements.axisLabelLineOffset) / 2) + + (this.yLabelWidth / 2) || 0; return `translate(0, ${yCoordinate}) rotate(-90)`; }, @@ -166,39 +172,41 @@ > Time </text> - <g - class="legend-group" - v-for="(series, index) in timeSeries" - :key="index" - :transform="translateLegendGroup(index)" - > - <line - :stroke="series.lineColor" - :stroke-width="measurements.legends.height" - :stroke-dasharray="strokeDashArray(series.lineStyle)" - :x1="measurements.legends.offsetX" - :x2="measurements.legends.offsetX + measurements.legends.width" - :y1="graphHeight - measurements.legends.offsetY" - :y2="graphHeight - measurements.legends.offsetY" - /> - <text - v-if="timeSeries.length > 1" - class="legend-metric-title" - ref="legendTitleSvg" - x="38" - :y="graphHeight - 30" - > - {{ createSeriesString(index, series) }} - </text> - <text - v-else - class="legend-metric-title" - ref="legendTitleSvg" - x="38" - :y="graphHeight - 30" + <template v-if="showLegendGroup"> + <g + class="legend-group" + v-for="(series, index) in timeSeries" + :key="index" + :transform="translateLegendGroup(index)" > - {{ legendTitle }} {{ formatMetricUsage(series) }} - </text> - </g> + <line + :stroke="series.lineColor" + :stroke-width="measurements.legends.height" + :stroke-dasharray="strokeDashArray(series.lineStyle)" + :x1="measurements.legends.offsetX" + :x2="measurements.legends.offsetX + measurements.legends.width" + :y1="graphHeight - measurements.legends.offsetY" + :y2="graphHeight - measurements.legends.offsetY" + /> + <text + v-if="timeSeries.length > 1" + class="legend-metric-title" + ref="legendTitleSvg" + x="38" + :y="graphHeight - 30" + > + {{ createSeriesString(index, series) }} + </text> + <text + v-else + class="legend-metric-title" + ref="legendTitleSvg" + x="38" + :y="graphHeight - 30" + > + {{ legendTitle }} {{ formatMetricUsage(series) }} + </text> + </g> + </template> </g> </template> diff --git a/app/assets/javascripts/monitoring/components/graph_group.vue b/app/assets/javascripts/monitoring/components/graph_group.vue index 079351a69af..f71cf614552 100644 --- a/app/assets/javascripts/monitoring/components/graph_group.vue +++ b/app/assets/javascripts/monitoring/components/graph_group.vue @@ -5,12 +5,20 @@ type: String, required: true, }, + showPanels: { + type: Boolean, + required: false, + default: true, + }, }, }; </script> <template> - <div class="panel panel-default prometheus-panel"> + <div + v-if="showPanels" + class="panel panel-default prometheus-panel" + > <div class="panel-heading"> <h4>{{ name }}</h4> </div> @@ -18,4 +26,10 @@ <slot></slot> </div> </div> + <div + v-else + class="prometheus-graph-group" + > + <slot></slot> + </div> </template> diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js index c3b0ef7e9ca..41270e015d4 100644 --- a/app/assets/javascripts/monitoring/monitoring_bundle.js +++ b/app/assets/javascripts/monitoring/monitoring_bundle.js @@ -1,7 +1,22 @@ import Vue from 'vue'; +import { convertPermissionToBoolean } from '~/lib/utils/common_utils'; import Dashboard from './components/dashboard.vue'; -export default () => new Vue({ - el: '#prometheus-graphs', - render: createElement => createElement(Dashboard), -}); +export default () => { + const el = document.getElementById('prometheus-graphs'); + + if (el && el.dataset) { + // eslint-disable-next-line no-new + new Vue({ + el, + render(createElement) { + return createElement(Dashboard, { + props: { + ...el.dataset, + hasMetrics: convertPermissionToBoolean(el.dataset.hasMetrics), + }, + }); + }, + }); + } +}; diff --git a/app/assets/javascripts/monitoring/services/monitoring_service.js b/app/assets/javascripts/monitoring/services/monitoring_service.js index e230a06cd8c..6fcca36d2fa 100644 --- a/app/assets/javascripts/monitoring/services/monitoring_service.js +++ b/app/assets/javascripts/monitoring/services/monitoring_service.js @@ -40,6 +40,9 @@ export default class MonitoringService { } getDeploymentData() { + if (!this.deploymentEndpoint) { + return Promise.resolve([]); + } return backOffRequest(() => axios.get(this.deploymentEndpoint)) .then(resp => resp.data) .then((response) => { diff --git a/app/assets/javascripts/monitoring/utils/multiple_time_series.js b/app/assets/javascripts/monitoring/utils/multiple_time_series.js index 4ce3dad440c..b5b8e3c255d 100644 --- a/app/assets/javascripts/monitoring/utils/multiple_time_series.js +++ b/app/assets/javascripts/monitoring/utils/multiple_time_series.js @@ -76,7 +76,7 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom metricTag = seriesCustomizationData.value || timeSeriesMetricLabel; [lineColor, areaColor] = pickColor(seriesCustomizationData.color); } else { - metricTag = timeSeriesMetricLabel || `series ${timeSeriesNumber + 1}`; + metricTag = timeSeriesMetricLabel || query.label || `series ${timeSeriesNumber + 1}`; [lineColor, areaColor] = pickColor(); } diff --git a/app/assets/javascripts/mr_notes/index.js b/app/assets/javascripts/mr_notes/index.js index f4cba998fa7..096c4ef5f31 100644 --- a/app/assets/javascripts/mr_notes/index.js +++ b/app/assets/javascripts/mr_notes/index.js @@ -3,14 +3,16 @@ import notesApp from '../notes/components/notes_app.vue'; import discussionCounter from '../notes/components/discussion_counter.vue'; import store from '../notes/stores'; -document.addEventListener('DOMContentLoaded', () => { - new Vue({ // eslint-disable-line +export default function initMrNotes() { + // eslint-disable-next-line no-new + new Vue({ el: '#js-vue-mr-discussions', components: { notesApp, }, data() { - const notesDataset = document.getElementById('js-vue-mr-discussions').dataset; + const notesDataset = document.getElementById('js-vue-mr-discussions') + .dataset; return { noteableData: JSON.parse(notesDataset.noteableData), currentUserData: JSON.parse(notesDataset.currentUserData), @@ -28,7 +30,8 @@ document.addEventListener('DOMContentLoaded', () => { }, }); - new Vue({ // eslint-disable-line + // eslint-disable-next-line no-new + new Vue({ el: '#js-vue-discussion-counter', components: { discussionCounter, @@ -38,4 +41,4 @@ document.addEventListener('DOMContentLoaded', () => { return createElement('discussion-counter'); }, }); -}); +} diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js index aa377327107..c7a8aac79df 100644 --- a/app/assets/javascripts/namespace_select.js +++ b/app/assets/javascripts/namespace_select.js @@ -1,4 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, no-var, comma-dangle, object-shorthand, no-else-return, prefer-template, quotes, prefer-arrow-callback, max-len */ + +import $ from 'jquery'; import Api from './api'; import { mergeUrlParams } from './lib/utils/url_utility'; diff --git a/app/assets/javascripts/network/branch_graph.js b/app/assets/javascripts/network/branch_graph.js index d3edcb724f1..bd007c707f2 100644 --- a/app/assets/javascripts/network/branch_graph.js +++ b/app/assets/javascripts/network/branch_graph.js @@ -1,5 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, quotes, comma-dangle, one-var, one-var-declaration-per-line, no-mixed-operators, no-loop-func, no-floating-decimal, consistent-return, no-unused-vars, prefer-template, prefer-arrow-callback, camelcase, max-len */ +import $ from 'jquery'; import { __ } from '../locale'; import axios from '../lib/utils/axios_utils'; import flash from '../flash'; diff --git a/app/assets/javascripts/new_branch_form.js b/app/assets/javascripts/new_branch_form.js index 77733b67c4d..40c08ee0ace 100644 --- a/app/assets/javascripts/new_branch_form.js +++ b/app/assets/javascripts/new_branch_form.js @@ -1,4 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, no-var, one-var, prefer-rest-params, max-len, vars-on-top, wrap-iife, consistent-return, comma-dangle, one-var-declaration-per-line, quotes, no-return-assign, prefer-arrow-callback, prefer-template, no-shadow, no-else-return, max-len, object-shorthand */ + +import $ from 'jquery'; import RefSelectDropdown from './ref_select_dropdown'; export default class NewBranchForm { diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index c640003d958..09f0ea37103 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -16,6 +16,10 @@ import Autosize from 'autosize'; import 'vendor/jquery.caret'; // required by jquery.atwho import 'vendor/jquery.atwho'; import AjaxCache from '~/lib/utils/ajax_cache'; +import Vue from 'vue'; +import syntaxHighlight from '~/syntax_highlight'; +import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; +import { __ } from '~/locale'; import axios from './lib/utils/axios_utils'; import { getLocationHash } from './lib/utils/url_utility'; import Flash from './flash'; @@ -24,7 +28,13 @@ import GLForm from './gl_form'; import loadAwardsHandler from './awards_handler'; import Autosave from './autosave'; import TaskList from './task_list'; -import { isInViewport, getPagePath, scrollToElement, isMetaKey, hasVueMRDiscussionsCookie } from './lib/utils/common_utils'; +import { + isInViewport, + getPagePath, + scrollToElement, + isMetaKey, + hasVueMRDiscussionsCookie, +} from './lib/utils/common_utils'; import imageDiffHelper from './image_diff/helpers/index'; import { localTimeAgo } from './lib/utils/datetime_utility'; @@ -38,9 +48,21 @@ const MAX_VISIBLE_COMMIT_LIST_COUNT = 3; const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm; export default class Notes { - static initialize(notes_url, note_ids, last_fetched_at, view, enableGFM = true) { + static initialize( + notes_url, + note_ids, + last_fetched_at, + view, + enableGFM = true, + ) { if (!this.instance) { - this.instance = new Notes(notes_url, note_ids, last_fetched_at, view, enableGFM); + this.instance = new Notes( + notes_url, + note_ids, + last_fetched_at, + view, + enableGFM, + ); } } @@ -78,10 +100,14 @@ export default class Notes { this.updatedNotesTrackingMap = {}; this.last_fetched_at = last_fetched_at; this.noteable_url = document.URL; - this.notesCountBadge || (this.notesCountBadge = $('.issuable-details').find('.notes-tab .badge')); + this.notesCountBadge || + (this.notesCountBadge = $('.issuable-details').find('.notes-tab .badge')); this.basePollingInterval = 15000; this.maxPollingSteps = 4; + this.$wrapperEl = hasVueMRDiscussionsCookie() + ? $(document).find('.diffs') + : $(document); this.cleanBinding(); this.addBinding(); this.setPollingInterval(); @@ -89,15 +115,24 @@ export default class Notes { this.taskList = new TaskList({ dataType: 'note', fieldName: 'note', - selector: '.notes' + selector: '.notes', }); this.collapseLongCommitList(); this.setViewType(view); // We are in the Merge Requests page so we need another edit form for Changes tab if (getPagePath(1) === 'merge_requests') { - $('.note-edit-form').clone() - .addClass('mr-note-edit-form').insertAfter('.note-edit-form'); + $('.note-edit-form') + .clone() + .addClass('mr-note-edit-form') + .insertAfter('.note-edit-form'); + } + + const hash = getLocationHash(); + const $anchor = hash && document.getElementById(hash); + + if ($anchor) { + this.loadLazyDiff({ currentTarget: $anchor }); } } @@ -106,58 +141,93 @@ export default class Notes { } addBinding() { - this.$wrapperEl = hasVueMRDiscussionsCookie() ? $(document).find('.diffs') : $(document); - // Edit note link this.$wrapperEl.on('click', '.js-note-edit', this.showEditForm.bind(this)); this.$wrapperEl.on('click', '.note-edit-cancel', this.cancelEdit); // Reopen and close actions for Issue/MR combined with note form submit this.$wrapperEl.on('click', '.js-comment-submit-button', this.postComment); this.$wrapperEl.on('click', '.js-comment-save-button', this.updateComment); - this.$wrapperEl.on('keyup input', '.js-note-text', this.updateTargetButtons); + this.$wrapperEl.on( + 'keyup input', + '.js-note-text', + this.updateTargetButtons, + ); // resolve a discussion this.$wrapperEl.on('click', '.js-comment-resolve-button', this.postComment); // remove a note (in general) this.$wrapperEl.on('click', '.js-note-delete', this.removeNote); // delete note attachment - this.$wrapperEl.on('click', '.js-note-attachment-delete', this.removeAttachment); + this.$wrapperEl.on( + 'click', + '.js-note-attachment-delete', + this.removeAttachment, + ); // reset main target form when clicking discard this.$wrapperEl.on('click', '.js-note-discard', this.resetMainTargetForm); // update the file name when an attachment is selected - this.$wrapperEl.on('change', '.js-note-attachment-input', this.updateFormAttachment); + this.$wrapperEl.on( + 'change', + '.js-note-attachment-input', + this.updateFormAttachment, + ); // reply to diff/discussion notes - this.$wrapperEl.on('click', '.js-discussion-reply-button', this.onReplyToDiscussionNote); + this.$wrapperEl.on( + 'click', + '.js-discussion-reply-button', + this.onReplyToDiscussionNote, + ); // add diff note this.$wrapperEl.on('click', '.js-add-diff-note-button', this.onAddDiffNote); // add diff note for images - this.$wrapperEl.on('click', '.js-add-image-diff-note-button', this.onAddImageDiffNote); + this.$wrapperEl.on( + 'click', + '.js-add-image-diff-note-button', + this.onAddImageDiffNote, + ); // hide diff note form - this.$wrapperEl.on('click', '.js-close-discussion-note-form', this.cancelDiscussionForm); + this.$wrapperEl.on( + 'click', + '.js-close-discussion-note-form', + this.cancelDiscussionForm, + ); // toggle commit list - this.$wrapperEl.on('click', '.system-note-commit-list-toggler', this.toggleCommitList); + this.$wrapperEl.on( + 'click', + '.system-note-commit-list-toggler', + this.toggleCommitList, + ); + + this.$wrapperEl.on('click', '.js-toggle-lazy-diff', this.loadLazyDiff); // fetch notes when tab becomes visible this.$wrapperEl.on('visibilitychange', this.visibilityChange); // when issue status changes, we need to refresh data this.$wrapperEl.on('issuable:change', this.refresh); // ajax:events that happen on Form when actions like Reopen, Close are performed on Issues and MRs. this.$wrapperEl.on('ajax:success', '.js-main-target-form', this.addNote); - this.$wrapperEl.on('ajax:success', '.js-discussion-note-form', this.addDiscussionNote); - this.$wrapperEl.on('ajax:success', '.js-main-target-form', this.resetMainTargetForm); - this.$wrapperEl.on('ajax:complete', '.js-main-target-form', this.reenableTargetFormSubmitButton); + this.$wrapperEl.on( + 'ajax:success', + '.js-discussion-note-form', + this.addDiscussionNote, + ); + this.$wrapperEl.on( + 'ajax:success', + '.js-main-target-form', + this.resetMainTargetForm, + ); + this.$wrapperEl.on( + 'ajax:complete', + '.js-main-target-form', + this.reenableTargetFormSubmitButton, + ); // when a key is clicked on the notes this.$wrapperEl.on('keydown', '.js-note-text', this.keydownNoteText); // When the URL fragment/hash has changed, `#note_xxx` $(window).on('hashchange', this.onHashChange); this.boundGetContent = this.getContent.bind(this); document.addEventListener('refreshLegacyNotes', this.boundGetContent); - this.eventsBound = true; } cleanBinding() { - if (!this.eventsBound) { - return; - } - this.$wrapperEl.off('click', '.js-note-edit'); this.$wrapperEl.off('click', '.note-edit-cancel'); this.$wrapperEl.off('click', '.js-note-delete'); @@ -173,6 +243,7 @@ export default class Notes { this.$wrapperEl.off('keydown', '.js-note-text'); this.$wrapperEl.off('click', '.js-comment-resolve-button'); this.$wrapperEl.off('click', '.system-note-commit-list-toggler'); + this.$wrapperEl.off('click', '.js-toggle-lazy-diff'); this.$wrapperEl.off('ajax:success', '.js-main-target-form'); this.$wrapperEl.off('ajax:success', '.js-discussion-note-form'); this.$wrapperEl.off('ajax:complete', '.js-main-target-form'); @@ -181,10 +252,16 @@ export default class Notes { } static initCommentTypeToggle(form) { - const dropdownTrigger = form.querySelector('.js-comment-type-dropdown .dropdown-toggle'); - const dropdownList = form.querySelector('.js-comment-type-dropdown .dropdown-menu'); + const dropdownTrigger = form.querySelector( + '.js-comment-type-dropdown .dropdown-toggle', + ); + const dropdownList = form.querySelector( + '.js-comment-type-dropdown .dropdown-menu', + ); const noteTypeInput = form.querySelector('#note_type'); - const submitButton = form.querySelector('.js-comment-type-dropdown .js-comment-submit-button'); + const submitButton = form.querySelector( + '.js-comment-type-dropdown .js-comment-submit-button', + ); const closeButton = form.querySelector('.js-note-target-close'); const reopenButton = form.querySelector('.js-note-target-reopen'); @@ -201,7 +278,13 @@ export default class Notes { } keydownNoteText(e) { - var $textarea, discussionNoteForm, editNote, myLastNote, myLastNoteEditBtn, newText, originalText; + var $textarea, + discussionNoteForm, + editNote, + myLastNote, + myLastNoteEditBtn, + newText, + originalText; if (isMetaKey(e)) { return; } @@ -213,7 +296,12 @@ export default class Notes { if ($textarea.val() !== '') { return; } - myLastNote = $(`li.note[data-author-id='${gon.current_user_id}'][data-editable]:last`, $textarea.closest('.note, .notes_holder, #notes')); + myLastNote = $( + `li.note[data-author-id='${ + gon.current_user_id + }'][data-editable]:last`, + $textarea.closest('.note, .notes_holder, #notes'), + ); if (myLastNote.length) { myLastNoteEditBtn = myLastNote.find('.js-note-edit'); return myLastNoteEditBtn.trigger('click', [true, myLastNote]); @@ -224,7 +312,9 @@ export default class Notes { discussionNoteForm = $textarea.closest('.js-discussion-note-form'); if (discussionNoteForm.length) { if ($textarea.val() !== '') { - if (!confirm('Are you sure you want to cancel creating this comment?')) { + if ( + !confirm('Are you sure you want to cancel creating this comment?') + ) { return; } } @@ -236,7 +326,9 @@ export default class Notes { originalText = $textarea.closest('form').data('originalNote'); newText = $textarea.val(); if (originalText !== newText) { - if (!confirm('Are you sure you want to cancel editing this comment?')) { + if ( + !confirm('Are you sure you want to cancel editing this comment?') + ) { return; } } @@ -249,11 +341,14 @@ export default class Notes { if (Notes.interval) { clearInterval(Notes.interval); } - return Notes.interval = setInterval((function(_this) { - return function() { - return _this.refresh(); - }; - })(this), this.pollingInterval); + return (Notes.interval = setInterval( + (function(_this) { + return function() { + return _this.refresh(); + }; + })(this), + this.pollingInterval, + )); } refresh() { @@ -269,20 +364,23 @@ export default class Notes { this.refreshing = true; - axios.get(`${this.notes_url}?html=true`, { - headers: { - 'X-Last-Fetched-At': this.last_fetched_at, - }, - }).then(({ data }) => { - const notes = data.notes; - this.last_fetched_at = data.last_fetched_at; - this.setPollingInterval(data.notes.length); - $.each(notes, (i, note) => this.renderNote(note)); - - this.refreshing = false; - }).catch(() => { - this.refreshing = false; - }); + axios + .get(`${this.notes_url}?html=true`, { + headers: { + 'X-Last-Fetched-At': this.last_fetched_at, + }, + }) + .then(({ data }) => { + const notes = data.notes; + this.last_fetched_at = data.last_fetched_at; + this.setPollingInterval(data.notes.length); + $.each(notes, (i, note) => this.renderNote(note)); + + this.refreshing = false; + }) + .catch(() => { + this.refreshing = false; + }); } /** @@ -298,7 +396,8 @@ export default class Notes { if (shouldReset == null) { shouldReset = true; } - nthInterval = this.basePollingInterval * Math.pow(2, this.maxPollingSteps - 1); + nthInterval = + this.basePollingInterval * Math.pow(2, this.maxPollingSteps - 1); if (shouldReset) { this.pollingInterval = this.basePollingInterval; } else if (this.pollingInterval < nthInterval) { @@ -317,12 +416,17 @@ export default class Notes { if ('emoji_award' in noteEntity.commands_changes) { votesBlock = $('.js-awards-block').eq(0); - loadAwardsHandler().then((awardsHandler) => { - awardsHandler.addAwardToEmojiBar(votesBlock, noteEntity.commands_changes.emoji_award); - awardsHandler.scrollToAwards(); - }).catch(() => { - // ignore - }); + loadAwardsHandler() + .then(awardsHandler => { + awardsHandler.addAwardToEmojiBar( + votesBlock, + noteEntity.commands_changes.emoji_award, + ); + awardsHandler.scrollToAwards(); + }) + .catch(() => { + // ignore + }); } } } @@ -367,11 +471,17 @@ export default class Notes { if (!noteEntity.valid) { if (noteEntity.errors && noteEntity.errors.commands_only) { - if (noteEntity.commands_changes && - Object.keys(noteEntity.commands_changes).length > 0) { + if ( + noteEntity.commands_changes && + Object.keys(noteEntity.commands_changes).length > 0 + ) { $notesList.find('.system-note.being-posted').remove(); } - this.addFlash(noteEntity.errors.commands_only, 'notice', this.parentTimeline.get(0)); + this.addFlash( + noteEntity.errors.commands_only, + 'notice', + this.parentTimeline.get(0), + ); this.refresh(); } return; @@ -393,28 +503,30 @@ export default class Notes { this.setupNewNote($newNote); this.refresh(); return this.updateNotesCount(1); - } - // The server can send the same update multiple times so we need to make sure to only update once per actual update. - else if (Notes.isUpdatedNote(noteEntity, $note)) { + } else if (Notes.isUpdatedNote(noteEntity, $note)) { + // The server can send the same update multiple times so we need to make sure to only update once per actual update. const isEditing = $note.hasClass('is-editing'); const initialContent = normalizeNewlines( - $note.find('.original-note-content').text().trim() + $note + .find('.original-note-content') + .text() + .trim(), ); const $textarea = $note.find('.js-note-text'); const currentContent = $textarea.val(); // There can be CRLF vs LF mismatches if we don't sanitize and compare the same way const sanitizedNoteNote = normalizeNewlines(noteEntity.note); - const isTextareaUntouched = currentContent === initialContent || currentContent === sanitizedNoteNote; + const isTextareaUntouched = + currentContent === initialContent || + currentContent === sanitizedNoteNote; if (isEditing && isTextareaUntouched) { $textarea.val(noteEntity.note); this.updatedNotesTrackingMap[noteEntity.id] = noteEntity; - } - else if (isEditing && !isTextareaUntouched) { + } else if (isEditing && !isTextareaUntouched) { this.putConflictEditWarningInPlace(noteEntity, $note); this.updatedNotesTrackingMap[noteEntity.id] = noteEntity; - } - else { + } else { const $updatedNote = Notes.animateUpdateNote(noteEntity.html, $note); this.setupNewNote($updatedNote); } @@ -438,17 +550,31 @@ export default class Notes { } this.note_ids.push(noteEntity.id); - form = $form || $(`.js-discussion-note-form[data-discussion-id="${noteEntity.discussion_id}"]`); - row = (form.length || !noteEntity.discussion_line_code) ? form.closest('tr') : $(`#${noteEntity.discussion_line_code}`); + form = + $form || + $( + `.js-discussion-note-form[data-discussion-id="${ + noteEntity.discussion_id + }"]`, + ); + row = + form.length || !noteEntity.discussion_line_code + ? form.closest('tr') + : $(`#${noteEntity.discussion_line_code}`); if (noteEntity.on_image) { row = form; } lineType = this.isParallelView() ? form.find('#line_type').val() : 'old'; - diffAvatarContainer = row.prevAll('.line_holder').first().find('.js-avatar-container.' + lineType + '_line'); + diffAvatarContainer = row + .prevAll('.line_holder') + .first() + .find('.js-avatar-container.' + lineType + '_line'); // is this the first note of discussion? - discussionContainer = $(`.notes[data-discussion-id="${noteEntity.discussion_id}"]`); + discussionContainer = $( + `.notes[data-discussion-id="${noteEntity.discussion_id}"]`, + ); if (!discussionContainer.length) { discussionContainer = form.closest('.discussion').find('.notes'); } @@ -456,25 +582,42 @@ export default class Notes { if (noteEntity.diff_discussion_html) { var $discussion = $(noteEntity.diff_discussion_html).renderGFM(); - if (!this.isParallelView() || row.hasClass('js-temp-notes-holder') || noteEntity.on_image) { + if ( + !this.isParallelView() || + row.hasClass('js-temp-notes-holder') || + noteEntity.on_image + ) { // insert the note and the reply button after the temp row row.after($discussion); } else { // Merge new discussion HTML in - var $notes = $discussion.find(`.notes[data-discussion-id="${noteEntity.discussion_id}"]`); - var contentContainerClass = '.' + $notes.closest('.notes_content') - .attr('class') - .split(' ') - .join('.'); - - row.find(contentContainerClass + ' .content').append($notes.closest('.content').children()); + var $notes = $discussion.find( + `.notes[data-discussion-id="${noteEntity.discussion_id}"]`, + ); + var contentContainerClass = + '.' + + $notes + .closest('.notes_content') + .attr('class') + .split(' ') + .join('.'); + + row + .find(contentContainerClass + ' .content') + .append($notes.closest('.content').children()); } } // Init discussion on 'Discussion' page if it is merge request page const page = $('body').attr('data-page'); - if ((page && page.indexOf('projects:merge_request') !== -1) || !noteEntity.diff_discussion_html) { + if ( + (page && page.indexOf('projects:merge_request') !== -1) || + !noteEntity.diff_discussion_html + ) { if (!hasVueMRDiscussionsCookie()) { - Notes.animateAppendNote(noteEntity.discussion_html, $('.main-notes-list')); + Notes.animateAppendNote( + noteEntity.discussion_html, + $('.main-notes-list'), + ); } } } else { @@ -482,7 +625,10 @@ export default class Notes { Notes.animateAppendNote(noteEntity.html, discussionContainer); } - if (typeof gl.diffNotesCompileComponents !== 'undefined' && noteEntity.discussion_resolvable) { + if ( + typeof gl.diffNotesCompileComponents !== 'undefined' && + noteEntity.discussion_resolvable + ) { gl.diffNotesCompileComponents(); this.renderDiscussionAvatar(diffAvatarContainer, noteEntity); @@ -494,7 +640,8 @@ export default class Notes { } getLineHolder(changesDiscussionContainer) { - return $(changesDiscussionContainer).closest('.notes_holder') + return $(changesDiscussionContainer) + .closest('.notes_holder') .prevAll('.line_holder') .first() .get(0); @@ -527,8 +674,14 @@ export default class Notes { form.find('.js-errors').remove(); // reset text and preview form.find('.js-md-write-button').click(); - form.find('.js-note-text').val('').trigger('input'); - form.find('.js-note-text').data('autosave').reset(); + form + .find('.js-note-text') + .val('') + .trigger('input'); + form + .find('.js-note-text') + .data('autosave') + .reset(); var event = document.createEvent('Event'); event.initEvent('autosize:update', true, false); @@ -564,7 +717,10 @@ export default class Notes { form.find('#note_type').val(''); form.find('#note_project_id').remove(); form.find('#in_reply_to_discussion_id').remove(); - form.find('.js-comment-resolve-button').closest('comment-and-resolve-btn').remove(); + form + .find('.js-comment-resolve-button') + .closest('comment-and-resolve-btn') + .remove(); this.parentTimeline = form.parents('.timeline'); if (form.length) { @@ -618,11 +774,17 @@ export default class Notes { } else if ($form.hasClass('js-discussion-note-form')) { formParentTimeline = $form.closest('.discussion-notes').find('.notes'); } - return this.addFlash('Your comment could not be submitted! Please check your network connection and try again.', 'alert', formParentTimeline.get(0)); + return this.addFlash( + 'Your comment could not be submitted! Please check your network connection and try again.', + 'alert', + formParentTimeline.get(0), + ); } updateNoteError($parentTimeline) { - new Flash('Your comment could not be updated! Please check your network connection and try again.'); + new Flash( + 'Your comment could not be updated! Please check your network connection and try again.', + ); } /** @@ -671,14 +833,16 @@ export default class Notes { } checkContentToAllowEditing($el) { - var initialContent = $el.find('.original-note-content').text().trim(); + var initialContent = $el + .find('.original-note-content') + .text() + .trim(); var currentContent = $el.find('.js-note-text').val(); var isAllowed = true; if (currentContent === initialContent) { this.removeNoteEditForm($el); - } - else { + } else { var $buttons = $el.find('.note-form-actions'); var isWidgetVisible = isInViewport($el.get(0)); @@ -740,8 +904,7 @@ export default class Notes { this.setupNewNote($newNote); // Now that we have taken care of the update, clear it out delete this.updatedNotesTrackingMap[noteId]; - } - else { + } else { $note.find('.js-finish-edit-warning').hide(); this.removeNoteEditForm($note); } @@ -774,7 +937,9 @@ export default class Notes { form.removeClass('current-note-edit-form'); form.find('.js-finish-edit-warning').hide(); // Replace markdown textarea text with original note text. - return form.find('.js-note-text').val(form.find('form.edit-note').data('originalNote')); + return form + .find('.js-note-text') + .val(form.find('form.edit-note').data('originalNote')); } /** @@ -788,58 +953,67 @@ export default class Notes { $note = $(e.currentTarget).closest('.note'); noteElId = $note.attr('id'); noteId = $note.attr('data-note-id'); - lineHolder = $(e.currentTarget).closest('.notes[data-discussion-id]') + lineHolder = $(e.currentTarget) + .closest('.notes[data-discussion-id]') .closest('.notes_holder') .prev('.line_holder'); - $(`.note[id="${noteElId}"]`).each((function(_this) { - // A same note appears in the "Discussion" and in the "Changes" tab, we have - // to remove all. Using $('.note[id='noteId']') ensure we get all the notes, - // where $('#noteId') would return only one. - return function(i, el) { - var $note, $notes; - $note = $(el); - $notes = $note.closest('.discussion-notes'); - const discussionId = $('.notes', $notes).data('discussionId'); - - if (typeof gl.diffNotesCompileComponents !== 'undefined') { - if (gl.diffNoteApps[noteElId]) { - gl.diffNoteApps[noteElId].$destroy(); + $(`.note[id="${noteElId}"]`).each( + (function(_this) { + // A same note appears in the "Discussion" and in the "Changes" tab, we have + // to remove all. Using $('.note[id='noteId']') ensure we get all the notes, + // where $('#noteId') would return only one. + return function(i, el) { + var $note, $notes; + $note = $(el); + $notes = $note.closest('.discussion-notes'); + const discussionId = $('.notes', $notes).data('discussionId'); + + if (typeof gl.diffNotesCompileComponents !== 'undefined') { + if (gl.diffNoteApps[noteElId]) { + gl.diffNoteApps[noteElId].$destroy(); + } } - } - - $note.remove(); - // check if this is the last note for this line - if ($notes.find('.note').length === 0) { - var notesTr = $notes.closest('tr'); - - // "Discussions" tab - $notes.closest('.timeline-entry').remove(); - - $(`.js-diff-avatars-${discussionId}`).trigger('remove.vue'); - - // The notes tr can contain multiple lists of notes, like on the parallel diff - // notesTr does not exist for image diffs - if (notesTr.find('.discussion-notes').length > 1 || notesTr.length === 0) { - const $diffFile = $notes.closest('.diff-file'); - if ($diffFile.length > 0) { - const removeBadgeEvent = new CustomEvent('removeBadge.imageDiff', { - detail: { - // badgeNumber's start with 1 and index starts with 0 - badgeNumber: $notes.index() + 1, - }, - }); - - $diffFile[0].dispatchEvent(removeBadgeEvent); + $note.remove(); + + // check if this is the last note for this line + if ($notes.find('.note').length === 0) { + var notesTr = $notes.closest('tr'); + + // "Discussions" tab + $notes.closest('.timeline-entry').remove(); + + $(`.js-diff-avatars-${discussionId}`).trigger('remove.vue'); + + // The notes tr can contain multiple lists of notes, like on the parallel diff + // notesTr does not exist for image diffs + if ( + notesTr.find('.discussion-notes').length > 1 || + notesTr.length === 0 + ) { + const $diffFile = $notes.closest('.diff-file'); + if ($diffFile.length > 0) { + const removeBadgeEvent = new CustomEvent( + 'removeBadge.imageDiff', + { + detail: { + // badgeNumber's start with 1 and index starts with 0 + badgeNumber: $notes.index() + 1, + }, + }, + ); + + $diffFile[0].dispatchEvent(removeBadgeEvent); + } + + $notes.remove(); + } else if (notesTr.length > 0) { + notesTr.remove(); } - - $notes.remove(); - } else if (notesTr.length > 0) { - notesTr.remove(); } - } - }; - })(this)); + }; + })(this), + ); Notes.refreshVueNotes(); Notes.checkMergeRequestStatus(); @@ -921,7 +1095,12 @@ export default class Notes { // DiffNote form.find('#note_position').val(dataHolder.attr('data-position')); - form.find('.js-note-discard').show().removeClass('js-note-discard').addClass('js-close-discussion-note-form').text(form.find('.js-close-discussion-note-form').data('cancelText')); + form + .find('.js-note-discard') + .show() + .removeClass('js-note-discard') + .addClass('js-close-discussion-note-form') + .text(form.find('.js-close-discussion-note-form').data('cancelText')); form.find('.js-note-target-close').remove(); form.find('.js-note-new-discussion').remove(); this.setupNoteForm(form); @@ -957,7 +1136,7 @@ export default class Notes { this.toggleDiffNote({ target: $link, lineType: link.dataset.lineType, - showReplyInput + showReplyInput, }); } @@ -973,7 +1152,9 @@ export default class Notes { // Setup comment form let newForm; - const $noteContainer = $link.closest('.diff-viewer').find('.note-container'); + const $noteContainer = $link + .closest('.diff-viewer') + .find('.note-container'); const $form = $noteContainer.find('> .discussion-form'); if ($form.length === 0) { @@ -986,13 +1167,17 @@ export default class Notes { this.setupDiscussionNoteForm($link, newForm); } - toggleDiffNote({ - target, - lineType, - forceShow, - showReplyInput = false, - }) { - var $link, addForm, hasNotes, newForm, noteForm, replyButton, row, rowCssToAdd, targetContent, isDiffCommentAvatar; + toggleDiffNote({ target, lineType, forceShow, showReplyInput = false }) { + var $link, + addForm, + hasNotes, + newForm, + noteForm, + replyButton, + row, + rowCssToAdd, + targetContent, + isDiffCommentAvatar; $link = $(target); row = $link.closest('tr'); const nextRow = row.next(); @@ -1004,11 +1189,13 @@ export default class Notes { hasNotes = nextRow.is('.notes_holder'); addForm = false; let lineTypeSelector = ''; - rowCssToAdd = '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line" colspan="2"></td><td class="notes_content"><div class="content"></div></td></tr>'; + rowCssToAdd = + '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line" colspan="2"></td><td class="notes_content"><div class="content"></div></td></tr>'; // In parallel view, look inside the correct left/right pane if (this.isParallelView()) { lineTypeSelector = `.${lineType}`; - rowCssToAdd = '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line old"></td><td class="notes_content parallel old"><div class="content"></div></td><td class="notes_line new"></td><td class="notes_content parallel new"><div class="content"></div></td></tr>'; + rowCssToAdd = + '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line old"></td><td class="notes_content parallel old"><div class="content"></div></td><td class="notes_line new"></td><td class="notes_content parallel new"><div class="content"></div></td></tr>'; } const notesContentSelector = `.notes_content${lineTypeSelector} .content`; let notesContent = targetRow.find(notesContentSelector); @@ -1036,7 +1223,9 @@ export default class Notes { notesContent = targetRow.find(notesContentSelector); addForm = true; } else { - const isCurrentlyShown = targetRow.find('.content:not(:empty)').is(':visible'); + const isCurrentlyShown = targetRow + .find('.content:not(:empty)') + .is(':visible'); const isForced = forceShow === true || forceShow === false; const showNow = forceShow === true || (!isCurrentlyShown && !isForced); @@ -1063,11 +1252,12 @@ export default class Notes { row = form.closest('tr'); glForm = form.data('glForm'); glForm.destroy(); - form.find('.js-note-text').data('autosave').reset(); - // show the reply button (will only work for replies) form - .prev('.discussion-reply-holder') - .show(); + .find('.js-note-text') + .data('autosave') + .reset(); + // show the reply button (will only work for replies) + form.prev('.discussion-reply-holder').show(); if (row.is('.js-temp-notes-holder')) { // remove temporary row for diff lines return row.remove(); @@ -1108,7 +1298,9 @@ export default class Notes { var filename, form; form = $(this).closest('form'); // get only the basename - filename = $(this).val().replace(/^.*[\\\/]/, ''); + filename = $(this) + .val() + .replace(/^.*[\\\/]/, ''); return form.find('.js-attachment-filename').text(filename); } @@ -1180,12 +1372,16 @@ export default class Notes { this.glForm = new GLForm($editForm.find('form'), this.enableGFM); - $editForm.find('form') + $editForm + .find('form') .attr('action', `${postUrl}?html=true`) .attr('data-remote', 'true'); $editForm.find('.js-form-target-id').val(targetId); $editForm.find('.js-form-target-type').val(targetType); - $editForm.find('.js-note-text').focus().val(originalContent); + $editForm + .find('.js-note-text') + .focus() + .val(originalContent); $editForm.find('.js-md-write-button').trigger('click'); $editForm.find('.referenced-users').hide(); } @@ -1194,7 +1390,9 @@ export default class Notes { if ($note.find('.js-conflict-edit-warning').length === 0) { const $alert = $(`<div class="js-conflict-edit-warning alert alert-danger"> This comment has changed since you started editing, please review the - <a href="#note_${noteEntity.id}" target="_blank" rel="noopener noreferrer"> + <a href="#note_${ + noteEntity.id + }" target="_blank" rel="noopener noreferrer"> updated comment </a> to ensure information is not lost @@ -1204,14 +1402,79 @@ export default class Notes { } updateNotesCount(updateCount) { - return this.notesCountBadge.text(parseInt(this.notesCountBadge.text(), 10) + updateCount); + return this.notesCountBadge.text( + parseInt(this.notesCountBadge.text(), 10) + updateCount, + ); + } + + static renderPlaceholderComponent($container) { + const el = $container.find('.js-code-placeholder').get(0); + new Vue({ + // eslint-disable-line no-new + el, + components: { + SkeletonLoadingContainer, + }, + render(createElement) { + return createElement('skeleton-loading-container'); + }, + }); + } + + static renderDiffContent($container, data) { + const { discussion_html } = data; + const lines = $(discussion_html).find('.line_holder'); + lines.addClass('fade-in'); + $container.find('tbody').prepend(lines); + const fileHolder = $container.find('.file-holder'); + $container.find('.line-holder-placeholder').remove(); + syntaxHighlight(fileHolder); + } + + static renderDiffError($container) { + $container.find('.line_content').html( + $(` + <div class="nothing-here-block"> + ${__( + 'Unable to load the diff.', + )} <a class="js-toggle-lazy-diff" href="javascript:void(0)">Try again</a>? + </div> + `), + ); + } + + loadLazyDiff(e) { + const $container = $(e.currentTarget).closest('.js-toggle-container'); + Notes.renderPlaceholderComponent($container); + + $container.find('.js-toggle-lazy-diff').removeClass('js-toggle-lazy-diff'); + + const tableEl = $container.find('tbody'); + if (tableEl.length === 0) return; + + const fileHolder = $container.find('.file-holder'); + const url = fileHolder.data('linesPath'); + + axios + .get(url) + .then(({ data }) => { + Notes.renderDiffContent($container, data); + }) + .catch(() => { + Notes.renderDiffError($container); + }); } toggleCommitList(e) { const $element = $(e.currentTarget); - const $closestSystemCommitList = $element.siblings('.system-note-commit-list'); + const $closestSystemCommitList = $element.siblings( + '.system-note-commit-list', + ); - $element.find('.fa').toggleClass('fa-angle-down').toggleClass('fa-angle-up'); + $element + .find('.fa') + .toggleClass('fa-angle-down') + .toggleClass('fa-angle-up'); $closestSystemCommitList.toggleClass('hide-shade'); } @@ -1221,11 +1484,17 @@ export default class Notes { * intrusive. */ collapseLongCommitList() { - const systemNotes = $('#notes-list').find('li.system-note').has('ul'); + const systemNotes = $('#notes-list') + .find('li.system-note') + .has('ul'); $.each(systemNotes, function(index, systemNote) { const $systemNote = $(systemNote); - const headerMessage = $systemNote.find('.note-text').find('p:first').text().replace(':', ''); + const headerMessage = $systemNote + .find('.note-text') + .find('p:first') + .text() + .replace(':', ''); $systemNote.find('.note-header .system-note-message').html(headerMessage); @@ -1233,7 +1502,9 @@ export default class Notes { $systemNote.find('.note-text').addClass('system-note-commit-list'); $systemNote.find('.system-note-commit-list-toggler').show(); } else { - $systemNote.find('.note-text').addClass('system-note-commit-list hide-shade'); + $systemNote + .find('.note-text') + .addClass('system-note-commit-list hide-shade'); } }); } @@ -1251,14 +1522,10 @@ export default class Notes { cleanForm($form) { // Remove JS classes that are not needed here - $form - .find('.js-comment-type-dropdown') - .removeClass('btn-group'); + $form.find('.js-comment-type-dropdown').removeClass('btn-group'); // Remove dropdown - $form - .find('.dropdown-menu') - .remove(); + $form.find('.dropdown-menu').remove(); return $form; } @@ -1277,7 +1544,11 @@ export default class Notes { // There can be CRLF vs LF mismatches if we don't sanitize and compare the same way const sanitizedNoteEntityText = normalizeNewlines(noteEntity.note.trim()); const currentNoteText = normalizeNewlines( - $note.find('.original-note-content').first().text().trim() + $note + .find('.original-note-content') + .first() + .text() + .trim(), ); return sanitizedNoteEntityText !== currentNoteText; } @@ -1367,7 +1638,14 @@ export default class Notes { * Once comment is _actually_ posted on server, we will have final element * in response that we will show in place of this temporary element. */ - createPlaceholderNote({ formContent, uniqueId, isDiscussionNote, currentUsername, currentUserFullname, currentUserAvatar }) { + createPlaceholderNote({ + formContent, + uniqueId, + isDiscussionNote, + currentUsername, + currentUserFullname, + currentUserAvatar, + }) { const discussionClass = isDiscussionNote ? 'discussion' : ''; const $tempNote = $( `<li id="${uniqueId}" class="note being-posted fade-in-half timeline-entry"> @@ -1381,8 +1659,12 @@ export default class Notes { <div class="note-header"> <div class="note-header-info"> <a href="/${_.escape(currentUsername)}"> - <span class="hidden-xs">${_.escape(currentUsername)}</span> - <span class="note-headline-light">${_.escape(currentUsername)}</span> + <span class="hidden-xs">${_.escape( + currentUsername, + )}</span> + <span class="note-headline-light">${_.escape( + currentUsername, + )}</span> </a> </div> </div> @@ -1393,11 +1675,13 @@ export default class Notes { </div> </div> </div> - </li>` + </li>`, ); $tempNote.find('.hidden-xs').text(_.escape(currentUserFullname)); - $tempNote.find('.note-headline-light').text(`@${_.escape(currentUsername)}`); + $tempNote + .find('.note-headline-light') + .text(`@${_.escape(currentUsername)}`); return $tempNote; } @@ -1413,7 +1697,7 @@ export default class Notes { <i>${formContent}</i> </div> </div> - </li>` + </li>`, ); return $tempNote; @@ -1443,13 +1727,25 @@ export default class Notes { // Get Form metadata const $submitBtn = $(e.target); + $submitBtn.prop('disabled', true); let $form = $submitBtn.parents('form'); const $closeBtn = $form.find('.js-note-target-close'); - const isDiscussionNote = $submitBtn.parent().find('li.droplab-item-selected').attr('id') === 'discussion'; + const isDiscussionNote = + $submitBtn + .parent() + .find('li.droplab-item-selected') + .attr('id') === 'discussion'; const isMainForm = $form.hasClass('js-main-target-form'); const isDiscussionForm = $form.hasClass('js-discussion-note-form'); - const isDiscussionResolve = $submitBtn.hasClass('js-comment-resolve-button'); - const { formData, formContent, formAction, formContentOriginal } = this.getFormData($form); + const isDiscussionResolve = $submitBtn.hasClass( + 'js-comment-resolve-button', + ); + const { + formData, + formContent, + formAction, + formContentOriginal, + } = this.getFormData($form); let noteUniqueId; let systemNoteUniqueId; let hasQuickActions = false; @@ -1466,7 +1762,6 @@ export default class Notes { // If comment is to resolve discussion, disable submit buttons while // comment posting is finished. if (isDiscussionResolve) { - $submitBtn.disable(); $form.find('.js-comment-submit-button').disable(); } @@ -1479,23 +1774,30 @@ export default class Notes { // Show placeholder note if (tempFormContent) { noteUniqueId = _.uniqueId('tempNote_'); - $notesContainer.append(this.createPlaceholderNote({ - formContent: tempFormContent, - uniqueId: noteUniqueId, - isDiscussionNote, - currentUsername: gon.current_username, - currentUserFullname: gon.current_user_fullname, - currentUserAvatar: gon.current_user_avatar_url, - })); + $notesContainer.append( + this.createPlaceholderNote({ + formContent: tempFormContent, + uniqueId: noteUniqueId, + isDiscussionNote, + currentUsername: gon.current_username, + currentUserFullname: gon.current_user_fullname, + currentUserAvatar: gon.current_user_avatar_url, + }), + ); } // Show placeholder system note if (hasQuickActions) { systemNoteUniqueId = _.uniqueId('tempSystemNote_'); - $notesContainer.append(this.createPlaceholderSystemNote({ - formContent: this.getQuickActionDescription(formContent, AjaxCache.get(gl.GfmAutoComplete.dataSources.commands)), - uniqueId: systemNoteUniqueId, - })); + $notesContainer.append( + this.createPlaceholderSystemNote({ + formContent: this.getQuickActionDescription( + formContent, + AjaxCache.get(gl.GfmAutoComplete.dataSources.commands), + ), + uniqueId: systemNoteUniqueId, + }), + ); } // Clear the form textarea @@ -1509,10 +1811,12 @@ export default class Notes { /* eslint-disable promise/catch-or-return */ // Make request to submit comment on server - axios.post(`${formAction}?html=true`, formData) - .then((res) => { + axios + .post(`${formAction}?html=true`, formData) + .then(res => { const note = res.data; + $submitBtn.prop('disabled', false); // Submission successful! remove placeholder $notesContainer.find(`#${noteUniqueId}`).remove(); @@ -1527,7 +1831,9 @@ export default class Notes { // Reset cached commands list when command is applied if (hasQuickActions) { - $form.find('textarea.js-note-text').trigger('clear-commands-cache.atwho'); + $form + .find('textarea.js-note-text') + .trigger('clear-commands-cache.atwho'); } // Clear previous form errors @@ -1572,11 +1878,14 @@ export default class Notes { // append flash-container to the Notes list if ($notesContainer.length) { - $notesContainer.append('<div class="flash-container" style="display: none;"></div>'); + $notesContainer.append( + '<div class="flash-container" style="display: none;"></div>', + ); } Notes.refreshVueNotes(); - } else if (isMainForm) { // Check if this was main thread comment + } else if (isMainForm) { + // Check if this was main thread comment // Show final note element on UI and perform form and action buttons cleanup this.addNote($form, note); this.reenableTargetFormSubmitButton(e); @@ -1587,10 +1896,11 @@ export default class Notes { } $form.trigger('ajax:success', [note]); - }).catch(() => { + }) + .catch(() => { // Submission failed, remove placeholder note and show Flash error message $notesContainer.find(`#${noteUniqueId}`).remove(); - + $submitBtn.prop('disabled', false); const blurEvent = new CustomEvent('blur.imageDiff', { detail: e, }); @@ -1607,7 +1917,9 @@ export default class Notes { // Show form again on UI on failure if (isDiscussionForm && $notesContainer.length) { - const replyButton = $notesContainer.parent().find('.js-discussion-reply-button'); + const replyButton = $notesContainer + .parent() + .find('.js-discussion-reply-button'); this.replyToDiscussionNote(replyButton[0]); $form = $notesContainer.parent().find('form'); } @@ -1652,12 +1964,19 @@ export default class Notes { // Show updated comment content temporarily $noteBodyText.html(formContent); - $editingNote.removeClass('is-editing fade-in-full').addClass('being-posted fade-in-half'); - $editingNote.find('.note-headline-meta a').html('<i class="fa fa-spinner fa-spin" aria-label="Comment is being updated" aria-hidden="true"></i>'); + $editingNote + .removeClass('is-editing fade-in-full') + .addClass('being-posted fade-in-half'); + $editingNote + .find('.note-headline-meta a') + .html( + '<i class="fa fa-spinner fa-spin" aria-label="Comment is being updated" aria-hidden="true"></i>', + ); /* eslint-disable promise/catch-or-return */ // Make request to update comment on server - axios.post(`${formAction}?html=true`, formData) + axios + .post(`${formAction}?html=true`, formData) .then(({ data }) => { // Submission successful! render final note element this.updateNote(data, $editingNote); diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index b85c1a6ad72..90dcafd75b7 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -1,297 +1,311 @@ <script> - import { mapActions, mapGetters } from 'vuex'; - import _ from 'underscore'; - import Autosize from 'autosize'; - import { __, sprintf } from '~/locale'; - import Flash from '../../flash'; - import Autosave from '../../autosave'; - import TaskList from '../../task_list'; - import { capitalizeFirstCharacter, convertToCamelCase } from '../../lib/utils/text_utility'; - import * as constants from '../constants'; - import eventHub from '../event_hub'; - import issueWarning from '../../vue_shared/components/issue/issue_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'; +import $ from 'jquery'; +import { mapActions, mapGetters, mapState } from 'vuex'; +import _ from 'underscore'; +import Autosize from 'autosize'; +import { __, sprintf } from '~/locale'; +import Flash from '../../flash'; +import Autosave from '../../autosave'; +import TaskList from '../../task_list'; +import { + capitalizeFirstCharacter, + convertToCamelCase, +} from '../../lib/utils/text_utility'; +import * as constants from '../constants'; +import eventHub from '../event_hub'; +import issueWarning from '../../vue_shared/components/issue/issue_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'; - export default { - name: 'CommentForm', - components: { - issueWarning, - noteSignedOutWidget, - discussionLockedWidget, - markdownField, - userAvatarLink, - loadingButton, +export default { + name: 'CommentForm', + components: { + issueWarning, + noteSignedOutWidget, + discussionLockedWidget, + markdownField, + userAvatarLink, + loadingButton, + }, + mixins: [issuableStateMixin], + props: { + noteableType: { + type: String, + required: true, }, - mixins: [ - issuableStateMixin, - ], - props: { - noteableType: { - type: String, - required: true, - }, + }, + data() { + return { + note: '', + noteType: constants.COMMENT, + isSubmitting: false, + isSubmitButtonDisabled: true, + }; + }, + computed: { + ...mapGetters([ + 'getCurrentUserLastNote', + 'getUserData', + 'getNoteableData', + 'getNotesData', + 'openState', + ]), + ...mapState(['isToggleStateButtonLoading']), + noteableDisplayName() { + return this.noteableType.replace(/_/g, ' '); }, - data() { - return { - note: '', - noteType: constants.COMMENT, - isSubmitting: false, - isSubmitButtonDisabled: true, - }; + isLoggedIn() { + return this.getUserData.id; + }, + commentButtonTitle() { + return this.noteType === constants.COMMENT + ? 'Comment' + : 'Start discussion'; + }, + isOpen() { + return ( + this.openState === constants.OPENED || + this.openState === constants.REOPENED + ); }, - computed: { - ...mapGetters([ - 'getCurrentUserLastNote', - 'getUserData', - 'getNoteableData', - 'getNotesData', - 'openState', - ]), - noteableDisplayName() { - return this.noteableType.replace(/_/g, ' '); - }, - isLoggedIn() { - return this.getUserData.id; - }, - commentButtonTitle() { - return this.noteType === constants.COMMENT ? 'Comment' : 'Start discussion'; - }, - isOpen() { - return this.openState === constants.OPENED || this.openState === constants.REOPENED; - }, - canCreateNote() { - return this.getNoteableData.current_user.can_create_note; - }, - issueActionButtonTitle() { - const openOrClose = this.isOpen ? 'close' : 'reopen'; + canCreateNote() { + return this.getNoteableData.current_user.can_create_note; + }, + issueActionButtonTitle() { + const openOrClose = this.isOpen ? 'close' : 'reopen'; - if (this.note.length) { - return sprintf( - __('%{actionText} & %{openOrClose} %{noteable}'), - { - actionText: this.commentButtonTitle, - openOrClose, - noteable: this.noteableDisplayName, - }, - ); - } + if (this.note.length) { + return sprintf(__('%{actionText} & %{openOrClose} %{noteable}'), { + actionText: this.commentButtonTitle, + openOrClose, + noteable: this.noteableDisplayName, + }); + } - return sprintf( - __('%{openOrClose} %{noteable}'), - { - openOrClose: capitalizeFirstCharacter(openOrClose), - noteable: this.noteableDisplayName, - }, - ); - }, - actionButtonClassNames() { - return { - 'btn-reopen': !this.isOpen, - 'btn-close': this.isOpen, - 'js-note-target-close': this.isOpen, - 'js-note-target-reopen': !this.isOpen, - }; - }, - markdownDocsPath() { - return this.getNotesData.markdownDocsPath; - }, - quickActionsDocsPath() { - return this.getNotesData.quickActionsDocsPath; - }, - markdownPreviewPath() { - return this.getNoteableData.preview_note_path; - }, - author() { - return this.getUserData; - }, - canUpdateIssue() { - return this.getNoteableData.current_user.can_update; - }, - endpoint() { - return this.getNoteableData.create_note_path; - }, + return sprintf(__('%{openOrClose} %{noteable}'), { + openOrClose: capitalizeFirstCharacter(openOrClose), + noteable: this.noteableDisplayName, + }); }, - watch: { - note(newNote) { - this.setIsSubmitButtonDisabled(newNote, this.isSubmitting); - }, - isSubmitting(newValue) { - this.setIsSubmitButtonDisabled(this.note, newValue); - }, + actionButtonClassNames() { + return { + 'btn-reopen': !this.isOpen, + 'btn-close': this.isOpen, + 'js-note-target-close': this.isOpen, + 'js-note-target-reopen': !this.isOpen, + }; }, - mounted() { - // jQuery is needed here because it is a custom event being dispatched with jQuery. - $(document).on('issuable:change', (e, isClosed) => { - this.toggleIssueLocalState(isClosed ? constants.CLOSED : constants.REOPENED); - }); + markdownDocsPath() { + return this.getNotesData.markdownDocsPath; + }, + quickActionsDocsPath() { + return this.getNotesData.quickActionsDocsPath; + }, + markdownPreviewPath() { + return this.getNoteableData.preview_note_path; + }, + author() { + return this.getUserData; + }, + canUpdateIssue() { + return this.getNoteableData.current_user.can_update; + }, + endpoint() { + return this.getNoteableData.create_note_path; + }, + }, + watch: { + note(newNote) { + this.setIsSubmitButtonDisabled(newNote, this.isSubmitting); + }, + isSubmitting(newValue) { + this.setIsSubmitButtonDisabled(this.note, newValue); + }, + }, + mounted() { + // jQuery is needed here because it is a custom event being dispatched with jQuery. + $(document).on('issuable:change', (e, isClosed) => { + this.toggleIssueLocalState( + isClosed ? constants.CLOSED : constants.REOPENED, + ); + }); - this.initAutoSave(); - this.initTaskList(); + this.initAutoSave(); + this.initTaskList(); + }, + methods: { + ...mapActions([ + 'saveNote', + 'stopPolling', + 'restartPolling', + 'removePlaceholderNotes', + 'closeIssue', + 'reopenIssue', + 'toggleIssueLocalState', + 'toggleStateButtonLoading', + ]), + setIsSubmitButtonDisabled(note, isSubmitting) { + if (!_.isEmpty(note) && !isSubmitting) { + this.isSubmitButtonDisabled = false; + } else { + this.isSubmitButtonDisabled = true; + } }, - methods: { - ...mapActions([ - 'saveNote', - 'stopPolling', - 'restartPolling', - 'removePlaceholderNotes', - 'closeIssue', - 'reopenIssue', - 'toggleIssueLocalState', - ]), - setIsSubmitButtonDisabled(note, isSubmitting) { - if (!_.isEmpty(note) && !isSubmitting) { - this.isSubmitButtonDisabled = false; - } else { - this.isSubmitButtonDisabled = true; - } - }, - handleSave(withIssueAction) { - this.isSubmitting = true; + handleSave(withIssueAction) { + this.isSubmitting = true; - if (this.note.length) { - const noteData = { - endpoint: this.endpoint, - flashContainer: this.$el, - data: { - note: { - noteable_type: this.noteableType, - noteable_id: this.getNoteableData.id, - note: this.note, - }, + if (this.note.length) { + const noteData = { + endpoint: this.endpoint, + flashContainer: this.$el, + data: { + note: { + noteable_type: this.noteableType, + noteable_id: this.getNoteableData.id, + note: this.note, }, - }; + }, + }; - if (this.noteType === constants.DISCUSSION) { - noteData.data.note.type = constants.DISCUSSION_NOTE; - } - this.note = ''; // Empty textarea while being requested. Repopulate in catch - this.resizeTextarea(); - this.stopPolling(); + if (this.noteType === constants.DISCUSSION) { + noteData.data.note.type = constants.DISCUSSION_NOTE; + } - this.saveNote(noteData) - .then((res) => { - this.isSubmitting = false; - this.restartPolling(); + this.note = ''; // Empty textarea while being requested. Repopulate in catch + this.resizeTextarea(); + this.stopPolling(); - if (res.errors) { - if (res.errors.commands_only) { - this.discard(); - } else { - Flash( - 'Something went wrong while adding your comment. Please try again.', - 'alert', - this.$refs.commentForm, - ); - } - } else { + this.saveNote(noteData) + .then(res => { + this.enableButton(); + this.restartPolling(); + + if (res.errors) { + if (res.errors.commands_only) { this.discard(); + } else { + Flash( + 'Something went wrong while adding your comment. Please try again.', + 'alert', + this.$refs.commentForm, + ); } + } else { + this.discard(); + } - if (withIssueAction) { - this.toggleIssueState(); - } - }) - .catch(() => { - this.isSubmitting = false; - this.discard(false); - const msg = - `Your comment could not be submitted! + if (withIssueAction) { + this.toggleIssueState(); + } + }) + .catch(() => { + this.enableButton(); + this.discard(false); + const msg = `Your comment could not be submitted! Please check your network connection and try again.`; - Flash(msg, 'alert', this.$el); - this.note = noteData.data.note.note; // Restore textarea content. - this.removePlaceholderNotes(); - }); - } else { - this.toggleIssueState(); - } - }, - enableButton() { - this.isSubmitting = false; - }, - toggleIssueState() { - if (this.isOpen) { - this.closeIssue() - .then(() => this.enableButton()) - .catch(() => { - this.enableButton(); - Flash( - sprintf( - __('Something went wrong while closing the %{issuable}. Please try again later'), - { issuable: this.noteableDisplayName }, + Flash(msg, 'alert', this.$el); + this.note = noteData.data.note.note; // Restore textarea content. + this.removePlaceholderNotes(); + }); + } else { + this.toggleIssueState(); + } + }, + enableButton() { + this.isSubmitting = false; + }, + toggleIssueState() { + if (this.isOpen) { + this.closeIssue() + .then(() => this.enableButton()) + .catch(() => { + this.enableButton(); + this.toggleStateButtonLoading(false); + Flash( + sprintf( + __( + 'Something went wrong while closing the %{issuable}. Please try again later', ), - ); - }); - } else { - this.reopenIssue() - .then(() => this.enableButton()) - .catch(() => { - this.enableButton(); - Flash( - sprintf( - __('Something went wrong while reopening the %{issuable}. Please try again later'), - { issuable: this.noteableDisplayName }, + { issuable: this.noteableDisplayName }, + ), + ); + }); + } else { + this.reopenIssue() + .then(() => this.enableButton()) + .catch(() => { + this.enableButton(); + this.toggleStateButtonLoading(false); + Flash( + sprintf( + __( + 'Something went wrong while reopening the %{issuable}. Please try again later', ), - ); - }); - } - }, - discard(shouldClear = true) { - // `blur` is needed to clear slash commands autocomplete cache if event fired. - // `focus` is needed to remain cursor in the textarea. - this.$refs.textarea.blur(); - this.$refs.textarea.focus(); + { issuable: this.noteableDisplayName }, + ), + ); + }); + } + }, + discard(shouldClear = true) { + // `blur` is needed to clear slash commands autocomplete cache if event fired. + // `focus` is needed to remain cursor in the textarea. + this.$refs.textarea.blur(); + this.$refs.textarea.focus(); - if (shouldClear) { - this.note = ''; - this.resizeTextarea(); - this.$refs.markdownField.previewMarkdown = false; - } + if (shouldClear) { + this.note = ''; + this.resizeTextarea(); + this.$refs.markdownField.previewMarkdown = false; + } - this.autosave.reset(); - }, - setNoteType(type) { - this.noteType = type; - }, - editCurrentUserLastNote() { - if (this.note === '') { - const lastNote = this.getCurrentUserLastNote; + this.autosave.reset(); + }, + setNoteType(type) { + this.noteType = type; + }, + editCurrentUserLastNote() { + if (this.note === '') { + const lastNote = this.getCurrentUserLastNote; - if (lastNote) { - eventHub.$emit('enterEditMode', { - noteId: lastNote.id, - }); - } + if (lastNote) { + eventHub.$emit('enterEditMode', { + noteId: lastNote.id, + }); } - }, - initAutoSave() { - if (this.isLoggedIn) { - const noteableType = capitalizeFirstCharacter(convertToCamelCase(this.noteableType)); + } + }, + initAutoSave() { + if (this.isLoggedIn) { + const noteableType = capitalizeFirstCharacter( + convertToCamelCase(this.noteableType), + ); - this.autosave = new Autosave( - $(this.$refs.textarea), - ['Note', noteableType, this.getNoteableData.id], - ); - } - }, - initTaskList() { - return new TaskList({ - dataType: 'note', - fieldName: 'note', - selector: '.notes', - }); - }, - resizeTextarea() { - this.$nextTick(() => { - Autosize.update(this.$refs.textarea); - }); - }, + this.autosave = new Autosave($(this.$refs.textarea), [ + 'Note', + noteableType, + this.getNoteableData.id, + ]); + } + }, + initTaskList() { + return new TaskList({ + dataType: 'note', + fieldName: 'note', + selector: '.notes', + }); }, - }; + resizeTextarea() { + this.$nextTick(() => { + Autosize.update(this.$refs.textarea); + }); + }, + }, +}; </script> <template> @@ -418,13 +432,13 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" <loading-button v-if="canUpdateIssue" - :loading="isSubmitting" + :loading="isToggleStateButtonLoading" @click="handleSave(true)" :container-class="[ actionButtonClassNames, 'btn btn-comment btn-comment-and-close js-action-button' ]" - :disabled="isSubmitting" + :disabled="isToggleStateButtonLoading || isSubmitting" :label="issueActionButtonTitle" /> diff --git a/app/assets/javascripts/notes/components/diff_file_header.vue b/app/assets/javascripts/notes/components/diff_file_header.vue index fe5baa3537f..94d9dc69964 100644 --- a/app/assets/javascripts/notes/components/diff_file_header.vue +++ b/app/assets/javascripts/notes/components/diff_file_header.vue @@ -1,24 +1,24 @@ <script> - import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; - import Icon from '~/vue_shared/components/icon.vue'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import Icon from '~/vue_shared/components/icon.vue'; - export default { - components: { - ClipboardButton, - Icon, +export default { + components: { + ClipboardButton, + Icon, + }, + props: { + diffFile: { + type: Object, + required: true, }, - props: { - diffFile: { - type: Object, - required: true, - }, + }, + computed: { + titleTag() { + return this.diffFile.discussionPath ? 'a' : 'span'; }, - computed: { - titleTag() { - return this.diffFile.discussionPath ? 'a' : 'span'; - }, - }, - }; + }, +}; </script> <template> @@ -35,6 +35,7 @@ <clipboard-button title="Copy file path to clipboard" :text="diffFile.submoduleLink" + css-class="btn-default btn-transparent btn-clipboard" /> </span> </div> @@ -79,6 +80,7 @@ <clipboard-button title="Copy file path to clipboard" :text="diffFile.filePath" + css-class="btn-default btn-transparent btn-clipboard" /> <small diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue index 75a32709ad5..ee01ec85bbb 100644 --- a/app/assets/javascripts/notes/components/diff_with_note.vue +++ b/app/assets/javascripts/notes/components/diff_with_note.vue @@ -1,55 +1,60 @@ <script> - import syntaxHighlight from '~/syntax_highlight'; - import imageDiffHelper from '~/image_diff/helpers/index'; - import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; - import DiffFileHeader from './diff_file_header.vue'; +import $ from 'jquery'; +import syntaxHighlight from '~/syntax_highlight'; +import imageDiffHelper from '~/image_diff/helpers/index'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; +import DiffFileHeader from './diff_file_header.vue'; - export default { - components: { - DiffFileHeader, +export default { + components: { + DiffFileHeader, + }, + props: { + discussion: { + type: Object, + required: true, }, - props: { - discussion: { - type: Object, - required: true, - }, + }, + computed: { + isImageDiff() { + return !this.diffFile.text; }, - computed: { - isImageDiff() { - return !this.diffFile.text; - }, - diffFileClass() { - const { text } = this.diffFile; - return text ? 'text-file' : 'js-image-file'; - }, - diffRows() { - return $(this.discussion.truncatedDiffLines); - }, - diffFile() { - return convertObjectPropsToCamelCase(this.discussion.diffFile); - }, - imageDiffHtml() { - return this.discussion.imageDiffHtml; - }, + diffFileClass() { + const { text } = this.diffFile; + return text ? 'text-file' : 'js-image-file'; }, - mounted() { - if (this.isImageDiff) { - const canCreateNote = false; - const renderCommentBadge = true; - imageDiffHelper.initImageDiff(this.$refs.fileHolder, canCreateNote, renderCommentBadge); - } else { - const fileHolder = $(this.$refs.fileHolder); - this.$nextTick(() => { - syntaxHighlight(fileHolder); - }); - } + diffRows() { + return $(this.discussion.truncatedDiffLines); }, - methods: { - rowTag(html) { - return html.outerHTML ? 'tr' : 'template'; - }, + diffFile() { + return convertObjectPropsToCamelCase(this.discussion.diffFile); }, - }; + imageDiffHtml() { + return this.discussion.imageDiffHtml; + }, + }, + mounted() { + if (this.isImageDiff) { + const canCreateNote = false; + const renderCommentBadge = true; + imageDiffHelper.initImageDiff( + this.$refs.fileHolder, + canCreateNote, + renderCommentBadge, + ); + } else { + const fileHolder = $(this.$refs.fileHolder); + this.$nextTick(() => { + syntaxHighlight(fileHolder); + }); + } + }, + methods: { + rowTag(html) { + return html.outerHTML ? 'tr' : 'template'; + }, + }, +}; </script> <template> diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue index 0158f58b569..d492d1cd001 100644 --- a/app/assets/javascripts/notes/components/discussion_counter.vue +++ b/app/assets/javascripts/notes/components/discussion_counter.vue @@ -1,67 +1,69 @@ <script> - import { mapGetters } from 'vuex'; - import resolveSvg from 'icons/_icon_resolve_discussion.svg'; - import resolvedSvg from 'icons/_icon_status_success_solid.svg'; - import mrIssueSvg from 'icons/_icon_mr_issue.svg'; - import nextDiscussionSvg from 'icons/_next_discussion.svg'; - import { pluralize } from '../../lib/utils/text_utility'; - import { scrollToElement } from '../../lib/utils/common_utils'; - import tooltip from '../../vue_shared/directives/tooltip'; +import { mapGetters } from 'vuex'; +import resolveSvg from 'icons/_icon_resolve_discussion.svg'; +import resolvedSvg from 'icons/_icon_status_success_solid.svg'; +import mrIssueSvg from 'icons/_icon_mr_issue.svg'; +import nextDiscussionSvg from 'icons/_next_discussion.svg'; +import { pluralize } from '../../lib/utils/text_utility'; +import { scrollToElement } from '../../lib/utils/common_utils'; +import tooltip from '../../vue_shared/directives/tooltip'; - export default { - directives: { - tooltip, +export default { + directives: { + tooltip, + }, + computed: { + ...mapGetters([ + 'getUserData', + 'getNoteableData', + 'discussionCount', + 'unresolvedDiscussions', + 'resolvedDiscussionCount', + ]), + isLoggedIn() { + return this.getUserData.id; }, - computed: { - ...mapGetters([ - 'getUserData', - 'getNoteableData', - 'discussionCount', - 'unresolvedDiscussions', - 'resolvedDiscussionCount', - ]), - isLoggedIn() { - return this.getUserData.id; - }, - hasNextButton() { - return this.isLoggedIn && !this.allResolved; - }, - countText() { - return pluralize('discussion', this.discussionCount); - }, - allResolved() { - return this.resolvedDiscussionCount === this.discussionCount; - }, - resolveAllDiscussionsIssuePath() { - return this.getNoteableData.create_issue_to_resolve_discussions_path; - }, - firstUnresolvedDiscussionId() { - const item = this.unresolvedDiscussions[0] || {}; - - return item.id; - }, + hasNextButton() { + return this.isLoggedIn && !this.allResolved; + }, + countText() { + return pluralize('discussion', this.discussionCount); + }, + allResolved() { + return this.resolvedDiscussionCount === this.discussionCount; }, - created() { - this.resolveSvg = resolveSvg; - this.resolvedSvg = resolvedSvg; - this.mrIssueSvg = mrIssueSvg; - this.nextDiscussionSvg = nextDiscussionSvg; + resolveAllDiscussionsIssuePath() { + return this.getNoteableData.create_issue_to_resolve_discussions_path; + }, + firstUnresolvedDiscussionId() { + const item = this.unresolvedDiscussions[0] || {}; + + return item.id; }, - methods: { - jumpToFirstDiscussion() { - const el = document.querySelector(`[data-discussion-id="${this.firstUnresolvedDiscussionId}"]`); - const activeTab = window.mrTabs.currentAction; + }, + created() { + this.resolveSvg = resolveSvg; + this.resolvedSvg = resolvedSvg; + this.mrIssueSvg = mrIssueSvg; + this.nextDiscussionSvg = nextDiscussionSvg; + }, + methods: { + jumpToFirstDiscussion() { + const el = document.querySelector( + `[data-discussion-id="${this.firstUnresolvedDiscussionId}"]`, + ); + const activeTab = window.mrTabs.currentAction; - if (activeTab === 'commits' || activeTab === 'pipelines') { - window.mrTabs.activateTab('show'); - } + if (activeTab === 'commits' || activeTab === 'pipelines') { + window.mrTabs.activateTab('show'); + } - if (el) { - scrollToElement(el); - } - }, + if (el) { + scrollToElement(el); + } }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/notes/components/discussion_locked_widget.vue b/app/assets/javascripts/notes/components/discussion_locked_widget.vue index fc0722042cc..13283b187d1 100644 --- a/app/assets/javascripts/notes/components/discussion_locked_widget.vue +++ b/app/assets/javascripts/notes/components/discussion_locked_widget.vue @@ -1,15 +1,13 @@ <script> - import Icon from '~/vue_shared/components/icon.vue'; - import Issuable from '~/vue_shared/mixins/issuable'; +import Icon from '~/vue_shared/components/icon.vue'; +import Issuable from '~/vue_shared/mixins/issuable'; - export default { - components: { - Icon, - }, - mixins: [ - Issuable, - ], - }; +export default { + components: { + Icon, + }, + mixins: [Issuable], +}; </script> <template> diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue index c26aa6fa15d..a7e2d857013 100644 --- a/app/assets/javascripts/notes/components/note_actions.vue +++ b/app/assets/javascripts/notes/components/note_actions.vue @@ -1,121 +1,119 @@ <script> - import { mapGetters } from 'vuex'; - import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg'; - import emojiSmile from 'icons/_emoji_smile.svg'; - import emojiSmiley from 'icons/_emoji_smiley.svg'; - import editSvg from 'icons/_icon_pencil.svg'; - import resolveDiscussionSvg from 'icons/_icon_resolve_discussion.svg'; - import resolvedDiscussionSvg from 'icons/_icon_status_success_solid.svg'; - import ellipsisSvg from 'icons/_ellipsis_v.svg'; - import loadingIcon from '~/vue_shared/components/loading_icon.vue'; - import tooltip from '~/vue_shared/directives/tooltip'; +import { mapGetters } from 'vuex'; +import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg'; +import emojiSmile from 'icons/_emoji_smile.svg'; +import emojiSmiley from 'icons/_emoji_smiley.svg'; +import editSvg from 'icons/_icon_pencil.svg'; +import resolveDiscussionSvg from 'icons/_icon_resolve_discussion.svg'; +import resolvedDiscussionSvg from 'icons/_icon_status_success_solid.svg'; +import ellipsisSvg from 'icons/_ellipsis_v.svg'; +import loadingIcon from '~/vue_shared/components/loading_icon.vue'; +import tooltip from '~/vue_shared/directives/tooltip'; - export default { - name: 'NoteActions', - directives: { - tooltip, - }, - components: { - loadingIcon, - }, - props: { - authorId: { - type: Number, - required: true, - }, - noteId: { - type: Number, - required: true, - }, - accessLevel: { - type: String, - required: false, - default: '', - }, - reportAbusePath: { - type: String, - required: true, - }, - canEdit: { - type: Boolean, - required: true, - }, - canDelete: { - type: Boolean, - required: true, - }, - resolvable: { - type: Boolean, - required: false, - default: false, - }, - isResolved: { - type: Boolean, - required: false, - default: false, - }, - isResolving: { - type: Boolean, - required: false, - default: false, - }, - resolvedBy: { - type: Object, - required: false, - default: () => ({}), - }, - canReportAsAbuse: { - type: Boolean, - required: true, - }, - }, - computed: { - ...mapGetters([ - 'getUserDataByProp', - ]), - shouldShowActionsDropdown() { - return this.currentUserId && (this.canEdit || this.canReportAsAbuse); - }, - canAddAwardEmoji() { - return this.currentUserId; - }, - isAuthoredByCurrentUser() { - return this.authorId === this.currentUserId; - }, - currentUserId() { - return this.getUserDataByProp('id'); - }, - resolveButtonTitle() { - let title = 'Mark as resolved'; +export default { + name: 'NoteActions', + directives: { + tooltip, + }, + components: { + loadingIcon, + }, + props: { + authorId: { + type: Number, + required: true, + }, + noteId: { + type: Number, + required: true, + }, + accessLevel: { + type: String, + required: false, + default: '', + }, + reportAbusePath: { + type: String, + required: true, + }, + canEdit: { + type: Boolean, + required: true, + }, + canDelete: { + type: Boolean, + required: true, + }, + resolvable: { + type: Boolean, + required: false, + default: false, + }, + isResolved: { + type: Boolean, + required: false, + default: false, + }, + isResolving: { + type: Boolean, + required: false, + default: false, + }, + resolvedBy: { + type: Object, + required: false, + default: () => ({}), + }, + canReportAsAbuse: { + type: Boolean, + required: true, + }, + }, + computed: { + ...mapGetters(['getUserDataByProp']), + shouldShowActionsDropdown() { + return this.currentUserId && (this.canEdit || this.canReportAsAbuse); + }, + canAddAwardEmoji() { + return this.currentUserId; + }, + isAuthoredByCurrentUser() { + return this.authorId === this.currentUserId; + }, + currentUserId() { + return this.getUserDataByProp('id'); + }, + resolveButtonTitle() { + let title = 'Mark as resolved'; - if (this.resolvedBy) { - title = `Resolved by ${this.resolvedBy.name}`; - } + if (this.resolvedBy) { + title = `Resolved by ${this.resolvedBy.name}`; + } - return title; - }, - }, - created() { - this.emojiSmiling = emojiSmiling; - this.emojiSmile = emojiSmile; - this.emojiSmiley = emojiSmiley; - this.editSvg = editSvg; - this.ellipsisSvg = ellipsisSvg; - this.resolveDiscussionSvg = resolveDiscussionSvg; - this.resolvedDiscussionSvg = resolvedDiscussionSvg; - }, - methods: { - onEdit() { - this.$emit('handleEdit'); - }, - onDelete() { - this.$emit('handleDelete'); - }, - onResolve() { - this.$emit('handleResolve'); - }, - }, - }; + return title; + }, + }, + created() { + this.emojiSmiling = emojiSmiling; + this.emojiSmile = emojiSmile; + this.emojiSmiley = emojiSmiley; + this.editSvg = editSvg; + this.ellipsisSvg = ellipsisSvg; + this.resolveDiscussionSvg = resolveDiscussionSvg; + this.resolvedDiscussionSvg = resolvedDiscussionSvg; + }, + methods: { + onEdit() { + this.$emit('handleEdit'); + }, + onDelete() { + this.$emit('handleDelete'); + }, + onResolve() { + this.$emit('handleResolve'); + }, + }, +}; </script> <template> diff --git a/app/assets/javascripts/notes/components/note_attachment.vue b/app/assets/javascripts/notes/components/note_attachment.vue index 618b807b9cc..34ecbd00c63 100644 --- a/app/assets/javascripts/notes/components/note_attachment.vue +++ b/app/assets/javascripts/notes/components/note_attachment.vue @@ -1,13 +1,13 @@ <script> - export default { - name: 'NoteAttachment', - props: { - attachment: { - type: Object, - required: true, - }, +export default { + name: 'NoteAttachment', + props: { + attachment: { + type: Object, + required: true, }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue index caa9701e03f..6cb8229e268 100644 --- a/app/assets/javascripts/notes/components/note_awards_list.vue +++ b/app/assets/javascripts/notes/components/note_awards_list.vue @@ -1,179 +1,192 @@ <script> - import { mapActions, mapGetters } from 'vuex'; - import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg'; - import emojiSmile from 'icons/_emoji_smile.svg'; - import emojiSmiley from 'icons/_emoji_smiley.svg'; - import Flash from '../../flash'; - import { glEmojiTag } from '../../emoji'; - import tooltip from '../../vue_shared/directives/tooltip'; - - export default { - directives: { - tooltip, +import { mapActions, mapGetters } from 'vuex'; +import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg'; +import emojiSmile from 'icons/_emoji_smile.svg'; +import emojiSmiley from 'icons/_emoji_smiley.svg'; +import Flash from '../../flash'; +import { glEmojiTag } from '../../emoji'; +import tooltip from '../../vue_shared/directives/tooltip'; + +export default { + directives: { + tooltip, + }, + props: { + awards: { + type: Array, + required: true, }, - props: { - awards: { - type: Array, - required: true, - }, - toggleAwardPath: { - type: String, - required: true, - }, - noteAuthorId: { - type: Number, - required: true, - }, - noteId: { - type: Number, - required: true, - }, + toggleAwardPath: { + type: String, + required: true, }, - computed: { - ...mapGetters([ - 'getUserData', - ]), - // `this.awards` is an array with emojis but they are not grouped by emoji name. See below. - // [ { name: foo, user: user1 }, { name: bar, user: user1 }, { name: foo, user: user2 } ] - // This method will group emojis by their name as an Object. See below. - // { - // foo: [ { name: foo, user: user1 }, { name: foo, user: user2 } ], - // bar: [ { name: bar, user: user1 } ] - // } - // We need to do this otherwise we will render the same emoji over and over again. - groupedAwards() { - const awards = this.awards.reduce((acc, award) => { - if (Object.prototype.hasOwnProperty.call(acc, award.name)) { - acc[award.name].push(award); - } else { - Object.assign(acc, { [award.name]: [award] }); - } - - return acc; - }, {}); - - const orderedAwards = {}; - const { thumbsdown, thumbsup } = awards; - // Always show thumbsup and thumbsdown first - if (thumbsup) { - orderedAwards.thumbsup = thumbsup; - delete awards.thumbsup; - } - if (thumbsdown) { - orderedAwards.thumbsdown = thumbsdown; - delete awards.thumbsdown; - } - - return Object.assign({}, orderedAwards, awards); - }, - isAuthoredByMe() { - return this.noteAuthorId === this.getUserData.id; - }, - isLoggedIn() { - return this.getUserData.id; - }, + noteAuthorId: { + type: Number, + required: true, }, - created() { - this.emojiSmiling = emojiSmiling; - this.emojiSmile = emojiSmile; - this.emojiSmiley = emojiSmiley; + noteId: { + type: Number, + required: true, }, - methods: { - ...mapActions([ - 'toggleAwardRequest', - ]), - getAwardHTML(name) { - return glEmojiTag(name); - }, - getAwardClassBindings(awardList, awardName) { - return { - active: this.hasReactionByCurrentUser(awardList), - disabled: !this.canInteractWithEmoji(awardList, awardName), - }; - }, - canInteractWithEmoji(awardList, awardName) { - let isAllowed = true; - const restrictedEmojis = ['thumbsup', 'thumbsdown']; - - // Users can not add :+1: and :-1: to their own notes - if (this.getUserData.id === this.noteAuthorId && restrictedEmojis.indexOf(awardName) > -1) { - isAllowed = false; - } - - return this.getUserData.id && isAllowed; - }, - hasReactionByCurrentUser(awardList) { - return awardList.filter(award => award.user.id === this.getUserData.id).length; - }, - awardTitle(awardsList) { - const hasReactionByCurrentUser = this.hasReactionByCurrentUser(awardsList); - const TOOLTIP_NAME_COUNT = hasReactionByCurrentUser ? 9 : 10; - let awardList = awardsList; - - // Filter myself from list if I am awarded. - if (hasReactionByCurrentUser) { - awardList = awardList.filter(award => award.user.id !== this.getUserData.id); - } - - // Get only 9-10 usernames to show in tooltip text. - const namesToShow = awardList.slice(0, TOOLTIP_NAME_COUNT).map(award => award.user.name); - - // Get the remaining list to use in `and x more` text. - const remainingAwardList = awardList.slice(TOOLTIP_NAME_COUNT, awardList.length); - - // Add myself to the begining of the list so title will start with You. - if (hasReactionByCurrentUser) { - namesToShow.unshift('You'); - } - - let title = ''; - - // We have 10+ awarded user, join them with comma and add `and x more`. - if (remainingAwardList.length) { - title = `${namesToShow.join(', ')}, and ${remainingAwardList.length} more.`; - } else if (namesToShow.length > 1) { - // Join all names with comma but not the last one, it will be added with and text. - title = namesToShow.slice(0, namesToShow.length - 1).join(', '); - // If we have more than 2 users we need an extra comma before and text. - title += namesToShow.length > 2 ? ',' : ''; - title += ` and ${namesToShow.slice(-1)}`; // Append and text - } else { // We have only 2 users so join them with and. - title = namesToShow.join(' and '); - } - - return title; - }, - handleAward(awardName) { - if (!this.isLoggedIn) { - return; - } - - let parsedName; - - // 100 and 1234 emoji are a number. Callback for v-for click sends it as a string - switch (awardName) { - case '100': - parsedName = 100; - break; - case '1234': - parsedName = 1234; - break; - default: - parsedName = awardName; - break; + }, + computed: { + ...mapGetters(['getUserData']), + // `this.awards` is an array with emojis but they are not grouped by emoji name. See below. + // [ { name: foo, user: user1 }, { name: bar, user: user1 }, { name: foo, user: user2 } ] + // This method will group emojis by their name as an Object. See below. + // { + // foo: [ { name: foo, user: user1 }, { name: foo, user: user2 } ], + // bar: [ { name: bar, user: user1 } ] + // } + // We need to do this otherwise we will render the same emoji over and over again. + groupedAwards() { + const awards = this.awards.reduce((acc, award) => { + if (Object.prototype.hasOwnProperty.call(acc, award.name)) { + acc[award.name].push(award); + } else { + Object.assign(acc, { [award.name]: [award] }); } - const data = { - endpoint: this.toggleAwardPath, - noteId: this.noteId, - awardName: parsedName, - }; - - this.toggleAwardRequest(data) - .catch(() => Flash('Something went wrong on our end.')); - }, + return acc; + }, {}); + + const orderedAwards = {}; + const { thumbsdown, thumbsup } = awards; + // Always show thumbsup and thumbsdown first + if (thumbsup) { + orderedAwards.thumbsup = thumbsup; + delete awards.thumbsup; + } + if (thumbsdown) { + orderedAwards.thumbsdown = thumbsdown; + delete awards.thumbsdown; + } + + return Object.assign({}, orderedAwards, awards); + }, + isAuthoredByMe() { + return this.noteAuthorId === this.getUserData.id; + }, + isLoggedIn() { + return this.getUserData.id; + }, + }, + created() { + this.emojiSmiling = emojiSmiling; + this.emojiSmile = emojiSmile; + this.emojiSmiley = emojiSmiley; + }, + methods: { + ...mapActions(['toggleAwardRequest']), + getAwardHTML(name) { + return glEmojiTag(name); + }, + getAwardClassBindings(awardList, awardName) { + return { + active: this.hasReactionByCurrentUser(awardList), + disabled: !this.canInteractWithEmoji(awardList, awardName), + }; + }, + canInteractWithEmoji(awardList, awardName) { + let isAllowed = true; + const restrictedEmojis = ['thumbsup', 'thumbsdown']; + + // Users can not add :+1: and :-1: to their own notes + if ( + this.getUserData.id === this.noteAuthorId && + restrictedEmojis.indexOf(awardName) > -1 + ) { + isAllowed = false; + } + + return this.getUserData.id && isAllowed; + }, + hasReactionByCurrentUser(awardList) { + return awardList.filter(award => award.user.id === this.getUserData.id) + .length; + }, + awardTitle(awardsList) { + const hasReactionByCurrentUser = this.hasReactionByCurrentUser( + awardsList, + ); + const TOOLTIP_NAME_COUNT = hasReactionByCurrentUser ? 9 : 10; + let awardList = awardsList; + + // Filter myself from list if I am awarded. + if (hasReactionByCurrentUser) { + awardList = awardList.filter( + award => award.user.id !== this.getUserData.id, + ); + } + + // Get only 9-10 usernames to show in tooltip text. + const namesToShow = awardList + .slice(0, TOOLTIP_NAME_COUNT) + .map(award => award.user.name); + + // Get the remaining list to use in `and x more` text. + const remainingAwardList = awardList.slice( + TOOLTIP_NAME_COUNT, + awardList.length, + ); + + // Add myself to the begining of the list so title will start with You. + if (hasReactionByCurrentUser) { + namesToShow.unshift('You'); + } + + let title = ''; + + // We have 10+ awarded user, join them with comma and add `and x more`. + if (remainingAwardList.length) { + title = `${namesToShow.join(', ')}, and ${ + remainingAwardList.length + } more.`; + } else if (namesToShow.length > 1) { + // Join all names with comma but not the last one, it will be added with and text. + title = namesToShow.slice(0, namesToShow.length - 1).join(', '); + // If we have more than 2 users we need an extra comma before and text. + title += namesToShow.length > 2 ? ',' : ''; + title += ` and ${namesToShow.slice(-1)}`; // Append and text + } else { + // We have only 2 users so join them with and. + title = namesToShow.join(' and '); + } + + return title; + }, + handleAward(awardName) { + if (!this.isLoggedIn) { + return; + } + + let parsedName; + + // 100 and 1234 emoji are a number. Callback for v-for click sends it as a string + switch (awardName) { + case '100': + parsedName = 100; + break; + case '1234': + parsedName = 1234; + break; + default: + parsedName = awardName; + break; + } + + const data = { + endpoint: this.toggleAwardPath, + noteId: this.noteId, + awardName: parsedName, + }; + + this.toggleAwardRequest(data).catch(() => + Flash('Something went wrong on our end.'), + ); }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue index ca12df9db64..069f94c5845 100644 --- a/app/assets/javascripts/notes/components/note_body.vue +++ b/app/assets/javascripts/notes/components/note_body.vue @@ -1,82 +1,81 @@ <script> - import noteEditedText from './note_edited_text.vue'; - import noteAwardsList from './note_awards_list.vue'; - import noteAttachment from './note_attachment.vue'; - import noteForm from './note_form.vue'; - import TaskList from '../../task_list'; - import autosave from '../mixins/autosave'; +import $ from 'jquery'; +import noteEditedText from './note_edited_text.vue'; +import noteAwardsList from './note_awards_list.vue'; +import noteAttachment from './note_attachment.vue'; +import noteForm from './note_form.vue'; +import TaskList from '../../task_list'; +import autosave from '../mixins/autosave'; - export default { - components: { - noteEditedText, - noteAwardsList, - noteAttachment, - noteForm, +export default { + components: { + noteEditedText, + noteAwardsList, + noteAttachment, + noteForm, + }, + mixins: [autosave], + props: { + note: { + type: Object, + required: true, }, - mixins: [ - autosave, - ], - props: { - note: { - type: Object, - required: true, - }, - canEdit: { - type: Boolean, - required: true, - }, - isEditing: { - type: Boolean, - required: false, - default: false, - }, + canEdit: { + type: Boolean, + required: true, }, - computed: { - noteBody() { - return this.note.note; - }, + isEditing: { + type: Boolean, + required: false, + default: false, }, - mounted() { - this.renderGFM(); - this.initTaskList(); + }, + computed: { + noteBody() { + return this.note.note; + }, + }, + mounted() { + this.renderGFM(); + this.initTaskList(); + + if (this.isEditing) { + this.initAutoSave(this.note.noteable_type); + } + }, + updated() { + this.initTaskList(); + this.renderGFM(); - if (this.isEditing) { + if (this.isEditing) { + if (!this.autosave) { this.initAutoSave(this.note.noteable_type); + } else { + this.setAutoSave(); } + } + }, + methods: { + renderGFM() { + $(this.$refs['note-body']).renderGFM(); }, - updated() { - this.initTaskList(); - this.renderGFM(); - - if (this.isEditing) { - if (!this.autosave) { - this.initAutoSave(this.note.noteable_type); - } else { - this.setAutoSave(); - } + initTaskList() { + if (this.canEdit) { + this.taskList = new TaskList({ + dataType: 'note', + fieldName: 'note', + selector: '.notes', + }); } }, - methods: { - renderGFM() { - $(this.$refs['note-body']).renderGFM(); - }, - initTaskList() { - if (this.canEdit) { - this.taskList = new TaskList({ - dataType: 'note', - fieldName: 'note', - selector: '.notes', - }); - } - }, - handleFormUpdate(note, parentElement, callback) { - this.$emit('handleFormUpdate', note, parentElement, callback); - }, - formCancelHandler(shouldConfirm, isDirty) { - this.$emit('cancelFormEdition', shouldConfirm, isDirty); - }, + handleFormUpdate(note, parentElement, callback) { + this.$emit('handleFormUpdate', note, parentElement, callback); + }, + formCancelHandler(shouldConfirm, isDirty) { + this.$emit('cancelFormEdition', shouldConfirm, isDirty); }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/notes/components/note_edited_text.vue b/app/assets/javascripts/notes/components/note_edited_text.vue index ae2e52554d2..4ddca918495 100644 --- a/app/assets/javascripts/notes/components/note_edited_text.vue +++ b/app/assets/javascripts/notes/components/note_edited_text.vue @@ -1,32 +1,32 @@ <script> - import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; +import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; - export default { - name: 'EditedNoteText', - components: { - timeAgoTooltip, +export default { + name: 'EditedNoteText', + components: { + timeAgoTooltip, + }, + props: { + actionText: { + type: String, + required: true, }, - props: { - actionText: { - type: String, - required: true, - }, - editedAt: { - type: String, - required: true, - }, - editedBy: { - type: Object, - required: false, - default: () => ({}), - }, - className: { - type: String, - required: false, - default: 'edited-text', - }, + editedAt: { + type: String, + required: true, }, - }; + editedBy: { + type: Object, + required: false, + default: () => ({}), + }, + className: { + type: String, + required: false, + default: 'edited-text', + }, + }, +}; </script> <template> diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index 1a13fdbeb7c..c59a2e7a406 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -1,128 +1,136 @@ <script> - import { mapGetters, mapActions } from 'vuex'; - import eventHub from '../event_hub'; - import issueWarning from '../../vue_shared/components/issue/issue_warning.vue'; - import markdownField from '../../vue_shared/components/markdown/field.vue'; - import issuableStateMixin from '../mixins/issuable_state'; - import resolvable from '../mixins/resolvable'; +import { mapGetters, mapActions } from 'vuex'; +import eventHub from '../event_hub'; +import issueWarning from '../../vue_shared/components/issue/issue_warning.vue'; +import markdownField from '../../vue_shared/components/markdown/field.vue'; +import issuableStateMixin from '../mixins/issuable_state'; +import resolvable from '../mixins/resolvable'; - export default { - name: 'IssueNoteForm', - components: { - issueWarning, - markdownField, +export default { + name: 'IssueNoteForm', + components: { + issueWarning, + markdownField, + }, + mixins: [issuableStateMixin, resolvable], + props: { + noteBody: { + type: String, + required: false, + default: '', }, - mixins: [ - issuableStateMixin, - resolvable, - ], - props: { - noteBody: { - type: String, - required: false, - default: '', - }, - noteId: { - type: Number, - required: false, - default: 0, - }, - saveButtonTitle: { - type: String, - required: false, - default: 'Save comment', - }, - note: { - type: Object, - required: false, - default: () => ({}), - }, - isEditing: { - type: Boolean, - required: true, - }, + noteId: { + type: Number, + required: false, + default: 0, }, - data() { - return { - updatedNoteBody: this.noteBody, - conflictWhileEditing: false, - isSubmitting: false, - isResolving: false, - resolveAsThread: true, - }; + saveButtonTitle: { + type: String, + required: false, + default: 'Save comment', }, - computed: { - ...mapGetters([ - 'getDiscussionLastNote', - 'getNoteableData', - 'getNoteableDataByProp', - 'getNotesDataByProp', - 'getUserDataByProp', - ]), - noteHash() { - return `#note_${this.noteId}`; - }, - markdownPreviewPath() { - return this.getNoteableDataByProp('preview_note_path'); - }, - markdownDocsPath() { - return this.getNotesDataByProp('markdownDocsPath'); - }, - quickActionsDocsPath() { - return !this.isEditing ? this.getNotesDataByProp('quickActionsDocsPath') : undefined; - }, - currentUserId() { - return this.getUserDataByProp('id'); - }, - isDisabled() { - return !this.updatedNoteBody.length || this.isSubmitting; - }, + note: { + type: Object, + required: false, + default: () => ({}), }, - watch: { - noteBody() { - if (this.updatedNoteBody === this.noteBody) { - this.updatedNoteBody = this.noteBody; - } else { - this.conflictWhileEditing = true; - } - }, + isEditing: { + type: Boolean, + required: true, + }, + }, + data() { + return { + updatedNoteBody: this.noteBody, + conflictWhileEditing: false, + isSubmitting: false, + isResolving: false, + resolveAsThread: true, + }; + }, + computed: { + ...mapGetters([ + 'getDiscussionLastNote', + 'getNoteableData', + 'getNoteableDataByProp', + 'getNotesDataByProp', + 'getUserDataByProp', + ]), + noteHash() { + return `#note_${this.noteId}`; + }, + markdownPreviewPath() { + return this.getNoteableDataByProp('preview_note_path'); + }, + markdownDocsPath() { + return this.getNotesDataByProp('markdownDocsPath'); + }, + quickActionsDocsPath() { + return !this.isEditing + ? this.getNotesDataByProp('quickActionsDocsPath') + : undefined; }, - mounted() { - this.$refs.textarea.focus(); + currentUserId() { + return this.getUserDataByProp('id'); }, - methods: { - ...mapActions([ - 'toggleResolveNote', - ]), - handleUpdate(shouldResolve) { - const beforeSubmitDiscussionState = this.discussionResolved; - this.isSubmitting = true; + isDisabled() { + return !this.updatedNoteBody.length || this.isSubmitting; + }, + }, + watch: { + noteBody() { + if (this.updatedNoteBody === this.noteBody) { + this.updatedNoteBody = this.noteBody; + } else { + this.conflictWhileEditing = true; + } + }, + }, + mounted() { + this.$refs.textarea.focus(); + }, + methods: { + ...mapActions(['toggleResolveNote']), + handleUpdate(shouldResolve) { + const beforeSubmitDiscussionState = this.discussionResolved; + this.isSubmitting = true; - this.$emit('handleFormUpdate', this.updatedNoteBody, this.$refs.editNoteForm, () => { + this.$emit( + 'handleFormUpdate', + this.updatedNoteBody, + this.$refs.editNoteForm, + () => { this.isSubmitting = false; if (shouldResolve) { this.resolveHandler(beforeSubmitDiscussionState); } - }); - }, - editMyLastNote() { - if (this.updatedNoteBody === '') { - const lastNoteInDiscussion = this.getDiscussionLastNote(this.updatedNoteBody); + }, + ); + }, + editMyLastNote() { + if (this.updatedNoteBody === '') { + const lastNoteInDiscussion = this.getDiscussionLastNote( + this.updatedNoteBody, + ); - if (lastNoteInDiscussion) { - eventHub.$emit('enterEditMode', { - noteId: lastNoteInDiscussion.id, - }); - } + if (lastNoteInDiscussion) { + eventHub.$emit('enterEditMode', { + noteId: lastNoteInDiscussion.id, + }); } - }, - cancelHandler(shouldConfirm = false) { - // Sends information about confirm message and if the textarea has changed - this.$emit('cancelFormEdition', shouldConfirm, this.noteBody !== this.updatedNoteBody); - }, + } + }, + cancelHandler(shouldConfirm = false) { + // Sends information about confirm message and if the textarea has changed + this.$emit( + 'cancelFormEdition', + shouldConfirm, + this.noteBody !== this.updatedNoteBody, + ); }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue index 4743d95b951..c3d1ef1fcc6 100644 --- a/app/assets/javascripts/notes/components/note_header.vue +++ b/app/assets/javascripts/notes/components/note_header.vue @@ -1,65 +1,63 @@ <script> - import { mapActions } from 'vuex'; - import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; +import { mapActions } from 'vuex'; +import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; - export default { - components: { - timeAgoTooltip, +export default { + components: { + timeAgoTooltip, + }, + props: { + author: { + type: Object, + required: true, }, - props: { - author: { - type: Object, - required: true, - }, - createdAt: { - type: String, - required: true, - }, - actionText: { - type: String, - required: false, - default: '', - }, - actionTextHtml: { - type: String, - required: false, - default: '', - }, - noteId: { - type: Number, - required: true, - }, - includeToggle: { - type: Boolean, - required: false, - default: false, - }, - expanded: { - type: Boolean, - required: false, - default: true, - }, + createdAt: { + type: String, + required: true, }, - computed: { - toggleChevronClass() { - return this.expanded ? 'fa-chevron-up' : 'fa-chevron-down'; - }, - noteTimestampLink() { - return `#note_${this.noteId}`; - }, + actionText: { + type: String, + required: false, + default: '', }, - methods: { - ...mapActions([ - 'setTargetNoteHash', - ]), - handleToggle() { - this.$emit('toggleHandler'); - }, - updateTargetNoteHash() { - this.setTargetNoteHash(this.noteTimestampLink); - }, + actionTextHtml: { + type: String, + required: false, + default: '', }, - }; + noteId: { + type: Number, + required: true, + }, + includeToggle: { + type: Boolean, + required: false, + default: false, + }, + expanded: { + type: Boolean, + required: false, + default: true, + }, + }, + computed: { + toggleChevronClass() { + return this.expanded ? 'fa-chevron-up' : 'fa-chevron-down'; + }, + noteTimestampLink() { + return `#note_${this.noteId}`; + }, + }, + methods: { + ...mapActions(['setTargetNoteHash']), + handleToggle() { + this.$emit('toggleHandler'); + }, + updateTargetNoteHash() { + this.setTargetNoteHash(this.noteTimestampLink); + }, + }, +}; </script> <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 45d3c2de355..91f7c269757 100644 --- a/app/assets/javascripts/notes/components/note_signed_out_widget.vue +++ b/app/assets/javascripts/notes/components/note_signed_out_widget.vue @@ -1,19 +1,17 @@ <script> - import { mapGetters } from 'vuex'; +import { mapGetters } from 'vuex'; - export default { - computed: { - ...mapGetters([ - 'getNotesDataByProp', - ]), - registerLink() { - return this.getNotesDataByProp('registerPath'); - }, - signInLink() { - return this.getNotesDataByProp('newSessionPath'); - }, +export default { + computed: { + ...mapGetters(['getNotesDataByProp']), + registerLink() { + return this.getNotesDataByProp('registerPath'); }, - }; + signInLink() { + return this.getNotesDataByProp('newSessionPath'); + }, + }, +}; </script> <template> diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index 76bb53eaf2f..cf579c5d4dc 100644 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -1,210 +1,210 @@ <script> - import { mapActions, mapGetters } from 'vuex'; - import resolveDiscussionsSvg from 'icons/_icon_mr_issue.svg'; - import nextDiscussionsSvg from 'icons/_next_discussion.svg'; - import Flash from '../../flash'; - import { SYSTEM_NOTE } from '../constants'; - import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; - import noteableNote from './noteable_note.vue'; - import noteHeader from './note_header.vue'; - import noteSignedOutWidget from './note_signed_out_widget.vue'; - import noteEditedText from './note_edited_text.vue'; - import noteForm from './note_form.vue'; - import diffWithNote from './diff_with_note.vue'; - import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue'; - import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue'; - import autosave from '../mixins/autosave'; - import noteable from '../mixins/noteable'; - import resolvable from '../mixins/resolvable'; - import tooltip from '../../vue_shared/directives/tooltip'; - import { scrollToElement } from '../../lib/utils/common_utils'; +import { mapActions, mapGetters } from 'vuex'; +import resolveDiscussionsSvg from 'icons/_icon_mr_issue.svg'; +import nextDiscussionsSvg from 'icons/_next_discussion.svg'; +import Flash from '../../flash'; +import { SYSTEM_NOTE } from '../constants'; +import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; +import noteableNote from './noteable_note.vue'; +import noteHeader from './note_header.vue'; +import noteSignedOutWidget from './note_signed_out_widget.vue'; +import noteEditedText from './note_edited_text.vue'; +import noteForm from './note_form.vue'; +import diffWithNote from './diff_with_note.vue'; +import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue'; +import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue'; +import autosave from '../mixins/autosave'; +import noteable from '../mixins/noteable'; +import resolvable from '../mixins/resolvable'; +import tooltip from '../../vue_shared/directives/tooltip'; +import { scrollToElement } from '../../lib/utils/common_utils'; - export default { - components: { - noteableNote, - diffWithNote, - userAvatarLink, - noteHeader, - noteSignedOutWidget, - noteEditedText, - noteForm, - placeholderNote, - placeholderSystemNote, +export default { + components: { + noteableNote, + diffWithNote, + userAvatarLink, + noteHeader, + noteSignedOutWidget, + noteEditedText, + noteForm, + placeholderNote, + placeholderSystemNote, + }, + directives: { + tooltip, + }, + mixins: [autosave, noteable, resolvable], + props: { + note: { + type: Object, + required: true, }, - directives: { - tooltip, - }, - mixins: [ - autosave, - noteable, - resolvable, - ], - props: { - note: { - type: Object, - required: true, - }, - }, - data() { + }, + data() { + return { + isReplying: false, + isResolving: false, + resolveAsThread: true, + }; + }, + computed: { + ...mapGetters([ + 'getNoteableData', + 'discussionCount', + 'resolvedDiscussionCount', + 'unresolvedDiscussions', + ]), + discussion() { return { - isReplying: false, - isResolving: false, - resolveAsThread: true, + ...this.note.notes[0], + truncatedDiffLines: this.note.truncated_diff_lines, + diffFile: this.note.diff_file, + diffDiscussion: this.note.diff_discussion, + imageDiffHtml: this.note.image_diff_html, }; }, - computed: { - ...mapGetters([ - 'getNoteableData', - 'discussionCount', - 'resolvedDiscussionCount', - 'unresolvedDiscussions', - ]), - discussion() { - return { - ...this.note.notes[0], - truncatedDiffLines: this.note.truncated_diff_lines, - diffFile: this.note.diff_file, - diffDiscussion: this.note.diff_discussion, - imageDiffHtml: this.note.image_diff_html, - }; - }, - author() { - return this.discussion.author; - }, - canReply() { - return this.getNoteableData.current_user.can_create_note; - }, - newNotePath() { - return this.getNoteableData.create_note_path; - }, - lastUpdatedBy() { - const { notes } = this.note; + author() { + return this.discussion.author; + }, + canReply() { + return this.getNoteableData.current_user.can_create_note; + }, + newNotePath() { + return this.getNoteableData.create_note_path; + }, + lastUpdatedBy() { + const { notes } = this.note; - if (notes.length > 1) { - return notes[notes.length - 1].author; - } + if (notes.length > 1) { + return notes[notes.length - 1].author; + } - return null; - }, - lastUpdatedAt() { - const { notes } = this.note; + return null; + }, + lastUpdatedAt() { + const { notes } = this.note; - if (notes.length > 1) { - return notes[notes.length - 1].created_at; - } + if (notes.length > 1) { + return notes[notes.length - 1].created_at; + } - return null; - }, - hasUnresolvedDiscussion() { - return this.unresolvedDiscussions.length > 0; - }, - wrapperComponent() { - return (this.discussion.diffDiscussion && this.discussion.diffFile) ? diffWithNote : 'div'; - }, - wrapperClass() { - return this.isDiffDiscussion ? '' : 'panel panel-default'; - }, + return null; + }, + hasUnresolvedDiscussion() { + return this.unresolvedDiscussions.length > 0; + }, + wrapperComponent() { + return this.discussion.diffDiscussion && this.discussion.diffFile + ? diffWithNote + : 'div'; }, - mounted() { - if (this.isReplying) { + wrapperClass() { + return this.isDiffDiscussion ? '' : 'panel panel-default'; + }, + }, + mounted() { + if (this.isReplying) { + this.initAutoSave(this.discussion.noteable_type); + } + }, + updated() { + if (this.isReplying) { + if (!this.autosave) { this.initAutoSave(this.discussion.noteable_type); + } else { + this.setAutoSave(); } - }, - updated() { - if (this.isReplying) { - if (!this.autosave) { - this.initAutoSave(this.discussion.noteable_type); - } else { - this.setAutoSave(); + } + }, + created() { + this.resolveDiscussionsSvg = resolveDiscussionsSvg; + this.nextDiscussionsSvg = nextDiscussionsSvg; + }, + methods: { + ...mapActions([ + 'saveNote', + 'toggleDiscussion', + 'removePlaceholderNotes', + 'toggleResolveNote', + ]), + componentName(note) { + if (note.isPlaceholderNote) { + if (note.placeholderType === SYSTEM_NOTE) { + return placeholderSystemNote; } + return placeholderNote; } + + return noteableNote; }, - created() { - this.resolveDiscussionsSvg = resolveDiscussionsSvg; - this.nextDiscussionsSvg = nextDiscussionsSvg; + componentData(note) { + return note.isPlaceholderNote ? this.note.notes[0] : note; }, - methods: { - ...mapActions([ - 'saveNote', - 'toggleDiscussion', - 'removePlaceholderNotes', - 'toggleResolveNote', - ]), - componentName(note) { - if (note.isPlaceholderNote) { - if (note.placeholderType === SYSTEM_NOTE) { - return placeholderSystemNote; - } - return placeholderNote; - } + toggleDiscussionHandler() { + this.toggleDiscussion({ discussionId: this.note.id }); + }, + showReplyForm() { + this.isReplying = true; + }, + cancelReplyForm(shouldConfirm) { + if (shouldConfirm && this.$refs.noteForm.isDirty) { + const msg = 'Are you sure you want to cancel creating this comment?'; - return noteableNote; - }, - componentData(note) { - return note.isPlaceholderNote ? this.note.notes[0] : note; - }, - toggleDiscussionHandler() { - this.toggleDiscussion({ discussionId: this.note.id }); - }, - showReplyForm() { - this.isReplying = true; - }, - cancelReplyForm(shouldConfirm) { - if (shouldConfirm && this.$refs.noteForm.isDirty) { - // eslint-disable-next-line no-alert - if (!confirm('Are you sure you want to cancel creating this comment?')) { - return; - } + // eslint-disable-next-line no-alert + if (!confirm(msg)) { + return; } + } - this.resetAutoSave(); - this.isReplying = false; - }, - saveReply(noteText, form, callback) { - const replyData = { - endpoint: this.newNotePath, - flashContainer: this.$el, - data: { - in_reply_to_discussion_id: this.note.reply_id, - target_type: this.noteableType, - target_id: this.discussion.noteable_id, - note: { note: noteText }, - }, - }; - this.isReplying = false; + this.resetAutoSave(); + this.isReplying = false; + }, + saveReply(noteText, form, callback) { + const replyData = { + endpoint: this.newNotePath, + flashContainer: this.$el, + data: { + in_reply_to_discussion_id: this.note.reply_id, + target_type: this.noteableType, + target_id: this.discussion.noteable_id, + note: { note: noteText }, + }, + }; + this.isReplying = false; - this.saveNote(replyData) - .then(() => { - this.resetAutoSave(); - callback(); - }) - .catch((err) => { - this.removePlaceholderNotes(); - this.isReplying = true; - this.$nextTick(() => { - const msg = `Your comment could not be submitted! + this.saveNote(replyData) + .then(() => { + this.resetAutoSave(); + callback(); + }) + .catch(err => { + this.removePlaceholderNotes(); + this.isReplying = true; + this.$nextTick(() => { + const msg = `Your comment could not be submitted! Please check your network connection and try again.`; - Flash(msg, 'alert', this.$el); - this.$refs.noteForm.note = noteText; - callback(err); - }); + Flash(msg, 'alert', this.$el); + this.$refs.noteForm.note = noteText; + callback(err); }); - }, - jumpToDiscussion() { - const unresolvedIds = this.unresolvedDiscussions.map(d => d.id); - const index = unresolvedIds.indexOf(this.note.id); + }); + }, + jumpToDiscussion() { + const unresolvedIds = this.unresolvedDiscussions.map(d => d.id); + const index = unresolvedIds.indexOf(this.note.id); - if (index >= 0 && index !== unresolvedIds.length) { - const nextId = unresolvedIds[index + 1]; - const el = document.querySelector(`[data-discussion-id="${nextId}"]`); + if (index >= 0 && index !== unresolvedIds.length) { + const nextId = unresolvedIds[index + 1]; + const el = document.querySelector(`[data-discussion-id="${nextId}"]`); - if (el) { - scrollToElement(el); - } + if (el) { + scrollToElement(el); } - }, + } }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue index 4d17bd5acc2..3554027d2b4 100644 --- a/app/assets/javascripts/notes/components/noteable_note.vue +++ b/app/assets/javascripts/notes/components/noteable_note.vue @@ -1,151 +1,152 @@ <script> - import { mapGetters, mapActions } from 'vuex'; - import { escape } from 'underscore'; - import Flash from '../../flash'; - import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; - import noteHeader from './note_header.vue'; - import noteActions from './note_actions.vue'; - import noteBody from './note_body.vue'; - import eventHub from '../event_hub'; - import noteable from '../mixins/noteable'; - import resolvable from '../mixins/resolvable'; +import $ from 'jquery'; +import { mapGetters, mapActions } from 'vuex'; +import { escape } from 'underscore'; +import Flash from '../../flash'; +import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; +import noteHeader from './note_header.vue'; +import noteActions from './note_actions.vue'; +import noteBody from './note_body.vue'; +import eventHub from '../event_hub'; +import noteable from '../mixins/noteable'; +import resolvable from '../mixins/resolvable'; - export default { - components: { - userAvatarLink, - noteHeader, - noteActions, - noteBody, +export default { + components: { + userAvatarLink, + noteHeader, + noteActions, + noteBody, + }, + mixins: [noteable, resolvable], + props: { + note: { + type: Object, + required: true, }, - mixins: [ - noteable, - resolvable, - ], - props: { - note: { - type: Object, - required: true, - }, + }, + data() { + return { + isEditing: false, + isDeleting: false, + isRequesting: false, + isResolving: false, + }; + }, + computed: { + ...mapGetters(['targetNoteHash', 'getUserData']), + author() { + return this.note.author; }, - data() { + classNameBindings() { return { - isEditing: false, - isDeleting: false, - isRequesting: false, - isResolving: false, + 'is-editing': this.isEditing && !this.isRequesting, + 'is-requesting being-posted': this.isRequesting, + 'disabled-content': this.isDeleting, + target: this.targetNoteHash === this.noteAnchorId, }; }, - computed: { - ...mapGetters([ - 'targetNoteHash', - 'getUserData', - ]), - author() { - return this.note.author; - }, - classNameBindings() { - return { - 'is-editing': this.isEditing && !this.isRequesting, - 'is-requesting being-posted': this.isRequesting, - 'disabled-content': this.isDeleting, - target: this.targetNoteHash === this.noteAnchorId, - }; - }, - canReportAsAbuse() { - return this.note.report_abuse_path && this.author.id !== this.getUserData.id; - }, - noteAnchorId() { - return `note_${this.note.id}`; - }, + canReportAsAbuse() { + return ( + this.note.report_abuse_path && this.author.id !== this.getUserData.id + ); }, - - created() { - eventHub.$on('enterEditMode', ({ noteId }) => { - if (noteId === this.note.id) { - this.isEditing = true; - this.scrollToNoteIfNeeded($(this.$el)); - } - }); + noteAnchorId() { + return `note_${this.note.id}`; }, + }, - methods: { - ...mapActions([ - 'deleteNote', - 'updateNote', - 'toggleResolveNote', - 'scrollToNoteIfNeeded', - ]), - editHandler() { + created() { + eventHub.$on('enterEditMode', ({ noteId }) => { + if (noteId === this.note.id) { this.isEditing = true; - }, - deleteHandler() { - // eslint-disable-next-line no-alert - if (confirm('Are you sure you want to delete this comment?')) { - this.isDeleting = true; + this.scrollToNoteIfNeeded($(this.$el)); + } + }); + }, - this.deleteNote(this.note) - .then(() => { - this.isDeleting = false; - }) - .catch(() => { - Flash('Something went wrong while deleting your note. Please try again.'); - this.isDeleting = false; - }); - } - }, - formUpdateHandler(noteText, parentElement, callback) { - const data = { - endpoint: this.note.path, - note: { - target_type: this.noteableType, - target_id: this.note.noteable_id, - note: { note: noteText }, - }, - }; - this.isRequesting = true; - this.oldContent = this.note.note_html; - this.note.note_html = escape(noteText); + methods: { + ...mapActions([ + 'deleteNote', + 'updateNote', + 'toggleResolveNote', + 'scrollToNoteIfNeeded', + ]), + editHandler() { + this.isEditing = true; + }, + deleteHandler() { + // eslint-disable-next-line no-alert + if (confirm('Are you sure you want to delete this comment?')) { + this.isDeleting = true; - this.updateNote(data) + this.deleteNote(this.note) .then(() => { - this.isEditing = false; - this.isRequesting = false; - this.oldContent = null; - $(this.$refs.noteBody.$el).renderGFM(); - this.$refs.noteBody.resetAutoSave(); - callback(); + this.isDeleting = false; }) .catch(() => { - this.isRequesting = false; - this.isEditing = true; - this.$nextTick(() => { - const msg = 'Something went wrong while editing your comment. Please try again.'; - Flash(msg, 'alert', this.$el); - this.recoverNoteContent(noteText); - callback(); - }); + Flash( + 'Something went wrong while deleting your note. Please try again.', + ); + this.isDeleting = false; }); - }, - formCancelHandler(shouldConfirm, isDirty) { - if (shouldConfirm && isDirty) { - // eslint-disable-next-line no-alert - if (!confirm('Are you sure you want to cancel editing this comment?')) return; - } - this.$refs.noteBody.resetAutoSave(); - if (this.oldContent) { - this.note.note_html = this.oldContent; + } + }, + formUpdateHandler(noteText, parentElement, callback) { + const data = { + endpoint: this.note.path, + note: { + target_type: this.noteableType, + target_id: this.note.noteable_id, + note: { note: noteText }, + }, + }; + this.isRequesting = true; + this.oldContent = this.note.note_html; + this.note.note_html = escape(noteText); + + this.updateNote(data) + .then(() => { + this.isEditing = false; + this.isRequesting = false; this.oldContent = null; - } - this.isEditing = false; - }, - recoverNoteContent(noteText) { - // we need to do this to prevent noteForm inconsistent content warning - // this is something we intentionally do so we need to recover the content - this.note.note = noteText; - this.$refs.noteBody.$refs.noteForm.note.note = noteText; - }, + $(this.$refs.noteBody.$el).renderGFM(); + this.$refs.noteBody.resetAutoSave(); + callback(); + }) + .catch(() => { + this.isRequesting = false; + this.isEditing = true; + this.$nextTick(() => { + const msg = + 'Something went wrong while editing your comment. Please try again.'; + Flash(msg, 'alert', this.$el); + this.recoverNoteContent(noteText); + callback(); + }); + }); + }, + formCancelHandler(shouldConfirm, isDirty) { + if (shouldConfirm && isDirty) { + // eslint-disable-next-line no-alert + if (!confirm('Are you sure you want to cancel editing this comment?')) + return; + } + this.$refs.noteBody.resetAutoSave(); + if (this.oldContent) { + this.note.note_html = this.oldContent; + this.oldContent = null; + } + this.isEditing = false; + }, + recoverNoteContent(noteText) { + // we need to do this to prevent noteForm inconsistent content warning + // this is something we intentionally do so we need to recover the content + this.note.note = noteText; + this.$refs.noteBody.$refs.noteForm.note.note = noteText; }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue index 74afed5560b..a90c6d6381d 100644 --- a/app/assets/javascripts/notes/components/notes_app.vue +++ b/app/assets/javascripts/notes/components/notes_app.vue @@ -1,159 +1,162 @@ <script> - import { mapGetters, mapActions } from 'vuex'; - import { getLocationHash } from '../../lib/utils/url_utility'; - import Flash from '../../flash'; - import store from '../stores/'; - import * as constants from '../constants'; - import noteableNote from './noteable_note.vue'; - import noteableDiscussion from './noteable_discussion.vue'; - import systemNote from '../../vue_shared/components/notes/system_note.vue'; - import commentForm from './comment_form.vue'; - import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue'; - import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue'; - import loadingIcon from '../../vue_shared/components/loading_icon.vue'; - import skeletonLoadingContainer from '../../vue_shared/components/notes/skeleton_note.vue'; +import $ from 'jquery'; +import { mapGetters, mapActions } from 'vuex'; +import { getLocationHash } from '../../lib/utils/url_utility'; +import Flash from '../../flash'; +import store from '../stores/'; +import * as constants from '../constants'; +import noteableNote from './noteable_note.vue'; +import noteableDiscussion from './noteable_discussion.vue'; +import systemNote from '../../vue_shared/components/notes/system_note.vue'; +import commentForm from './comment_form.vue'; +import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue'; +import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue'; +import loadingIcon from '../../vue_shared/components/loading_icon.vue'; +import skeletonLoadingContainer from '../../vue_shared/components/notes/skeleton_note.vue'; - export default { - name: 'NotesApp', - components: { - noteableNote, - noteableDiscussion, - systemNote, - commentForm, - loadingIcon, - placeholderNote, - placeholderSystemNote, +export default { + name: 'NotesApp', + components: { + noteableNote, + noteableDiscussion, + systemNote, + commentForm, + loadingIcon, + placeholderNote, + placeholderSystemNote, + }, + props: { + noteableData: { + type: Object, + required: true, }, - props: { - noteableData: { - type: Object, - required: true, - }, - notesData: { - type: Object, - required: true, - }, - userData: { - type: Object, - required: false, - default: () => ({}), - }, + notesData: { + type: Object, + required: true, }, - store, - data() { - return { - isLoading: true, - }; + userData: { + type: Object, + required: false, + default: () => ({}), }, - computed: { - ...mapGetters([ - 'notes', - 'getNotesDataByProp', - 'discussionCount', - ]), - noteableType() { - // FIXME -- @fatihacet Get this from JSON data. - const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE } = constants; + }, + store, + data() { + return { + isLoading: true, + }; + }, + computed: { + ...mapGetters(['notes', 'getNotesDataByProp', 'discussionCount']), + noteableType() { + // FIXME -- @fatihacet Get this from JSON data. + const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE } = constants; - return this.noteableData.merge_params ? MERGE_REQUEST_NOTEABLE_TYPE : ISSUE_NOTEABLE_TYPE; - }, - allNotes() { - if (this.isLoading) { - const totalNotes = parseInt(this.notesData.totalNotes, 10) || 0; - - return new Array(totalNotes).fill({ - isSkeletonNote: true, - }); - } - return this.notes; - }, - }, - created() { - this.setNotesData(this.notesData); - this.setNoteableData(this.noteableData); - this.setUserData(this.userData); + return this.noteableData.merge_params + ? MERGE_REQUEST_NOTEABLE_TYPE + : ISSUE_NOTEABLE_TYPE; }, - mounted() { - this.fetchNotes(); + allNotes() { + if (this.isLoading) { + const totalNotes = parseInt(this.notesData.totalNotes, 10) || 0; - const parentElement = this.$el.parentElement; - - if (parentElement && - parentElement.classList.contains('js-vue-notes-event')) { - parentElement.addEventListener('toggleAward', (event) => { - const { awardName, noteId } = event.detail; - this.actionToggleAward({ awardName, noteId }); + return new Array(totalNotes).fill({ + isSkeletonNote: true, }); } - document.addEventListener('refreshVueNotes', this.fetchNotes); - }, - beforeDestroy() { - document.removeEventListener('refreshVueNotes', this.fetchNotes); + return this.notes; }, - methods: { - ...mapActions({ - actionFetchNotes: 'fetchNotes', - poll: 'poll', - actionToggleAward: 'toggleAward', - scrollToNoteIfNeeded: 'scrollToNoteIfNeeded', - setNotesData: 'setNotesData', - setNoteableData: 'setNoteableData', - setUserData: 'setUserData', - setLastFetchedAt: 'setLastFetchedAt', - setTargetNoteHash: 'setTargetNoteHash', - }), - getComponentName(note) { - if (note.isSkeletonNote) { - return skeletonLoadingContainer; - } - if (note.isPlaceholderNote) { - if (note.placeholderType === constants.SYSTEM_NOTE) { - return placeholderSystemNote; - } - return placeholderNote; - } else if (note.individual_note) { - return note.notes[0].system ? systemNote : noteableNote; - } + }, + created() { + this.setNotesData(this.notesData); + this.setNoteableData(this.noteableData); + this.setUserData(this.userData); + }, + mounted() { + this.fetchNotes(); + + const parentElement = this.$el.parentElement; - return noteableDiscussion; - }, - getComponentData(note) { - return note.individual_note ? note.notes[0] : note; - }, - fetchNotes() { - return this.actionFetchNotes(this.getNotesDataByProp('discussionsPath')) - .then(() => this.initPolling()) - .then(() => { - this.isLoading = false; - }) - .then(() => this.$nextTick()) - .then(() => this.checkLocationHash()) - .catch(() => { - this.isLoading = false; - Flash('Something went wrong while fetching comments. Please try again.'); - }); - }, - initPolling() { - if (this.isPollingInitialized) { - return; + if ( + parentElement && + parentElement.classList.contains('js-vue-notes-event') + ) { + parentElement.addEventListener('toggleAward', event => { + const { awardName, noteId } = event.detail; + this.actionToggleAward({ awardName, noteId }); + }); + } + document.addEventListener('refreshVueNotes', this.fetchNotes); + }, + beforeDestroy() { + document.removeEventListener('refreshVueNotes', this.fetchNotes); + }, + methods: { + ...mapActions({ + actionFetchNotes: 'fetchNotes', + poll: 'poll', + actionToggleAward: 'toggleAward', + scrollToNoteIfNeeded: 'scrollToNoteIfNeeded', + setNotesData: 'setNotesData', + setNoteableData: 'setNoteableData', + setUserData: 'setUserData', + setLastFetchedAt: 'setLastFetchedAt', + setTargetNoteHash: 'setTargetNoteHash', + }), + getComponentName(note) { + if (note.isSkeletonNote) { + return skeletonLoadingContainer; + } + if (note.isPlaceholderNote) { + if (note.placeholderType === constants.SYSTEM_NOTE) { + return placeholderSystemNote; } + return placeholderNote; + } else if (note.individual_note) { + return note.notes[0].system ? systemNote : noteableNote; + } - this.setLastFetchedAt(this.getNotesDataByProp('lastFetchedAt')); + return noteableDiscussion; + }, + getComponentData(note) { + return note.individual_note ? note.notes[0] : note; + }, + fetchNotes() { + return this.actionFetchNotes(this.getNotesDataByProp('discussionsPath')) + .then(() => this.initPolling()) + .then(() => { + this.isLoading = false; + }) + .then(() => this.$nextTick()) + .then(() => this.checkLocationHash()) + .catch(() => { + this.isLoading = false; + Flash( + 'Something went wrong while fetching comments. Please try again.', + ); + }); + }, + initPolling() { + if (this.isPollingInitialized) { + return; + } - this.poll(); - this.isPollingInitialized = true; - }, - checkLocationHash() { - const hash = getLocationHash(); - const element = document.getElementById(hash); + this.setLastFetchedAt(this.getNotesDataByProp('lastFetchedAt')); - if (hash && element) { - this.setTargetNoteHash(hash); - this.scrollToNoteIfNeeded($(element)); - } - }, + this.poll(); + this.isPollingInitialized = true; + }, + checkLocationHash() { + const hash = getLocationHash(); + const element = document.getElementById(hash); + + if (hash && element) { + this.setTargetNoteHash(hash); + this.scrollToNoteIfNeeded($(element)); + } }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js index 545bf2c99a7..f90775d0157 100644 --- a/app/assets/javascripts/notes/index.js +++ b/app/assets/javascripts/notes/index.js @@ -1,35 +1,43 @@ import Vue from 'vue'; import notesApp from './components/notes_app.vue'; -document.addEventListener('DOMContentLoaded', () => new Vue({ - el: '#js-vue-notes', - components: { - notesApp, - }, - data() { - const notesDataset = document.getElementById('js-vue-notes').dataset; - const parsedUserData = JSON.parse(notesDataset.currentUserData); - const currentUserData = parsedUserData ? { - id: parsedUserData.id, - name: parsedUserData.name, - username: parsedUserData.username, - avatar_url: parsedUserData.avatar_path || parsedUserData.avatar_url, - path: parsedUserData.path, - } : {}; +document.addEventListener( + 'DOMContentLoaded', + () => + new Vue({ + el: '#js-vue-notes', + components: { + notesApp, + }, + data() { + const notesDataset = document.getElementById('js-vue-notes').dataset; + const parsedUserData = JSON.parse(notesDataset.currentUserData); + let currentUserData = {}; + + if (parsedUserData) { + currentUserData = { + id: parsedUserData.id, + name: parsedUserData.name, + username: parsedUserData.username, + avatar_url: parsedUserData.avatar_path || parsedUserData.avatar_url, + path: parsedUserData.path, + }; + } - return { - noteableData: JSON.parse(notesDataset.noteableData), - currentUserData, - notesData: JSON.parse(notesDataset.notesData), - }; - }, - render(createElement) { - return createElement('notes-app', { - props: { - noteableData: this.noteableData, - notesData: this.notesData, - userData: this.currentUserData, + return { + noteableData: JSON.parse(notesDataset.noteableData), + currentUserData, + notesData: JSON.parse(notesDataset.notesData), + }; + }, + render(createElement) { + return createElement('notes-app', { + props: { + noteableData: this.noteableData, + notesData: this.notesData, + userData: this.currentUserData, + }, + }); }, - }); - }, -})); + }), +); diff --git a/app/assets/javascripts/notes/mixins/autosave.js b/app/assets/javascripts/notes/mixins/autosave.js index a3d897f2f12..3dff715905f 100644 --- a/app/assets/javascripts/notes/mixins/autosave.js +++ b/app/assets/javascripts/notes/mixins/autosave.js @@ -1,10 +1,15 @@ +import $ from 'jquery'; import Autosave from '../../autosave'; import { capitalizeFirstCharacter } from '../../lib/utils/text_utility'; export default { methods: { initAutoSave(noteableType) { - this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), ['Note', capitalizeFirstCharacter(noteableType), this.note.id]); + this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), [ + 'Note', + capitalizeFirstCharacter(noteableType), + this.note.id, + ]); }, resetAutoSave() { this.autosave.reset(); diff --git a/app/assets/javascripts/notes/mixins/resolvable.js b/app/assets/javascripts/notes/mixins/resolvable.js index ab1ae115e52..f79049b85f6 100644 --- a/app/assets/javascripts/notes/mixins/resolvable.js +++ b/app/assets/javascripts/notes/mixins/resolvable.js @@ -12,7 +12,8 @@ export default { discussionResolved() { const { notes, resolved } = this.note; - if (notes) { // Decide resolved state using store. Only valid for discussions. + if (notes) { + // Decide resolved state using store. Only valid for discussions. return notes.every(note => note.resolved && !note.system); } @@ -26,7 +27,9 @@ export default { return __('Comment and resolve discussion'); } - return this.discussionResolved ? __('Unresolve discussion') : __('Resolve discussion'); + return this.discussionResolved + ? __('Unresolve discussion') + : __('Resolve discussion'); }, }, methods: { @@ -42,7 +45,9 @@ export default { }) .catch(() => { this.isResolving = false; - const msg = __('Something went wrong while resolving this discussion. Please try again.'); + const msg = __( + 'Something went wrong while resolving this discussion. Please try again.', + ); Flash(msg, 'alert', this.$el); }); }, diff --git a/app/assets/javascripts/notes/services/notes_service.js b/app/assets/javascripts/notes/services/notes_service.js index 4766351dfc5..7c623aac6ed 100644 --- a/app/assets/javascripts/notes/services/notes_service.js +++ b/app/assets/javascripts/notes/services/notes_service.js @@ -22,15 +22,18 @@ export default { }, toggleResolveNote(endpoint, isResolved) { const { RESOLVE_NOTE_METHOD_NAME, UNRESOLVE_NOTE_METHOD_NAME } = constants; - const method = isResolved ? UNRESOLVE_NOTE_METHOD_NAME : RESOLVE_NOTE_METHOD_NAME; + const method = isResolved + ? UNRESOLVE_NOTE_METHOD_NAME + : RESOLVE_NOTE_METHOD_NAME; return Vue.http[method](endpoint); }, poll(data = {}) { - const { endpoint, lastFetchedAt } = data; + const endpoint = data.notesData.notesPath; + const lastFetchedAt = data.lastFetchedAt; const options = { headers: { - 'X-Last-Fetched-At': lastFetchedAt, + 'X-Last-Fetched-At': lastFetchedAt ? `${lastFetchedAt}` : undefined, }, }; diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index 42fc2a131b8..244a6980b5a 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import Visibility from 'visibilityjs'; import Flash from '../../flash'; import Poll from '../../lib/utils/poll'; @@ -11,86 +12,115 @@ import { isInViewport, scrollToElement } from '../../lib/utils/common_utils'; let eTagPoll; -export const setNotesData = ({ commit }, data) => commit(types.SET_NOTES_DATA, data); -export const setNoteableData = ({ commit }, data) => commit(types.SET_NOTEABLE_DATA, data); -export const setUserData = ({ commit }, data) => commit(types.SET_USER_DATA, data); -export const setLastFetchedAt = ({ commit }, data) => commit(types.SET_LAST_FETCHED_AT, data); -export const setInitialNotes = ({ commit }, data) => commit(types.SET_INITIAL_NOTES, data); -export const setTargetNoteHash = ({ commit }, data) => commit(types.SET_TARGET_NOTE_HASH, data); -export const toggleDiscussion = ({ commit }, data) => commit(types.TOGGLE_DISCUSSION, data); - -export const fetchNotes = ({ commit }, path) => service - .fetchNotes(path) - .then(res => res.json()) - .then((res) => { - commit(types.SET_INITIAL_NOTES, res); - }); +export const setNotesData = ({ commit }, data) => + commit(types.SET_NOTES_DATA, data); +export const setNoteableData = ({ commit }, data) => + commit(types.SET_NOTEABLE_DATA, data); +export const setUserData = ({ commit }, data) => + commit(types.SET_USER_DATA, data); +export const setLastFetchedAt = ({ commit }, data) => + commit(types.SET_LAST_FETCHED_AT, data); +export const setInitialNotes = ({ commit }, data) => + commit(types.SET_INITIAL_NOTES, data); +export const setTargetNoteHash = ({ commit }, data) => + commit(types.SET_TARGET_NOTE_HASH, data); +export const toggleDiscussion = ({ commit }, data) => + commit(types.TOGGLE_DISCUSSION, data); + +export const fetchNotes = ({ commit }, path) => + service + .fetchNotes(path) + .then(res => res.json()) + .then(res => { + commit(types.SET_INITIAL_NOTES, res); + }); -export const deleteNote = ({ commit }, note) => service - .deleteNote(note.path) - .then(() => { +export const deleteNote = ({ commit }, note) => + service.deleteNote(note.path).then(() => { commit(types.DELETE_NOTE, note); }); -export const updateNote = ({ commit }, { endpoint, note }) => service - .updateNote(endpoint, note) - .then(res => res.json()) - .then((res) => { - commit(types.UPDATE_NOTE, res); - }); +export const updateNote = ({ commit }, { endpoint, note }) => + service + .updateNote(endpoint, note) + .then(res => res.json()) + .then(res => { + commit(types.UPDATE_NOTE, res); + }); -export const replyToDiscussion = ({ commit }, { endpoint, data }) => service - .replyToDiscussion(endpoint, data) - .then(res => res.json()) - .then((res) => { - commit(types.ADD_NEW_REPLY_TO_DISCUSSION, res); +export const replyToDiscussion = ({ commit }, { endpoint, data }) => + service + .replyToDiscussion(endpoint, data) + .then(res => res.json()) + .then(res => { + commit(types.ADD_NEW_REPLY_TO_DISCUSSION, res); - return res; - }); + return res; + }); -export const createNewNote = ({ commit }, { endpoint, data }) => service - .createNewNote(endpoint, data) - .then(res => res.json()) - .then((res) => { - if (!res.errors) { - commit(types.ADD_NEW_NOTE, res); - } - return res; - }); +export const createNewNote = ({ commit }, { endpoint, data }) => + service + .createNewNote(endpoint, data) + .then(res => res.json()) + .then(res => { + if (!res.errors) { + commit(types.ADD_NEW_NOTE, res); + } + return res; + }); export const removePlaceholderNotes = ({ commit }) => commit(types.REMOVE_PLACEHOLDER_NOTES); -export const toggleResolveNote = ({ commit }, { endpoint, isResolved, discussion }) => service - .toggleResolveNote(endpoint, isResolved) - .then(res => res.json()) - .then((res) => { - const mutationType = discussion ? types.UPDATE_DISCUSSION : types.UPDATE_NOTE; +export const toggleResolveNote = ( + { commit }, + { endpoint, isResolved, discussion }, +) => + service + .toggleResolveNote(endpoint, isResolved) + .then(res => res.json()) + .then(res => { + const mutationType = discussion + ? types.UPDATE_DISCUSSION + : types.UPDATE_NOTE; - commit(mutationType, res); - }); + commit(mutationType, res); + }); -export const closeIssue = ({ commit, dispatch, state }) => service - .toggleIssueState(state.notesData.closePath) - .then(res => res.json()) - .then((data) => { - commit(types.CLOSE_ISSUE); - dispatch('emitStateChangedEvent', data); - }); +export const closeIssue = ({ commit, dispatch, state }) => { + dispatch('toggleStateButtonLoading', true); + return service + .toggleIssueState(state.notesData.closePath) + .then(res => res.json()) + .then(data => { + commit(types.CLOSE_ISSUE); + dispatch('emitStateChangedEvent', data); + dispatch('toggleStateButtonLoading', false); + }); +}; -export const reopenIssue = ({ commit, dispatch, state }) => service - .toggleIssueState(state.notesData.reopenPath) - .then(res => res.json()) - .then((data) => { - commit(types.REOPEN_ISSUE); - dispatch('emitStateChangedEvent', data); - }); +export const reopenIssue = ({ commit, dispatch, state }) => { + dispatch('toggleStateButtonLoading', true); + return service + .toggleIssueState(state.notesData.reopenPath) + .then(res => res.json()) + .then(data => { + commit(types.REOPEN_ISSUE); + dispatch('emitStateChangedEvent', data); + dispatch('toggleStateButtonLoading', false); + }); +}; + +export const toggleStateButtonLoading = ({ commit }, value) => + commit(types.TOGGLE_STATE_BUTTON_LOADING, value); export const emitStateChangedEvent = ({ commit, getters }, data) => { - const event = new CustomEvent('issuable_vue_app:change', { detail: { - data, - isClosed: getters.openState === constants.CLOSED, - } }); + const event = new CustomEvent('issuable_vue_app:change', { + detail: { + data, + isClosed: getters.openState === constants.CLOSED, + }, + }); document.dispatchEvent(event); }; @@ -132,59 +162,70 @@ export const saveNote = ({ commit, dispatch }, noteData) => { }); } - return dispatch(methodToDispatch, noteData) - .then((res) => { - const { errors } = res; - const commandsChanges = res.commands_changes; - - if (hasQuickActions && errors && Object.keys(errors).length) { - eTagPoll.makeRequest(); + return dispatch(methodToDispatch, noteData).then(res => { + const { errors } = res; + const commandsChanges = res.commands_changes; - $('.js-gfm-input').trigger('clear-commands-cache.atwho'); - Flash('Commands applied', 'notice', noteData.flashContainer); - } + if (hasQuickActions && errors && Object.keys(errors).length) { + eTagPoll.makeRequest(); - if (commandsChanges) { - if (commandsChanges.emoji_award) { - const votesBlock = $('.js-awards-block').eq(0); - - loadAwardsHandler() - .then((awardsHandler) => { - awardsHandler.addAwardToEmojiBar(votesBlock, commandsChanges.emoji_award); - awardsHandler.scrollToAwards(); - }) - .catch(() => { - Flash( - 'Something went wrong while adding your award. Please try again.', - 'alert', - noteData.flashContainer, - ); - }); - } + $('.js-gfm-input').trigger('clear-commands-cache.atwho'); + Flash('Commands applied', 'notice', noteData.flashContainer); + } - if (commandsChanges.spend_time != null || commandsChanges.time_estimate != null) { - sidebarTimeTrackingEventHub.$emit('timeTrackingUpdated', res); - } + if (commandsChanges) { + if (commandsChanges.emoji_award) { + const votesBlock = $('.js-awards-block').eq(0); + + loadAwardsHandler() + .then(awardsHandler => { + awardsHandler.addAwardToEmojiBar( + votesBlock, + commandsChanges.emoji_award, + ); + awardsHandler.scrollToAwards(); + }) + .catch(() => { + Flash( + 'Something went wrong while adding your award. Please try again.', + 'alert', + noteData.flashContainer, + ); + }); } - if (errors && errors.commands_only) { - Flash(errors.commands_only, 'notice', noteData.flashContainer); + if ( + commandsChanges.spend_time != null || + commandsChanges.time_estimate != null + ) { + sidebarTimeTrackingEventHub.$emit('timeTrackingUpdated', res); } - commit(types.REMOVE_PLACEHOLDER_NOTES); + } - return res; - }); + if (errors && errors.commands_only) { + Flash(errors.commands_only, 'notice', noteData.flashContainer); + } + commit(types.REMOVE_PLACEHOLDER_NOTES); + + return res; + }); }; const pollSuccessCallBack = (resp, commit, state, getters) => { if (resp.notes && resp.notes.length) { const { notesById } = getters; - resp.notes.forEach((note) => { + resp.notes.forEach(note => { if (notesById[note.id]) { commit(types.UPDATE_NOTE, note); - } else if (note.type === constants.DISCUSSION_NOTE || note.type === constants.DIFF_NOTE) { - const discussion = utils.findNoteObjectById(state.notes, note.discussion_id); + } else if ( + note.type === constants.DISCUSSION_NOTE || + note.type === constants.DIFF_NOTE + ) { + const discussion = utils.findNoteObjectById( + state.notes, + note.discussion_id, + ); if (discussion) { commit(types.ADD_NEW_REPLY_TO_DISCUSSION, note); @@ -197,27 +238,28 @@ const pollSuccessCallBack = (resp, commit, state, getters) => { }); } - commit(types.SET_LAST_FETCHED_AT, resp.lastFetchedAt); + commit(types.SET_LAST_FETCHED_AT, resp.last_fetched_at); return resp; }; export const poll = ({ commit, state, getters }) => { - const requestData = { endpoint: state.notesData.notesPath, lastFetchedAt: state.lastFetchedAt }; - eTagPoll = new Poll({ resource: service, method: 'poll', - data: requestData, - successCallback: resp => resp.json() - .then(data => pollSuccessCallBack(data, commit, state, getters)), - errorCallback: () => Flash('Something went wrong while fetching latest comments.'), + data: state, + successCallback: resp => + resp + .json() + .then(data => pollSuccessCallBack(data, commit, state, getters)), + errorCallback: () => + Flash('Something went wrong while fetching latest comments.'), }); if (!Visibility.hidden()) { eTagPoll.makeRequest(); } else { - service.poll(requestData); + service.poll(state); } Visibility.change(() => { @@ -238,15 +280,22 @@ export const restartPolling = () => { }; export const fetchData = ({ commit, state, getters }) => { - const requestData = { endpoint: state.notesData.notesPath, lastFetchedAt: state.lastFetchedAt }; + const requestData = { + endpoint: state.notesData.notesPath, + lastFetchedAt: state.lastFetchedAt, + }; - service.poll(requestData) + service + .poll(requestData) .then(resp => resp.json) .then(data => pollSuccessCallBack(data, commit, state, getters)) .catch(() => Flash('Something went wrong while fetching latest comments.')); }; -export const toggleAward = ({ commit, state, getters, dispatch }, { awardName, noteId }) => { +export const toggleAward = ( + { commit, state, getters, dispatch }, + { awardName, noteId }, +) => { commit(types.TOGGLE_AWARD, { awardName, note: getters.notesById[noteId] }); }; diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js index e6180101c58..f89591a54d6 100644 --- a/app/assets/javascripts/notes/stores/getters.js +++ b/app/assets/javascripts/notes/stores/getters.js @@ -11,27 +11,31 @@ export const getNoteableDataByProp = state => prop => state.noteableData[prop]; export const openState = state => state.noteableData.state; export const getUserData = state => state.userData || {}; -export const getUserDataByProp = state => prop => state.userData && state.userData[prop]; +export const getUserDataByProp = state => prop => + state.userData && state.userData[prop]; -export const notesById = state => state.notes.reduce((acc, note) => { - note.notes.every(n => Object.assign(acc, { [n.id]: n })); - return acc; -}, {}); +export const notesById = state => + state.notes.reduce((acc, note) => { + note.notes.every(n => Object.assign(acc, { [n.id]: n })); + return acc; + }, {}); const reverseNotes = array => array.slice(0).reverse(); -const isLastNote = (note, state) => !note.system && - state.userData && note.author && +const isLastNote = (note, state) => + !note.system && + state.userData && + note.author && note.author.id === state.userData.id; -export const getCurrentUserLastNote = state => _.flatten( - reverseNotes(state.notes) - .map(note => reverseNotes(note.notes)), +export const getCurrentUserLastNote = state => + _.flatten( + reverseNotes(state.notes).map(note => reverseNotes(note.notes)), ).find(el => isLastNote(el, state)); -export const getDiscussionLastNote = state => discussion => reverseNotes(discussion.notes) - .find(el => isLastNote(el, state)); +export const getDiscussionLastNote = state => discussion => + reverseNotes(discussion.notes).find(el => isLastNote(el, state)); -export const discussionCount = (state) => { +export const discussionCount = state => { const discussions = state.notes.filter(n => !n.individual_note); return discussions.length; @@ -43,10 +47,10 @@ export const unresolvedDiscussions = (state, getters) => { return state.notes.filter(n => !n.individual_note && !resolvedMap[n.id]); }; -export const resolvedDiscussionsById = (state) => { +export const resolvedDiscussionsById = state => { const map = {}; - state.notes.forEach((n) => { + state.notes.forEach(n => { if (n.notes) { const resolved = n.notes.every(note => note.resolved && !note.system); diff --git a/app/assets/javascripts/notes/stores/index.js b/app/assets/javascripts/notes/stores/index.js index 488a9ca38d3..9ed19bf171e 100644 --- a/app/assets/javascripts/notes/stores/index.js +++ b/app/assets/javascripts/notes/stores/index.js @@ -12,6 +12,9 @@ export default new Vuex.Store({ targetNoteHash: null, lastFetchedAt: null, + // View layer + isToggleStateButtonLoading: false, + // holds endpoints and permissions provided through haml notesData: {}, userData: {}, diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js index da1b5a9e51a..b455e23ecde 100644 --- a/app/assets/javascripts/notes/stores/mutation_types.js +++ b/app/assets/javascripts/notes/stores/mutation_types.js @@ -17,3 +17,4 @@ export const UPDATE_DISCUSSION = 'UPDATE_DISCUSSION'; // Issue export const CLOSE_ISSUE = 'CLOSE_ISSUE'; export const REOPEN_ISSUE = 'REOPEN_ISSUE'; +export const TOGGLE_STATE_BUTTON_LOADING = 'TOGGLE_STATE_BUTTON_LOADING'; diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js index 963b40be3fd..c8edc06349f 100644 --- a/app/assets/javascripts/notes/stores/mutations.js +++ b/app/assets/javascripts/notes/stores/mutations.js @@ -7,7 +7,7 @@ export default { [types.ADD_NEW_NOTE](state, note) { const { discussion_id, type } = note; const [exists] = state.notes.filter(n => n.id === note.discussion_id); - const isDiscussion = (type === constants.DISCUSSION_NOTE); + const isDiscussion = type === constants.DISCUSSION_NOTE; if (!exists) { const noteData = { @@ -63,13 +63,15 @@ export default { const note = notes[i]; const children = note.notes; - if (children.length && !note.individual_note) { // remove placeholder from discussions + if (children.length && !note.individual_note) { + // remove placeholder from discussions for (let j = children.length - 1; j >= 0; j -= 1) { if (children[j].isPlaceholderNote) { children.splice(j, 1); } } - } else if (note.isPlaceholderNote) { // remove placeholders from state root + } else if (note.isPlaceholderNote) { + // remove placeholders from state root notes.splice(i, 1); } } @@ -89,20 +91,22 @@ export default { [types.SET_INITIAL_NOTES](state, notesData) { const notes = []; - notesData.forEach((note) => { - const nn = Object.assign({}, note); - + notesData.forEach(note => { // To support legacy notes, should be very rare case. if (note.individual_note && note.notes.length > 1) { - note.notes.forEach((n) => { - nn.notes = [n]; // override notes array to only have one item to mimick individual_note - notes.push(nn); + note.notes.forEach(n => { + notes.push({ + ...note, + notes: [n], // override notes array to only have one item to mimick individual_note + }); }); } else { const oldNote = utils.findNoteObjectById(state.notes, note.id); - nn.expanded = oldNote ? oldNote.expanded : note.expanded; - notes.push(nn); + notes.push({ + ...note, + expanded: oldNote ? oldNote.expanded : note.expanded, + }); } }); @@ -126,7 +130,9 @@ export default { notesArr.push({ individual_note: true, isPlaceholderNote: true, - placeholderType: data.isSystemNote ? constants.SYSTEM_NOTE : constants.NOTE, + placeholderType: data.isSystemNote + ? constants.SYSTEM_NOTE + : constants.NOTE, notes: [ { body: data.noteBody, @@ -139,12 +145,16 @@ export default { const { awardName, note } = data; const { id, name, username } = state.userData; - const hasEmojiAwardedByCurrentUser = note.award_emoji - .filter(emoji => emoji.name === data.awardName && emoji.user.id === id); + const hasEmojiAwardedByCurrentUser = note.award_emoji.filter( + emoji => emoji.name === data.awardName && emoji.user.id === id, + ); if (hasEmojiAwardedByCurrentUser.length) { // If current user has awarded this emoji, remove it. - note.award_emoji.splice(note.award_emoji.indexOf(hasEmojiAwardedByCurrentUser[0]), 1); + note.award_emoji.splice( + note.award_emoji.indexOf(hasEmojiAwardedByCurrentUser[0]), + 1, + ); } else { note.award_emoji.push({ name: awardName, @@ -197,4 +207,8 @@ export default { [types.REOPEN_ISSUE](state) { Object.assign(state.noteableData, { state: constants.REOPENED }); }, + + [types.TOGGLE_STATE_BUTTON_LOADING](state, value) { + Object.assign(state, { isToggleStateButtonLoading: value }); + }, }; diff --git a/app/assets/javascripts/notes/stores/utils.js b/app/assets/javascripts/notes/stores/utils.js index 275263a2aaa..a0e096ebfaf 100644 --- a/app/assets/javascripts/notes/stores/utils.js +++ b/app/assets/javascripts/notes/stores/utils.js @@ -2,13 +2,15 @@ import AjaxCache from '~/lib/utils/ajax_cache'; const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm; -export const findNoteObjectById = (notes, id) => notes.filter(n => n.id === id)[0]; +export const findNoteObjectById = (notes, id) => + notes.filter(n => n.id === id)[0]; -export const getQuickActionText = (note) => { +export const getQuickActionText = note => { let text = 'Applying command'; - const quickActions = AjaxCache.get(gl.GfmAutoComplete.dataSources.commands) || []; + const quickActions = + AjaxCache.get(gl.GfmAutoComplete.dataSources.commands) || []; - const executedCommands = quickActions.filter((command) => { + const executedCommands = quickActions.filter(command => { const commandRegex = new RegExp(`/${command.name}`); return commandRegex.test(note); }); @@ -27,4 +29,5 @@ export const getQuickActionText = (note) => { export const hasQuickActions = note => REGEX_QUICK_ACTIONS.test(note); -export const stripQuickActions = note => note.replace(REGEX_QUICK_ACTIONS, '').trim(); +export const stripQuickActions = note => + note.replace(REGEX_QUICK_ACTIONS, '').trim(); diff --git a/app/assets/javascripts/notifications_dropdown.js b/app/assets/javascripts/notifications_dropdown.js index 479a512ed65..8ff8bb446ad 100644 --- a/app/assets/javascripts/notifications_dropdown.js +++ b/app/assets/javascripts/notifications_dropdown.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import Flash from './flash'; export default function notificationsDropdown() { diff --git a/app/assets/javascripts/notifications_form.js b/app/assets/javascripts/notifications_form.js index 4e0afe13590..9e6cf67dff0 100644 --- a/app/assets/javascripts/notifications_form.js +++ b/app/assets/javascripts/notifications_form.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import { __ } from './locale'; import axios from './lib/utils/axios_utils'; import flash from './flash'; diff --git a/app/assets/javascripts/pager.js b/app/assets/javascripts/pager.js index 7e85bce0d73..86a43b66cc8 100644 --- a/app/assets/javascripts/pager.js +++ b/app/assets/javascripts/pager.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import { getParameterByName } from '~/lib/utils/common_utils'; import axios from './lib/utils/axios_utils'; import { removeParams } from './lib/utils/url_utility'; diff --git a/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js b/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js index 66702ec4ca0..15e737fff05 100644 --- a/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js +++ b/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import { truncate } from '../../../lib/utils/text_utility'; const MAX_MESSAGE_LENGTH = 500; diff --git a/app/assets/javascripts/pages/admin/admin.js b/app/assets/javascripts/pages/admin/admin.js index 45e05f111a7..91f154b7ecd 100644 --- a/app/assets/javascripts/pages/admin/admin.js +++ b/app/assets/javascripts/pages/admin/admin.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import { refreshCurrentPage } from '../../lib/utils/url_utility'; function showBlacklistType() { diff --git a/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js b/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js index f92450cbaa7..e7ceccb6f47 100644 --- a/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js +++ b/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import _ from 'underscore'; import axios from '~/lib/utils/axios_utils'; import flash from '~/flash'; diff --git a/app/assets/javascripts/pages/admin/projects/index/index.js b/app/assets/javascripts/pages/admin/projects/index/index.js index 3c597a1093e..ddbefec87b6 100644 --- a/app/assets/javascripts/pages/admin/projects/index/index.js +++ b/app/assets/javascripts/pages/admin/projects/index/index.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import Vue from 'vue'; import Translate from '~/vue_shared/translate'; diff --git a/app/assets/javascripts/pages/admin/users/index.js b/app/assets/javascripts/pages/admin/users/index.js index 4f5d6b55031..06599c3fd5f 100644 --- a/app/assets/javascripts/pages/admin/users/index.js +++ b/app/assets/javascripts/pages/admin/users/index.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import Vue from 'vue'; import Translate from '~/vue_shared/translate'; diff --git a/app/assets/javascripts/pages/ci/lints/create/index.js b/app/assets/javascripts/pages/ci/lints/new/index.js index 8e8a843da0b..8e8a843da0b 100644 --- a/app/assets/javascripts/pages/ci/lints/create/index.js +++ b/app/assets/javascripts/pages/ci/lints/new/index.js diff --git a/app/assets/javascripts/pages/dashboard/todos/index/todos.js b/app/assets/javascripts/pages/dashboard/todos/index/todos.js index 42f7460ad55..c334eaa90f8 100644 --- a/app/assets/javascripts/pages/dashboard/todos/index/todos.js +++ b/app/assets/javascripts/pages/dashboard/todos/index/todos.js @@ -1,4 +1,6 @@ /* eslint-disable class-methods-use-this, no-unneeded-ternary, quote-props */ + +import $ from 'jquery'; import { visitUrl } from '~/lib/utils/url_utility'; import UsersSelect from '~/users_select'; import { isMetaClick } from '~/lib/utils/common_utils'; diff --git a/app/assets/javascripts/pages/groups/boards/index.js b/app/assets/javascripts/pages/groups/boards/index.js new file mode 100644 index 00000000000..5cfe8723204 --- /dev/null +++ b/app/assets/javascripts/pages/groups/boards/index.js @@ -0,0 +1,9 @@ +import UsersSelect from '~/users_select'; +import ShortcutsNavigation from '~/shortcuts_navigation'; +import initBoards from '~/boards'; + +document.addEventListener('DOMContentLoaded', () => { + new UsersSelect(); // eslint-disable-line no-new + new ShortcutsNavigation(); // eslint-disable-line no-new + initBoards(); +}); diff --git a/app/assets/javascripts/pages/groups/edit/index.js b/app/assets/javascripts/pages/groups/edit/index.js index d44874c8741..bb91ac84ffb 100644 --- a/app/assets/javascripts/pages/groups/edit/index.js +++ b/app/assets/javascripts/pages/groups/edit/index.js @@ -1,7 +1,9 @@ import groupAvatar from '~/group_avatar'; import TransferDropdown from '~/groups/transfer_dropdown'; +import initConfirmDangerModal from '~/confirm_danger_modal'; document.addEventListener('DOMContentLoaded', () => { groupAvatar(); new TransferDropdown(); // eslint-disable-line no-new + initConfirmDangerModal(); }); diff --git a/app/assets/javascripts/pages/help/index/index.js b/app/assets/javascripts/pages/help/index/index.js index 05c81fc618b..1bafe564a37 100644 --- a/app/assets/javascripts/pages/help/index/index.js +++ b/app/assets/javascripts/pages/help/index/index.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import VersionCheckImage from '~/version_check_image'; import docs from '~/docs/docs_bundle'; diff --git a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue new file mode 100644 index 00000000000..22248418c41 --- /dev/null +++ b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue @@ -0,0 +1,64 @@ +<script> + import axios from '~/lib/utils/axios_utils'; + import createFlash from '~/flash'; + import GlModal from '~/vue_shared/components/gl_modal.vue'; + import { s__, sprintf } from '~/locale'; + import { visitUrl } from '~/lib/utils/url_utility'; + import eventHub from '../event_hub'; + + export default { + components: { + GlModal, + }, + props: { + milestoneTitle: { + type: String, + required: true, + }, + url: { + type: String, + required: true, + }, + }, + computed: { + title() { + return sprintf(s__('Milestones|Promote %{milestoneTitle} to group milestone?'), { milestoneTitle: this.milestoneTitle }); + }, + text() { + return s__(`Milestones|Promoting this milestone will make it available for all projects inside the group. + Existing project milestones with the same title will be merged. + This action cannot be reversed.`); + }, + }, + methods: { + onSubmit() { + eventHub.$emit('promoteMilestoneModal.requestStarted', this.url); + return axios.post(this.url, { params: { format: 'json' } }) + .then((response) => { + eventHub.$emit('promoteMilestoneModal.requestFinished', { milestoneUrl: this.url, successful: true }); + visitUrl(response.data.url); + }) + .catch((error) => { + eventHub.$emit('promoteMilestoneModal.requestFinished', { milestoneUrl: this.url, successful: false }); + createFlash(error); + }); + }, + }, + }; +</script> +<template> + <gl-modal + id="promote-milestone-modal" + footer-primary-button-variant="warning" + :footer-primary-button-text="s__('Milestones|Promote Milestone')" + @submit="onSubmit" + > + <template + slot="title" + > + {{ title }} + </template> + {{ text }} + </gl-modal> +</template> + diff --git a/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js b/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js new file mode 100644 index 00000000000..d51b5c221e3 --- /dev/null +++ b/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js @@ -0,0 +1,84 @@ +import Vue from 'vue'; +import Translate from '~/vue_shared/translate'; +import deleteMilestoneModal from './components/delete_milestone_modal.vue'; +import eventHub from './event_hub'; + +export default () => { + Vue.use(Translate); + + const onRequestFinished = ({ milestoneUrl, successful }) => { + const button = document.querySelector(`.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`); + + if (!successful) { + button.removeAttribute('disabled'); + } + + button.querySelector('.js-loading-icon').classList.add('hidden'); + }; + + const onRequestStarted = (milestoneUrl) => { + const button = document.querySelector(`.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`); + button.setAttribute('disabled', ''); + button.querySelector('.js-loading-icon').classList.remove('hidden'); + eventHub.$once('deleteMilestoneModal.requestFinished', onRequestFinished); + }; + + const onDeleteButtonClick = (event) => { + const button = event.currentTarget; + const modalProps = { + milestoneId: parseInt(button.dataset.milestoneId, 10), + milestoneTitle: button.dataset.milestoneTitle, + milestoneUrl: button.dataset.milestoneUrl, + issueCount: parseInt(button.dataset.milestoneIssueCount, 10), + mergeRequestCount: parseInt(button.dataset.milestoneMergeRequestCount, 10), + }; + eventHub.$once('deleteMilestoneModal.requestStarted', onRequestStarted); + eventHub.$emit('deleteMilestoneModal.props', modalProps); + }; + + const deleteMilestoneButtons = document.querySelectorAll('.js-delete-milestone-button'); + deleteMilestoneButtons.forEach((button) => { + button.addEventListener('click', onDeleteButtonClick); + }); + + eventHub.$once('deleteMilestoneModal.mounted', () => { + deleteMilestoneButtons.forEach((button) => { + button.removeAttribute('disabled'); + }); + }); + + return new Vue({ + el: '#delete-milestone-modal', + components: { + deleteMilestoneModal, + }, + data() { + return { + modalProps: { + milestoneId: -1, + milestoneTitle: '', + milestoneUrl: '', + issueCount: -1, + mergeRequestCount: -1, + }, + }; + }, + mounted() { + eventHub.$on('deleteMilestoneModal.props', this.setModalProps); + eventHub.$emit('deleteMilestoneModal.mounted'); + }, + beforeDestroy() { + eventHub.$off('deleteMilestoneModal.props', this.setModalProps); + }, + methods: { + setModalProps(modalProps) { + this.modalProps = modalProps; + }, + }, + render(createElement) { + return createElement(deleteMilestoneModal, { + props: this.modalProps, + }); + }, + }); +}; diff --git a/app/assets/javascripts/pages/milestones/shared/index.js b/app/assets/javascripts/pages/milestones/shared/index.js index 327e2cf569c..dabfe32848b 100644 --- a/app/assets/javascripts/pages/milestones/shared/index.js +++ b/app/assets/javascripts/pages/milestones/shared/index.js @@ -1,88 +1,7 @@ -import Vue from 'vue'; - -import Translate from '~/vue_shared/translate'; - -import deleteMilestoneModal from './components/delete_milestone_modal.vue'; -import eventHub from './event_hub'; +import initDeleteMilestoneModal from './delete_milestone_modal_init'; +import initPromoteMilestoneModal from './promote_milestone_modal_init'; export default () => { - Vue.use(Translate); - - const onRequestFinished = ({ milestoneUrl, successful }) => { - const button = document.querySelector(`.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`); - - if (!successful) { - button.removeAttribute('disabled'); - } - - button.querySelector('.js-loading-icon').classList.add('hidden'); - }; - - const onRequestStarted = (milestoneUrl) => { - const button = document.querySelector(`.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`); - button.setAttribute('disabled', ''); - button.querySelector('.js-loading-icon').classList.remove('hidden'); - eventHub.$once('deleteMilestoneModal.requestFinished', onRequestFinished); - }; - - const onDeleteButtonClick = (event) => { - const button = event.currentTarget; - const modalProps = { - milestoneId: parseInt(button.dataset.milestoneId, 10), - milestoneTitle: button.dataset.milestoneTitle, - milestoneUrl: button.dataset.milestoneUrl, - issueCount: parseInt(button.dataset.milestoneIssueCount, 10), - mergeRequestCount: parseInt(button.dataset.milestoneMergeRequestCount, 10), - }; - eventHub.$once('deleteMilestoneModal.requestStarted', onRequestStarted); - eventHub.$emit('deleteMilestoneModal.props', modalProps); - }; - - const deleteMilestoneButtons = document.querySelectorAll('.js-delete-milestone-button'); - for (let i = 0; i < deleteMilestoneButtons.length; i += 1) { - const button = deleteMilestoneButtons[i]; - button.addEventListener('click', onDeleteButtonClick); - } - - eventHub.$once('deleteMilestoneModal.mounted', () => { - for (let i = 0; i < deleteMilestoneButtons.length; i += 1) { - const button = deleteMilestoneButtons[i]; - button.removeAttribute('disabled'); - } - }); - - return new Vue({ - el: '#delete-milestone-modal', - components: { - deleteMilestoneModal, - }, - data() { - return { - modalProps: { - milestoneId: -1, - milestoneTitle: '', - milestoneUrl: '', - issueCount: -1, - mergeRequestCount: -1, - }, - }; - }, - mounted() { - eventHub.$on('deleteMilestoneModal.props', this.setModalProps); - eventHub.$emit('deleteMilestoneModal.mounted'); - }, - beforeDestroy() { - eventHub.$off('deleteMilestoneModal.props', this.setModalProps); - }, - methods: { - setModalProps(modalProps) { - this.modalProps = modalProps; - }, - }, - render(createElement) { - return createElement(deleteMilestoneModal, { - props: this.modalProps, - }); - }, - }); + initDeleteMilestoneModal(); + initPromoteMilestoneModal(); }; diff --git a/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js b/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js new file mode 100644 index 00000000000..d00f81c9094 --- /dev/null +++ b/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js @@ -0,0 +1,82 @@ +import Vue from 'vue'; +import Translate from '~/vue_shared/translate'; +import PromoteMilestoneModal from './components/promote_milestone_modal.vue'; +import eventHub from './event_hub'; + +Vue.use(Translate); + +export default () => { + const onRequestFinished = ({ milestoneUrl, successful }) => { + const button = document.querySelector(`.js-promote-project-milestone-button[data-url="${milestoneUrl}"]`); + + if (!successful) { + button.removeAttribute('disabled'); + } + }; + + const onRequestStarted = (milestoneUrl) => { + const button = document.querySelector(`.js-promote-project-milestone-button[data-url="${milestoneUrl}"]`); + button.setAttribute('disabled', ''); + eventHub.$once('promoteMilestoneModal.requestFinished', onRequestFinished); + }; + + const onDeleteButtonClick = (event) => { + const button = event.currentTarget; + const modalProps = { + milestoneTitle: button.dataset.milestoneTitle, + url: button.dataset.url, + }; + eventHub.$once('promoteMilestoneModal.requestStarted', onRequestStarted); + eventHub.$emit('promoteMilestoneModal.props', modalProps); + }; + + const promoteMilestoneButtons = document.querySelectorAll('.js-promote-project-milestone-button'); + promoteMilestoneButtons.forEach((button) => { + button.addEventListener('click', onDeleteButtonClick); + }); + + eventHub.$once('promoteMilestoneModal.mounted', () => { + promoteMilestoneButtons.forEach((button) => { + button.removeAttribute('disabled'); + }); + }); + + const promoteMilestoneModal = document.getElementById('promote-milestone-modal'); + let promoteMilestoneComponent; + + if (promoteMilestoneModal) { + promoteMilestoneComponent = new Vue({ + el: promoteMilestoneModal, + components: { + PromoteMilestoneModal, + }, + data() { + return { + modalProps: { + milestoneTitle: '', + url: '', + }, + }; + }, + mounted() { + eventHub.$on('promoteMilestoneModal.props', this.setModalProps); + eventHub.$emit('promoteMilestoneModal.mounted'); + }, + beforeDestroy() { + eventHub.$off('promoteMilestoneModal.props', this.setModalProps); + }, + methods: { + setModalProps(modalProps) { + this.modalProps = modalProps; + }, + }, + render(createElement) { + return createElement('promote-milestone-modal', { + props: this.modalProps, + }); + }, + }); + } + + return promoteMilestoneComponent; +}; diff --git a/app/assets/javascripts/pages/profiles/index.js b/app/assets/javascripts/pages/profiles/index.js index c52ad7bc335..04e50963699 100644 --- a/app/assets/javascripts/pages/profiles/index.js +++ b/app/assets/javascripts/pages/profiles/index.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import '~/profile/gl_crop'; import Profile from '~/profile/profile'; diff --git a/app/assets/javascripts/pages/profiles/notifications/show/index.js b/app/assets/javascripts/pages/profiles/notifications/show/index.js new file mode 100644 index 00000000000..2e24a10fa5c --- /dev/null +++ b/app/assets/javascripts/pages/profiles/notifications/show/index.js @@ -0,0 +1,7 @@ +import NotificationsForm from '../../../../notifications_form'; +import notificationsDropdown from '../../../../notifications_dropdown'; + +document.addEventListener('DOMContentLoaded', () => { + new NotificationsForm(); // eslint-disable-line no-new + notificationsDropdown(); +}); diff --git a/app/assets/javascripts/two_factor_auth.js b/app/assets/javascripts/pages/profiles/two_factor_auths/index.js index e3414d9afff..fbdef329ab2 100644 --- a/app/assets/javascripts/two_factor_auth.js +++ b/app/assets/javascripts/pages/profiles/two_factor_auths/index.js @@ -1,4 +1,5 @@ -import U2FRegister from './u2f/register'; +import $ from 'jquery'; +import U2FRegister from '~/u2f/register'; document.addEventListener('DOMContentLoaded', () => { const twoFactorNode = document.querySelector('.js-two-factor-auth'); diff --git a/app/assets/javascripts/pages/projects/blob/show/index.js b/app/assets/javascripts/pages/projects/blob/show/index.js index 26cbb279d4a..85c6862d629 100644 --- a/app/assets/javascripts/pages/projects/blob/show/index.js +++ b/app/assets/javascripts/pages/projects/blob/show/index.js @@ -1,7 +1,29 @@ +import Vue from 'vue'; +import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue'; import BlobViewer from '~/blob/viewer/index'; import initBlob from '~/pages/projects/init_blob'; document.addEventListener('DOMContentLoaded', () => { new BlobViewer(); // eslint-disable-line no-new initBlob(); + + const CommitPipelineStatusEl = document.querySelector('.js-commit-pipeline-status'); + const statusLink = document.querySelector('.commit-actions .ci-status-link'); + if (statusLink) { + statusLink.remove(); + // eslint-disable-next-line no-new + new Vue({ + el: CommitPipelineStatusEl, + components: { + commitPipelineStatus, + }, + render(createElement) { + return createElement('commit-pipeline-status', { + props: { + endpoint: CommitPipelineStatusEl.dataset.endpoint, + }, + }); + }, + }); + } }); diff --git a/app/assets/javascripts/pages/projects/branches/new/index.js b/app/assets/javascripts/pages/projects/branches/new/index.js index d32d5c6cb29..a9658fd1eb4 100644 --- a/app/assets/javascripts/pages/projects/branches/new/index.js +++ b/app/assets/javascripts/pages/projects/branches/new/index.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import NewBranchForm from '~/new_branch_form'; document.addEventListener('DOMContentLoaded', () => ( diff --git a/app/assets/javascripts/pages/projects/commit/pipelines/index.js b/app/assets/javascripts/pages/projects/commit/pipelines/index.js index cd923f13ce8..8cc3cb0a57c 100644 --- a/app/assets/javascripts/pages/projects/commit/pipelines/index.js +++ b/app/assets/javascripts/pages/projects/commit/pipelines/index.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown'; import initPipelines from '~/commit/pipelines/pipelines_bundle'; diff --git a/app/assets/javascripts/pages/projects/commit/show/index.js b/app/assets/javascripts/pages/projects/commit/show/index.js index 1aeed197385..2e23cce11ce 100644 --- a/app/assets/javascripts/pages/projects/commit/show/index.js +++ b/app/assets/javascripts/pages/projects/commit/show/index.js @@ -1,4 +1,6 @@ /* eslint-disable no-new */ + +import $ from 'jquery'; import Diff from '~/diff'; import ZenMode from '~/zen_mode'; import ShortcutsNavigation from '~/shortcuts_navigation'; diff --git a/app/assets/javascripts/pages/projects/edit/index.js b/app/assets/javascripts/pages/projects/edit/index.js index 064de22dfd6..be37df36be8 100644 --- a/app/assets/javascripts/pages/projects/edit/index.js +++ b/app/assets/javascripts/pages/projects/edit/index.js @@ -1,5 +1,6 @@ import initSettingsPanels from '~/settings_panels'; import setupProjectEdit from '~/project_edit'; +import initConfirmDangerModal from '~/confirm_danger_modal'; import ProjectNew from '../shared/project_new'; import projectAvatar from '../shared/project_avatar'; import initProjectPermissionsSettings from '../shared/permissions'; @@ -11,4 +12,5 @@ document.addEventListener('DOMContentLoaded', () => { initSettingsPanels(); projectAvatar(); initProjectPermissionsSettings(); + initConfirmDangerModal(); }); diff --git a/app/assets/javascripts/pages/projects/environments/index.js b/app/assets/javascripts/pages/projects/environments/index/index.js index ace8af00ece..ace8af00ece 100644 --- a/app/assets/javascripts/pages/projects/environments/index.js +++ b/app/assets/javascripts/pages/projects/environments/index/index.js diff --git a/app/assets/javascripts/pages/projects/environments/terminal/index.js b/app/assets/javascripts/pages/projects/environments/terminal/index.js new file mode 100644 index 00000000000..7129e24cee1 --- /dev/null +++ b/app/assets/javascripts/pages/projects/environments/terminal/index.js @@ -0,0 +1,3 @@ +import initTerminal from '~/terminal/'; + +document.addEventListener('DOMContentLoaded', initTerminal); diff --git a/app/assets/javascripts/pages/projects/find_file/show/index.js b/app/assets/javascripts/pages/projects/find_file/show/index.js index 23d857d69ec..24630c2aa05 100644 --- a/app/assets/javascripts/pages/projects/find_file/show/index.js +++ b/app/assets/javascripts/pages/projects/find_file/show/index.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import ProjectFindFile from '~/project_find_file'; import ShortcutsFindFile from '~/shortcuts_find_file'; diff --git a/app/assets/javascripts/pages/projects/graphs/charts/index.js b/app/assets/javascripts/pages/projects/graphs/charts/index.js index 42df19c2968..80159a82bd4 100644 --- a/app/assets/javascripts/pages/projects/graphs/charts/index.js +++ b/app/assets/javascripts/pages/projects/graphs/charts/index.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import Chart from 'chart.js'; import _ from 'underscore'; diff --git a/app/assets/javascripts/pages/projects/graphs/show/index.js b/app/assets/javascripts/pages/projects/graphs/show/index.js index f516ff20995..71f629fbc13 100644 --- a/app/assets/javascripts/pages/projects/graphs/show/index.js +++ b/app/assets/javascripts/pages/projects/graphs/show/index.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import flash from '~/flash'; import { __ } from '~/locale'; import axios from '~/lib/utils/axios_utils'; diff --git a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js index 9ac0b4c07e5..653e2502d01 100644 --- a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js +++ b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js @@ -1,5 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, camelcase, one-var-declaration-per-line, quotes, no-param-reassign, quote-props, comma-dangle, prefer-template, max-len, no-return-assign, no-shadow */ +import $ from 'jquery'; import _ from 'underscore'; import { n__, s__, createDateTimeFormat, sprintf } from '~/locale'; import { ContributorsGraph, ContributorsAuthorGraph, ContributorsMasterGraph } from './stat_graph_contributors_graph'; diff --git a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js index 6ffaa277a0a..a99ce0f1c36 100644 --- a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js +++ b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js @@ -1,4 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, max-len, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, newline-per-chained-call, no-else-return, no-shadow */ + +import $ from 'jquery'; import _ from 'underscore'; import { extent, max } from 'd3-array'; import { select, event as d3Event } from 'd3-selection'; diff --git a/app/assets/javascripts/pages/projects/issues/form.js b/app/assets/javascripts/pages/projects/issues/form.js index 5c7daf84738..14fddbc9a05 100644 --- a/app/assets/javascripts/pages/projects/issues/form.js +++ b/app/assets/javascripts/pages/projects/issues/form.js @@ -1,4 +1,6 @@ /* eslint-disable no-new */ + +import $ from 'jquery'; import GLForm from '~/gl_form'; import IssuableForm from '~/issuable_form'; import LabelsSelect from '~/labels_select'; diff --git a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue new file mode 100644 index 00000000000..54695dfeb99 --- /dev/null +++ b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue @@ -0,0 +1,79 @@ +<script> + import axios from '~/lib/utils/axios_utils'; + import createFlash from '~/flash'; + import GlModal from '~/vue_shared/components/gl_modal.vue'; + import { s__, sprintf } from '~/locale'; + import { visitUrl } from '~/lib/utils/url_utility'; + import eventHub from '../event_hub'; + + export default { + components: { + GlModal, + }, + props: { + url: { + type: String, + required: true, + }, + labelTitle: { + type: String, + required: true, + }, + labelColor: { + type: String, + required: true, + }, + labelTextColor: { + type: String, + required: true, + }, + }, + computed: { + text() { + return s__(`Milestones|Promoting this label will make it available for all projects inside the group. + Existing project labels with the same title will be merged. This action cannot be reversed.`); + }, + title() { + const label = `<span + class="label color-label" + style="background-color: ${this.labelColor}; color: ${this.labelTextColor};" + >${this.labelTitle}</span>`; + + return sprintf(s__('Labels|Promote label %{labelTitle} to Group Label?'), { + labelTitle: label, + }, false); + }, + }, + methods: { + onSubmit() { + eventHub.$emit('promoteLabelModal.requestStarted', this.url); + return axios.post(this.url, { params: { format: 'json' } }) + .then((response) => { + eventHub.$emit('promoteLabelModal.requestFinished', { labelUrl: this.url, successful: true }); + visitUrl(response.data.url); + }) + .catch((error) => { + eventHub.$emit('promoteLabelModal.requestFinished', { labelUrl: this.url, successful: false }); + createFlash(error); + }); + }, + }, + }; +</script> +<template> + <gl-modal + id="promote-label-modal" + footer-primary-button-variant="warning" + :footer-primary-button-text="s__('Labels|Promote Label')" + @submit="onSubmit" + > + <div + slot="title" + v-html="title" + > + {{ title }} + </div> + + {{ text }} + </gl-modal> +</template> diff --git a/app/assets/javascripts/pages/projects/labels/event_hub.js b/app/assets/javascripts/pages/projects/labels/event_hub.js new file mode 100644 index 00000000000..0948c2e5352 --- /dev/null +++ b/app/assets/javascripts/pages/projects/labels/event_hub.js @@ -0,0 +1,3 @@ +import Vue from 'vue'; + +export default new Vue(); diff --git a/app/assets/javascripts/pages/projects/labels/index/index.js b/app/assets/javascripts/pages/projects/labels/index/index.js index 6e45de2a724..2abcbfab1ed 100644 --- a/app/assets/javascripts/pages/projects/labels/index/index.js +++ b/app/assets/javascripts/pages/projects/labels/index/index.js @@ -1,3 +1,91 @@ +import Vue from 'vue'; +import Translate from '~/vue_shared/translate'; import initLabels from '~/init_labels'; +import eventHub from '../event_hub'; +import PromoteLabelModal from '../components/promote_label_modal.vue'; -document.addEventListener('DOMContentLoaded', initLabels); +Vue.use(Translate); + +const initLabelIndex = () => { + initLabels(); + + const onRequestFinished = ({ labelUrl, successful }) => { + const button = document.querySelector(`.js-promote-project-label-button[data-url="${labelUrl}"]`); + + if (!successful) { + button.removeAttribute('disabled'); + } + }; + + const onRequestStarted = (labelUrl) => { + const button = document.querySelector(`.js-promote-project-label-button[data-url="${labelUrl}"]`); + button.setAttribute('disabled', ''); + eventHub.$once('promoteLabelModal.requestFinished', onRequestFinished); + }; + + const onDeleteButtonClick = (event) => { + const button = event.currentTarget; + const modalProps = { + labelTitle: button.dataset.labelTitle, + labelColor: button.dataset.labelColor, + labelTextColor: button.dataset.labelTextColor, + url: button.dataset.url, + }; + eventHub.$once('promoteLabelModal.requestStarted', onRequestStarted); + eventHub.$emit('promoteLabelModal.props', modalProps); + }; + + const promoteLabelButtons = document.querySelectorAll('.js-promote-project-label-button'); + promoteLabelButtons.forEach((button) => { + button.addEventListener('click', onDeleteButtonClick); + }); + + eventHub.$once('promoteLabelModal.mounted', () => { + promoteLabelButtons.forEach((button) => { + button.removeAttribute('disabled'); + }); + }); + + const promoteLabelModal = document.getElementById('promote-label-modal'); + let promoteLabelModalComponent; + + if (promoteLabelModal) { + promoteLabelModalComponent = new Vue({ + el: promoteLabelModal, + components: { + PromoteLabelModal, + }, + data() { + return { + modalProps: { + labelTitle: '', + labelColor: '', + labelTextColor: '', + url: '', + }, + }; + }, + mounted() { + eventHub.$on('promoteLabelModal.props', this.setModalProps); + eventHub.$emit('promoteLabelModal.mounted'); + }, + beforeDestroy() { + eventHub.$off('promoteLabelModal.props', this.setModalProps); + }, + methods: { + setModalProps(modalProps) { + this.modalProps = modalProps; + }, + }, + render(createElement) { + return createElement('promote-label-modal', { + props: this.modalProps, + }); + }, + }); + } + + return promoteLabelModalComponent; +}; + +document.addEventListener('DOMContentLoaded', initLabelIndex); diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js index 8bfac606aab..406fc32f9a2 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js +++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js @@ -1,5 +1,6 @@ /* eslint-disable no-new */ +import $ from 'jquery'; import Diff from '~/diff'; import ShortcutsNavigation from '~/shortcuts_navigation'; import GLForm from '~/gl_form'; diff --git a/app/assets/javascripts/pages/projects/merge_requests/show/index.js b/app/assets/javascripts/pages/projects/merge_requests/show/index.js index 3e72f7a6f37..e5b2827b50c 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/show/index.js +++ b/app/assets/javascripts/pages/projects/merge_requests/show/index.js @@ -1,7 +1,13 @@ +import { hasVueMRDiscussionsCookie } from '~/lib/utils/common_utils'; +import initMrNotes from '~/mr_notes'; import initSidebarBundle from '~/sidebar/sidebar_bundle'; import initShow from '../init_merge_request_show'; document.addEventListener('DOMContentLoaded', () => { initShow(); initSidebarBundle(); + + if (hasVueMRDiscussionsCookie()) { + initMrNotes(); + } }); diff --git a/app/assets/javascripts/pages/projects/network/network.js b/app/assets/javascripts/pages/projects/network/network.js index 7354243e4c8..aa50dd4bb25 100644 --- a/app/assets/javascripts/pages/projects/network/network.js +++ b/app/assets/javascripts/pages/projects/network/network.js @@ -1,5 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, quote-props, prefer-template, comma-dangle, max-len */ +import $ from 'jquery'; import BranchGraph from '../../../network/branch_graph'; export default (function() { diff --git a/app/assets/javascripts/pages/projects/network/show/index.js b/app/assets/javascripts/pages/projects/network/show/index.js index e7dfd2d0128..a0b14fed10f 100644 --- a/app/assets/javascripts/pages/projects/network/show/index.js +++ b/app/assets/javascripts/pages/projects/network/show/index.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import ShortcutsNetwork from '../../../../shortcuts_network'; import Network from '../network'; 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 0c3926d76b5..4ef0d11dd36 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,3 +1,5 @@ +import $ from 'jquery'; + export default class TargetBranchDropdown { constructor() { this.$dropdown = $('.js-target-branch-dropdown'); 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 95ed9c7dc21..95b57d5e048 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,5 +1,7 @@ /* eslint-disable class-methods-use-this */ +import $ from 'jquery'; + const defaultTimezone = 'UTC'; export default class TimezoneDropdown { diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js index cfd30d6053f..c3ac54733a3 100644 --- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import Vue from 'vue'; import Translate from '../../../../vue_shared/translate'; import GlFieldErrors from '../../../../gl_field_errors'; diff --git a/app/assets/javascripts/pages/projects/pipelines/charts/index.js b/app/assets/javascripts/pages/projects/pipelines/charts/index.js index bb92f4e1459..07b6992eba1 100644 --- a/app/assets/javascripts/pages/projects/pipelines/charts/index.js +++ b/app/assets/javascripts/pages/projects/pipelines/charts/index.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import Chart from 'chart.js'; const options = { diff --git a/app/assets/javascripts/pages/projects/pipelines/index/index.js b/app/assets/javascripts/pages/projects/pipelines/index/index.js index 25dfa99ad9c..a84e2790680 100644 --- a/app/assets/javascripts/pages/projects/pipelines/index/index.js +++ b/app/assets/javascripts/pages/projects/pipelines/index/index.js @@ -2,6 +2,7 @@ import Vue from 'vue'; import PipelinesStore from '../../../../pipelines/stores/pipelines_store'; import pipelinesComponent from '../../../../pipelines/components/pipelines.vue'; import Translate from '../../../../vue_shared/translate'; +import { convertPermissionToBoolean } from '../../../../lib/utils/common_utils'; Vue.use(Translate); @@ -11,16 +12,28 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ pipelinesComponent, }, data() { - const store = new PipelinesStore(); - return { - store, + store: new PipelinesStore(), }; }, + created() { + this.dataset = document.querySelector(this.$options.el).dataset; + }, render(createElement) { return createElement('pipelines-component', { props: { store: this.store, + endpoint: this.dataset.endpoint, + helpPagePath: this.dataset.helpPagePath, + emptyStateSvgPath: this.dataset.emptyStateSvgPath, + errorStateSvgPath: this.dataset.errorStateSvgPath, + noPipelinesSvgPath: this.dataset.noPipelinesSvgPath, + autoDevopsPath: this.dataset.helpAutoDevopsPath, + newPipelinePath: this.dataset.newPipelinePath, + canCreatePipeline: convertPermissionToBoolean(this.dataset.canCreatePipeline), + hasGitlabCi: convertPermissionToBoolean(this.dataset.hasGitlabCi), + ciLintPath: this.dataset.ciLintPath, + resetCachePath: this.dataset.resetCachePath, }, }); }, diff --git a/app/assets/javascripts/pages/projects/pipelines/new/index.js b/app/assets/javascripts/pages/projects/pipelines/new/index.js index da20bd995e9..9aa8945e268 100644 --- a/app/assets/javascripts/pages/projects/pipelines/new/index.js +++ b/app/assets/javascripts/pages/projects/pipelines/new/index.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import NewBranchForm from '~/new_branch_form'; document.addEventListener('DOMContentLoaded', () => { diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js index d23ad9a92f4..c1e3425ec75 100644 --- a/app/assets/javascripts/pages/projects/project.js +++ b/app/assets/javascripts/pages/projects/project.js @@ -1,5 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, no-var, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */ +import $ from 'jquery'; import Cookies from 'js-cookie'; import { __ } from '~/locale'; import { visitUrl } from '~/lib/utils/url_utility'; diff --git a/app/assets/javascripts/pages/projects/releases/edit/index.js b/app/assets/javascripts/pages/projects/releases/edit/index.js index 0bf53a8de09..c70271b09c4 100644 --- a/app/assets/javascripts/pages/projects/releases/edit/index.js +++ b/app/assets/javascripts/pages/projects/releases/edit/index.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import initForm from '~/pages/projects/init_form'; document.addEventListener('DOMContentLoaded', () => initForm($('.release-form'))); diff --git a/app/assets/javascripts/pages/projects/settings/repository/show/index.js b/app/assets/javascripts/pages/projects/settings/repository/show/index.js index 001128ead59..788d86d1192 100644 --- a/app/assets/javascripts/pages/projects/settings/repository/show/index.js +++ b/app/assets/javascripts/pages/projects/settings/repository/show/index.js @@ -4,10 +4,14 @@ import ProtectedTagCreate from '~/protected_tags/protected_tag_create'; import ProtectedTagEditList from '~/protected_tags/protected_tag_edit_list'; import initSettingsPanels from '~/settings_panels'; import initDeployKeys from '~/deploy_keys'; +import ProtectedBranchCreate from '~/protected_branches/protected_branch_create'; +import ProtectedBranchEditList from '~/protected_branches/protected_branch_edit_list'; document.addEventListener('DOMContentLoaded', () => { new ProtectedTagCreate(); new ProtectedTagEditList(); initDeployKeys(); initSettingsPanels(); + new ProtectedBranchCreate(); // eslint-disable-line no-new + new ProtectedBranchEditList(); // eslint-disable-line no-new }); diff --git a/app/assets/javascripts/pages/projects/shared/project_avatar.js b/app/assets/javascripts/pages/projects/shared/project_avatar.js index 56627aa155c..447877752fe 100644 --- a/app/assets/javascripts/pages/projects/shared/project_avatar.js +++ b/app/assets/javascripts/pages/projects/shared/project_avatar.js @@ -1,3 +1,5 @@ +import $ from 'jquery'; + export default function projectAvatar() { $('.js-choose-project-avatar-button').bind('click', function onClickAvatar() { const form = $(this).closest('form'); diff --git a/app/assets/javascripts/pages/projects/shared/project_new.js b/app/assets/javascripts/pages/projects/shared/project_new.js index 86faba0b910..56d5574aa2f 100644 --- a/app/assets/javascripts/pages/projects/shared/project_new.js +++ b/app/assets/javascripts/pages/projects/shared/project_new.js @@ -1,5 +1,6 @@ /* eslint-disable func-names, no-var, no-underscore-dangle, prefer-template, prefer-arrow-callback*/ +import $ from 'jquery'; import VisibilitySelect from '../../../visibility_select'; function highlightChanges($elm) { diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js index 9b87f249f09..3b0f0f960b8 100644 --- a/app/assets/javascripts/pages/projects/show/index.js +++ b/app/assets/javascripts/pages/projects/show/index.js @@ -1,3 +1,5 @@ +import $ from 'jquery'; +import initBlob from '~/blob_edit/blob_bundle'; import ShortcutsNavigation from '~/shortcuts_navigation'; import NotificationsForm from '~/notifications_form'; import UserCallout from '~/user_callout'; @@ -18,10 +20,22 @@ document.addEventListener('DOMContentLoaded', () => { className: 'js-autodevops-banner', }); - if ($('#tree-slider').length) new TreeView(); // eslint-disable-line no-new - if ($('.blob-viewer').length) new BlobViewer(); // eslint-disable-line no-new - if ($('.project-show-activity').length) new Activities(); // eslint-disable-line no-new - $('#tree-slider').waitForImages(() => { + // Project show page loads different overview content based on user preferences + const treeSlider = document.querySelector('#tree-slider'); + if (treeSlider) { + new TreeView(); // eslint-disable-line no-new + initBlob(); + } + + if (document.querySelector('.blob-viewer')) { + new BlobViewer(); // eslint-disable-line no-new + } + + if (document.querySelector('.project-show-activity')) { + new Activities(); // eslint-disable-line no-new + } + + $(treeSlider).waitForImages(() => { ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath); }); }); diff --git a/app/assets/javascripts/pages/projects/snippets/edit/index.js b/app/assets/javascripts/pages/projects/snippets/edit/index.js index c15f798b630..53606acc508 100644 --- a/app/assets/javascripts/pages/projects/snippets/edit/index.js +++ b/app/assets/javascripts/pages/projects/snippets/edit/index.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import initSnippet from '~/snippet/snippet_bundle'; import initForm from '~/pages/projects/init_form'; diff --git a/app/assets/javascripts/pages/projects/snippets/new/index.js b/app/assets/javascripts/pages/projects/snippets/new/index.js index c15f798b630..53606acc508 100644 --- a/app/assets/javascripts/pages/projects/snippets/new/index.js +++ b/app/assets/javascripts/pages/projects/snippets/new/index.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import initSnippet from '~/snippet/snippet_bundle'; import initForm from '~/pages/projects/init_form'; diff --git a/app/assets/javascripts/pages/projects/tags/new/index.js b/app/assets/javascripts/pages/projects/tags/new/index.js index 191c98b36bb..8d0edf7e06c 100644 --- a/app/assets/javascripts/pages/projects/tags/new/index.js +++ b/app/assets/javascripts/pages/projects/tags/new/index.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import RefSelectDropdown from '../../../../ref_select_dropdown'; import ZenMode from '../../../../zen_mode'; import GLForm from '../../../../gl_form'; diff --git a/app/assets/javascripts/pages/projects/tree/show/index.js b/app/assets/javascripts/pages/projects/tree/show/index.js index ed7d3f1747c..7ad082a5e61 100644 --- a/app/assets/javascripts/pages/projects/tree/show/index.js +++ b/app/assets/javascripts/pages/projects/tree/show/index.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import Vue from 'vue'; import initBlob from '~/blob_edit/blob_bundle'; import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue'; diff --git a/app/assets/javascripts/pages/projects/wikis/index.js b/app/assets/javascripts/pages/projects/wikis/index.js index b9f8707fd6e..ec01c66ffda 100644 --- a/app/assets/javascripts/pages/projects/wikis/index.js +++ b/app/assets/javascripts/pages/projects/wikis/index.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import Wikis from './wikis'; import ShortcutsWiki from '../../../shortcuts_wiki'; import ZenMode from '../../../zen_mode'; diff --git a/app/assets/javascripts/pages/search/init_filtered_search.js b/app/assets/javascripts/pages/search/init_filtered_search.js index 57f08701a4f..7fdf4ee0bf3 100644 --- a/app/assets/javascripts/pages/search/init_filtered_search.js +++ b/app/assets/javascripts/pages/search/init_filtered_search.js @@ -5,6 +5,7 @@ export default ({ filteredSearchTokenKeys, isGroup, isGroupAncestor, + isGroupDecendent, stateFiltersSelector, }) => { const filteredSearchEnabled = FilteredSearchManager && document.querySelector('.filtered-search'); @@ -13,6 +14,7 @@ export default ({ page, isGroup, isGroupAncestor, + isGroupDecendent, filteredSearchTokenKeys, stateFiltersSelector, }); diff --git a/app/assets/javascripts/pages/search/show/search.js b/app/assets/javascripts/pages/search/show/search.js index cf44e291199..2e1fe78b3fa 100644 --- a/app/assets/javascripts/pages/search/show/search.js +++ b/app/assets/javascripts/pages/search/show/search.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import Flash from '~/flash'; import Api from '~/api'; diff --git a/app/assets/javascripts/pages/sessions/new/index.js b/app/assets/javascripts/pages/sessions/new/index.js index a0aa0499776..80a7114f94d 100644 --- a/app/assets/javascripts/pages/sessions/new/index.js +++ b/app/assets/javascripts/pages/sessions/new/index.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import UsernameValidator from './username_validator'; import SigninTabsMemoizer from './signin_tabs_memoizer'; import OAuthRememberMe from './oauth_remember_me'; diff --git a/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js b/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js index ffc2dd6bbca..53030045292 100644 --- a/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js +++ b/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js @@ -1,3 +1,5 @@ +import $ from 'jquery'; + /** * OAuth-based login buttons have a separate "remember me" checkbox. * diff --git a/app/assets/javascripts/pages/sessions/new/username_validator.js b/app/assets/javascripts/pages/sessions/new/username_validator.js index 745543c22da..825de01b5a2 100644 --- a/app/assets/javascripts/pages/sessions/new/username_validator.js +++ b/app/assets/javascripts/pages/sessions/new/username_validator.js @@ -1,5 +1,6 @@ /* eslint-disable comma-dangle, consistent-return, class-methods-use-this, arrow-parens, no-param-reassign, max-len */ +import $ from 'jquery'; import _ from 'underscore'; import axios from '~/lib/utils/axios_utils'; import flash from '~/flash'; diff --git a/app/assets/javascripts/pages/snippets/form.js b/app/assets/javascripts/pages/snippets/form.js index f996d3cd74e..72d05da1069 100644 --- a/app/assets/javascripts/pages/snippets/form.js +++ b/app/assets/javascripts/pages/snippets/form.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import GLForm from '~/gl_form'; import ZenMode from '~/zen_mode'; diff --git a/app/assets/javascripts/pages/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js index 57306322aa4..8ce938c958b 100644 --- a/app/assets/javascripts/pages/users/activity_calendar.js +++ b/app/assets/javascripts/pages/users/activity_calendar.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import _ from 'underscore'; import { scaleLinear, scaleThreshold } from 'd3-scale'; import { select } from 'd3-selection'; diff --git a/app/assets/javascripts/pages/users/index.js b/app/assets/javascripts/pages/users/index.js index 899dcd42e37..6b1626b0161 100644 --- a/app/assets/javascripts/pages/users/index.js +++ b/app/assets/javascripts/pages/users/index.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import UserCallout from '~/user_callout'; import Cookies from 'js-cookie'; import UserTabs from './user_tabs'; diff --git a/app/assets/javascripts/pages/users/user_tabs.js b/app/assets/javascripts/pages/users/user_tabs.js index c1217623467..124bc2ba710 100644 --- a/app/assets/javascripts/pages/users/user_tabs.js +++ b/app/assets/javascripts/pages/users/user_tabs.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import axios from '~/lib/utils/axios_utils'; import Activities from '~/activities'; import { localTimeAgo } from '~/lib/utils/datetime_utility'; diff --git a/app/assets/javascripts/performance_bar.js b/app/assets/javascripts/performance_bar.js deleted file mode 100644 index 0562a681c4b..00000000000 --- a/app/assets/javascripts/performance_bar.js +++ /dev/null @@ -1,63 +0,0 @@ -import 'vendor/peek'; -import 'vendor/peek.performance_bar'; -import { getParameterValues } from './lib/utils/url_utility'; - -export default class PerformanceBar { - constructor(opts) { - if (!PerformanceBar.singleton) { - this.init(opts); - PerformanceBar.singleton = this; - } - return PerformanceBar.singleton; - } - - init(opts) { - const $container = $(opts.container); - this.$sqlProfileLink = $container.find('.js-toggle-modal-peek-sql'); - this.$sqlProfileModal = $container.find('#modal-peek-pg-queries'); - this.$lineProfileLink = $container.find('.js-toggle-modal-peek-line-profile'); - this.$lineProfileModal = $('#modal-peek-line-profile'); - this.initEventListeners(); - this.showModalOnLoad(); - } - - initEventListeners() { - this.$sqlProfileLink.on('click', () => this.handleSQLProfileLink()); - this.$lineProfileLink.on('click', e => this.handleLineProfileLink(e)); - $(document).on('click', '.js-lineprof-file', PerformanceBar.toggleLineProfileFile); - } - - showModalOnLoad() { - // When a lineprofiler query-string param is present, we show the line - // profiler modal upon page load - if (/lineprofiler/.test(window.location.search)) { - PerformanceBar.toggleModal(this.$lineProfileModal); - } - } - - handleSQLProfileLink() { - PerformanceBar.toggleModal(this.$sqlProfileModal); - } - - handleLineProfileLink(e) { - const lineProfilerParameter = getParameterValues('lineprofiler'); - const lineProfilerParameterRegex = new RegExp(`lineprofiler=${lineProfilerParameter[0]}`); - const shouldToggleModal = lineProfilerParameter.length > 0 && - lineProfilerParameterRegex.test(e.currentTarget.href); - - if (shouldToggleModal) { - e.preventDefault(); - PerformanceBar.toggleModal(this.$lineProfileModal); - } - } - - static toggleModal($modal) { - if ($modal.length) { - $modal.modal('toggle'); - } - } - - static toggleLineProfileFile(e) { - $(e.currentTarget).parents('.peek-rblineprof-file').find('.data').toggle(); - } -} diff --git a/app/assets/javascripts/performance_bar/components/detailed_metric.vue b/app/assets/javascripts/performance_bar/components/detailed_metric.vue new file mode 100644 index 00000000000..d4881f07972 --- /dev/null +++ b/app/assets/javascripts/performance_bar/components/detailed_metric.vue @@ -0,0 +1,92 @@ +<script> +import GlModal from '~/vue_shared/components/gl_modal.vue'; + +export default { + components: { + GlModal, + }, + props: { + currentRequest: { + type: Object, + required: true, + }, + metric: { + type: String, + required: true, + }, + header: { + type: String, + required: true, + }, + details: { + type: String, + required: true, + }, + keys: { + type: Array, + required: true, + }, + }, + computed: { + metricDetails() { + return this.currentRequest.details[this.metric]; + }, + detailsList() { + return this.metricDetails[this.details]; + }, + }, +}; +</script> +<template> + <div + :id="`peek-view-${metric}`" + class="view" + v-if="currentRequest.details" + > + <button + :data-target="`#modal-peek-${metric}-details`" + class="btn-blank btn-link bold" + type="button" + data-toggle="modal" + > + {{ metricDetails.duration }} + / + {{ metricDetails.calls }} + </button> + <gl-modal + :id="`modal-peek-${metric}-details`" + :header-title-text="header" + class="performance-bar-modal" + > + <table + class="table" + > + <template v-if="detailsList.length"> + <tr + v-for="(item, index) in detailsList" + :key="index" + > + <td><strong>{{ item.duration }}ms</strong></td> + <td + v-for="key in keys" + :key="key" + > + {{ item[key] }} + </td> + </tr> + </template> + <template v-else> + <tr> + <td> + No {{ header.toLowerCase() }} for this request. + </td> + </tr> + </template> + </table> + + <div slot="footer"> + </div> + </gl-modal> + {{ metric }} + </div> +</template> diff --git a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue new file mode 100644 index 00000000000..2fd1715ee79 --- /dev/null +++ b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue @@ -0,0 +1,191 @@ +<script> +import $ from 'jquery'; + +import PerformanceBarService from '../services/performance_bar_service'; +import detailedMetric from './detailed_metric.vue'; +import requestSelector from './request_selector.vue'; +import simpleMetric from './simple_metric.vue'; +import upstreamPerformanceBar from './upstream_performance_bar.vue'; + +import Flash from '../../flash'; + +export default { + components: { + detailedMetric, + requestSelector, + simpleMetric, + upstreamPerformanceBar, + }, + props: { + store: { + type: Object, + required: true, + }, + env: { + type: String, + required: true, + }, + requestId: { + type: String, + required: true, + }, + peekUrl: { + type: String, + required: true, + }, + profileUrl: { + type: String, + required: true, + }, + }, + detailedMetrics: [ + { metric: 'pg', header: 'SQL queries', details: 'queries', keys: ['sql'] }, + { + metric: 'gitaly', + header: 'Gitaly calls', + details: 'details', + keys: ['feature', 'request'], + }, + ], + simpleMetrics: ['redis', 'sidekiq'], + data() { + return { currentRequestId: '' }; + }, + computed: { + requests() { + return this.store.requestsWithDetails(); + }, + currentRequest: { + get() { + return this.store.findRequest(this.currentRequestId); + }, + set(requestId) { + this.currentRequestId = requestId; + }, + }, + initialRequest() { + return this.currentRequestId === this.requestId; + }, + lineProfileModal() { + return $('#modal-peek-line-profile'); + }, + }, + mounted() { + this.interceptor = PerformanceBarService.registerInterceptor( + this.peekUrl, + this.loadRequestDetails, + ); + + this.loadRequestDetails(this.requestId, window.location.href); + this.currentRequest = this.requestId; + + if (this.lineProfileModal.length) { + this.lineProfileModal.modal('toggle'); + } + }, + beforeDestroy() { + PerformanceBarService.removeInterceptor(this.interceptor); + }, + methods: { + loadRequestDetails(requestId, requestUrl) { + if (!this.store.canTrackRequest(requestUrl)) { + return; + } + + this.store.addRequest(requestId, requestUrl); + + PerformanceBarService.fetchRequestDetails(this.peekUrl, requestId) + .then(res => { + this.store.addRequestDetails(requestId, res.data.data); + }) + .catch(() => + Flash(`Error getting performance bar results for ${requestId}`), + ); + }, + changeCurrentRequest(newRequestId) { + this.currentRequest = newRequestId; + }, + }, +}; +</script> +<template> + <div + id="js-peek" + :class="env" + > + <div + v-if="currentRequest" + class="container-fluid container-limited" + > + <div + id="peek-view-host" + class="view" + > + <span + v-if="currentRequest.details" + class="current-host" + > + {{ currentRequest.details.host.hostname }} + </span> + </div> + <upstream-performance-bar + v-if="initialRequest && currentRequest.details" + /> + <detailed-metric + v-for="metric in $options.detailedMetrics" + :key="metric.metric" + :current-request="currentRequest" + :metric="metric.metric" + :header="metric.header" + :details="metric.details" + :keys="metric.keys" + /> + <div + v-if="initialRequest" + id="peek-view-rblineprof" + class="view" + > + <button + v-if="lineProfileModal.length" + class="btn-link btn-blank" + data-toggle="modal" + data-target="#modal-peek-line-profile" + > + profile + </button> + <a + v-else + :href="profileUrl" + > + profile + </a> + </div> + <simple-metric + v-for="metric in $options.simpleMetrics" + :current-request="currentRequest" + :key="metric" + :metric="metric" + /> + <div + id="peek-view-gc" + class="view" + > + <span + v-if="currentRequest.details" + class="bold" + > + <span title="Invoke Time">{{ currentRequest.details.gc.gc_time }}</span>ms + / + <span title="Invoke Count">{{ currentRequest.details.gc.invokes }}</span> + gc + </span> + </div> + <request-selector + v-if="currentRequest" + :current-request="currentRequest" + :requests="requests" + @change-current-request="changeCurrentRequest" + /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/performance_bar/components/request_selector.vue b/app/assets/javascripts/performance_bar/components/request_selector.vue new file mode 100644 index 00000000000..3ed07a4a47d --- /dev/null +++ b/app/assets/javascripts/performance_bar/components/request_selector.vue @@ -0,0 +1,52 @@ +<script> +export default { + props: { + currentRequest: { + type: Object, + required: true, + }, + requests: { + type: Array, + required: true, + }, + }, + data() { + return { + currentRequestId: this.currentRequest.id, + }; + }, + watch: { + currentRequestId(newRequestId) { + this.$emit('change-current-request', newRequestId); + }, + }, + methods: { + truncatedUrl(requestUrl) { + const components = requestUrl.replace(/\/$/, '').split('/'); + let truncated = components[components.length - 1]; + + if (truncated.match(/^\d+$/)) { + truncated = `${components[components.length - 2]}/${truncated}`; + } + + return truncated; + }, + }, +}; +</script> +<template> + <div + id="peek-request-selector" + class="pull-right" + > + <select v-model="currentRequestId"> + <option + v-for="request in requests" + :key="request.id" + :value="request.id" + > + {{ truncatedUrl(request.url) }} + </option> + </select> + </div> +</template> diff --git a/app/assets/javascripts/performance_bar/components/simple_metric.vue b/app/assets/javascripts/performance_bar/components/simple_metric.vue new file mode 100644 index 00000000000..b654bc66249 --- /dev/null +++ b/app/assets/javascripts/performance_bar/components/simple_metric.vue @@ -0,0 +1,30 @@ +<script> +export default { + props: { + currentRequest: { + type: Object, + required: true, + }, + metric: { + type: String, + required: true, + }, + }, +}; +</script> +<template> + <div + :id="`peek-view-${metric}`" + class="view" + > + <span + v-if="currentRequest.details" + class="bold" + > + {{ currentRequest.details[metric].duration }} + / + {{ currentRequest.details[metric].calls }} + </span> + {{ metric }} + </div> +</template> diff --git a/app/assets/javascripts/performance_bar/components/upstream_performance_bar.vue b/app/assets/javascripts/performance_bar/components/upstream_performance_bar.vue new file mode 100644 index 00000000000..2b5915f381f --- /dev/null +++ b/app/assets/javascripts/performance_bar/components/upstream_performance_bar.vue @@ -0,0 +1,20 @@ +<script> +export default { + mounted() { + const upstreamPerformanceBar = document + .getElementById('peek-view-performance-bar') + .cloneNode(true); + + upstreamPerformanceBar.classList.remove('hidden'); + + this.$refs.wrapper.appendChild(upstreamPerformanceBar); + }, +}; +</script> +<template> + <div + id="peek-view-performance-bar-vue" + class="view" + ref="wrapper" + ></div> +</template> diff --git a/app/assets/javascripts/performance_bar/index.js b/app/assets/javascripts/performance_bar/index.js new file mode 100644 index 00000000000..a0ddf36a672 --- /dev/null +++ b/app/assets/javascripts/performance_bar/index.js @@ -0,0 +1,37 @@ +import 'vendor/peek.performance_bar'; + +import Vue from 'vue'; +import performanceBarApp from './components/performance_bar_app.vue'; +import PerformanceBarStore from './stores/performance_bar_store'; + +export default ({ container }) => + new Vue({ + el: container, + components: { + performanceBarApp, + }, + data() { + const performanceBarData = document.querySelector(this.$options.el) + .dataset; + const store = new PerformanceBarStore(); + + return { + store, + env: performanceBarData.env, + requestId: performanceBarData.requestId, + peekUrl: performanceBarData.peekUrl, + profileUrl: performanceBarData.profileUrl, + }; + }, + render(createElement) { + return createElement('performance-bar-app', { + props: { + store: this.store, + env: this.env, + requestId: this.requestId, + peekUrl: this.peekUrl, + profileUrl: this.profileUrl, + }, + }); + }, + }); diff --git a/app/assets/javascripts/performance_bar/services/performance_bar_service.js b/app/assets/javascripts/performance_bar/services/performance_bar_service.js new file mode 100644 index 00000000000..d8e792446c3 --- /dev/null +++ b/app/assets/javascripts/performance_bar/services/performance_bar_service.js @@ -0,0 +1,24 @@ +import axios from '../../lib/utils/axios_utils'; + +export default class PerformanceBarService { + static fetchRequestDetails(peekUrl, requestId) { + return axios.get(peekUrl, { params: { request_id: requestId } }); + } + + static registerInterceptor(peekUrl, callback) { + return axios.interceptors.response.use(response => { + const requestId = response.headers['x-request-id']; + const requestUrl = response.config.url; + + if (requestUrl !== peekUrl && requestId) { + callback(requestId, requestUrl); + } + + return response; + }); + } + + static removeInterceptor(interceptor) { + axios.interceptors.response.eject(interceptor); + } +} diff --git a/app/assets/javascripts/performance_bar/stores/performance_bar_store.js b/app/assets/javascripts/performance_bar/stores/performance_bar_store.js new file mode 100644 index 00000000000..c6b2f55243c --- /dev/null +++ b/app/assets/javascripts/performance_bar/stores/performance_bar_store.js @@ -0,0 +1,39 @@ +export default class PerformanceBarStore { + constructor() { + this.requests = []; + } + + addRequest(requestId, requestUrl, requestDetails) { + if (!this.findRequest(requestId)) { + this.requests.push({ + id: requestId, + url: requestUrl, + details: requestDetails, + }); + } + + return this.requests; + } + + findRequest(requestId) { + return this.requests.find(request => request.id === requestId); + } + + addRequestDetails(requestId, requestDetails) { + const request = this.findRequest(requestId); + + request.details = requestDetails; + + return request; + } + + requestsWithDetails() { + return this.requests.filter(request => request.details); + } + + canTrackRequest(requestUrl) { + return ( + this.requests.filter(request => request.url === requestUrl).length < 2 + ); + } +} diff --git a/app/assets/javascripts/pipelines/components/blank_state.vue b/app/assets/javascripts/pipelines/components/blank_state.vue new file mode 100644 index 00000000000..8d3d6223d7b --- /dev/null +++ b/app/assets/javascripts/pipelines/components/blank_state.vue @@ -0,0 +1,32 @@ +<script> + export default { + name: 'PipelinesSvgState', + props: { + svgPath: { + type: String, + required: true, + }, + + message: { + type: String, + required: true, + }, + }, + }; +</script> + +<template> + <div class="row empty-state"> + <div class="col-xs-12"> + <div class="svg-content"> + <img :src="svgPath" /> + </div> + </div> + + <div class="col-xs-12 text-center"> + <div class="text-content"> + <h4>{{ message }}</h4> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/pipelines/components/empty_state.vue b/app/assets/javascripts/pipelines/components/empty_state.vue index dfaa2574091..10ac8c08bed 100644 --- a/app/assets/javascripts/pipelines/components/empty_state.vue +++ b/app/assets/javascripts/pipelines/components/empty_state.vue @@ -1,5 +1,6 @@ <script> export default { + name: 'PipelinesEmptyState', props: { helpPagePath: { type: String, @@ -9,6 +10,10 @@ type: String, required: true, }, + canSetCi: { + type: Boolean, + required: true, + }, }, }; </script> @@ -22,22 +27,36 @@ <div class="col-xs-12"> <div class="text-content"> - <h4 class="text-center"> - {{ s__("Pipelines|Build with confidence") }} - </h4> - <p> - {{ s__(`Pipelines|Continous Integration can help -catch bugs by running your tests automatically, -while Continuous Deployment can help you deliver code to your product environment.`) }} + + <template v-if="canSetCi"> + <h4 class="text-center"> + {{ s__('Pipelines|Build with confidence') }} + </h4> + + <p> + {{ s__(`Pipelines|Continous Integration can help + catch bugs by running your tests automatically, + while Continuous Deployment can help you deliver + code to your product environment.`) }} + </p> + + <div class="text-center"> + <a + :href="helpPagePath" + class="btn btn-primary js-get-started-pipelines" + > + {{ s__('Pipelines|Get started with Pipelines') }} + </a> + </div> + </template> + + <p + v-else + class="text-center" + > + {{ s__('Pipelines|This project is not currently set up to run pipelines.') }} </p> - <div class="text-center"> - <a - :href="helpPagePath" - class="btn btn-info" - > - {{ s__("Pipelines|Get started with Pipelines") }} - </a> - </div> + </div> </div> </div> diff --git a/app/assets/javascripts/pipelines/components/error_state.vue b/app/assets/javascripts/pipelines/components/error_state.vue deleted file mode 100644 index 012853b201d..00000000000 --- a/app/assets/javascripts/pipelines/components/error_state.vue +++ /dev/null @@ -1,26 +0,0 @@ -<script> -export default { - props: { - errorStateSvgPath: { - type: String, - required: true, - }, - }, -}; -</script> - -<template> - <div class="row empty-state js-pipelines-error-state"> - <div class="col-xs-12"> - <div class="svg-content"> - <img :src="errorStateSvgPath"/> - </div> - </div> - - <div class="col-xs-12 text-center"> - <div class="text-content"> - <h4>The API failed to fetch the pipelines.</h4> - </div> - </div> - </div> -</template> diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue index b86e95f0b4a..be213c2ee78 100644 --- a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue @@ -1,4 +1,5 @@ <script> + import $ from 'jquery'; import jobNameComponent from './job_name_component.vue'; import jobComponent from './job_component.vue'; import tooltip from '../../../vue_shared/directives/tooltip'; diff --git a/app/assets/javascripts/pipelines/components/nav_controls.vue b/app/assets/javascripts/pipelines/components/nav_controls.vue index f31a91c3403..eba5678e3e5 100644 --- a/app/assets/javascripts/pipelines/components/nav_controls.vue +++ b/app/assets/javascripts/pipelines/components/nav_controls.vue @@ -1,67 +1,67 @@ <script> -export default { - name: 'PipelineNavControls', - props: { - newPipelinePath: { - type: String, - required: true, - }, + import LoadingButton from '../../vue_shared/components/loading_button.vue'; - hasCiEnabled: { - type: Boolean, - required: true, + export default { + name: 'PipelineNavControls', + components: { + LoadingButton, }, + props: { + newPipelinePath: { + type: String, + required: false, + default: null, + }, - helpPagePath: { - type: String, - required: true, - }, + resetCachePath: { + type: String, + required: false, + default: null, + }, - resetCachePath: { - type: String, - required: true, - }, + ciLintPath: { + type: String, + required: false, + default: null, + }, - ciLintPath: { - type: String, - required: true, + isResetCacheButtonLoading: { + type: Boolean, + required: false, + default: false, + }, }, - - canCreatePipeline: { - type: Boolean, - required: true, + methods: { + onClickResetCache() { + this.$emit('resetRunnersCache', this.resetCachePath); + }, }, - }, -}; + }; </script> <template> <div class="nav-controls"> <a - v-if="canCreatePipeline" + v-if="newPipelinePath" :href="newPipelinePath" - class="btn btn-create"> - Run Pipeline + class="btn btn-create js-run-pipeline" + > + {{ s__('Pipelines|Run Pipeline') }} </a> - <a - v-if="!hasCiEnabled" - :href="helpPagePath" - class="btn btn-info"> - Get started with Pipelines - </a> - - <a - data-method="post" - rel="nofollow" - :href="resetCachePath" - class="btn btn-default"> - Clear runner caches - </a> + <loading-button + v-if="resetCachePath" + @click="onClickResetCache" + :loading="isResetCacheButtonLoading" + class="btn btn-default js-clear-cache" + :label="s__('Pipelines|Clear Runner Caches')" + /> <a + v-if="ciLintPath" :href="ciLintPath" - class="btn btn-default"> - CI Lint + class="btn btn-default js-ci-lint" + > + {{ s__('Pipelines|CI Lint') }} </a> </div> </template> diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue index 90930d5ff44..e0a7284124d 100644 --- a/app/assets/javascripts/pipelines/components/pipelines.vue +++ b/app/assets/javascripts/pipelines/components/pipelines.vue @@ -1,12 +1,13 @@ <script> import _ from 'underscore'; + import { __, sprintf, s__ } from '../../locale'; + import createFlash from '../../flash'; import PipelinesService from '../services/pipelines_service'; import pipelinesMixin from '../mixins/pipelines'; - import tablePagination from '../../vue_shared/components/table_pagination.vue'; - import navigationTabs from '../../vue_shared/components/navigation_tabs.vue'; - import navigationControls from './nav_controls.vue'; + import TablePagination from '../../vue_shared/components/table_pagination.vue'; + import NavigationTabs from '../../vue_shared/components/navigation_tabs.vue'; + import NavigationControls from './nav_controls.vue'; import { - convertPermissionToBoolean, getParameterByName, parseQueryStringIntoObject, } from '../../lib/utils/common_utils'; @@ -14,9 +15,9 @@ export default { components: { - tablePagination, - navigationTabs, - navigationControls, + TablePagination, + NavigationTabs, + NavigationControls, }, mixins: [ pipelinesMixin, @@ -36,111 +37,187 @@ required: false, default: 'root', }, + endpoint: { + type: String, + required: true, + }, + helpPagePath: { + type: String, + required: true, + }, + emptyStateSvgPath: { + type: String, + required: true, + }, + errorStateSvgPath: { + type: String, + required: true, + }, + noPipelinesSvgPath: { + type: String, + required: true, + }, + autoDevopsPath: { + type: String, + required: true, + }, + hasGitlabCi: { + type: Boolean, + required: true, + }, + canCreatePipeline: { + type: Boolean, + required: true, + }, + ciLintPath: { + type: String, + required: false, + default: null, + }, + resetCachePath: { + type: String, + required: false, + default: null, + }, + newPipelinePath: { + type: String, + required: false, + default: null, + }, }, data() { - const pipelinesData = document.querySelector('#pipelines-list-vue').dataset; - return { - endpoint: pipelinesData.endpoint, - helpPagePath: pipelinesData.helpPagePath, - emptyStateSvgPath: pipelinesData.emptyStateSvgPath, - errorStateSvgPath: pipelinesData.errorStateSvgPath, - autoDevopsPath: pipelinesData.helpAutoDevopsPath, - newPipelinePath: pipelinesData.newPipelinePath, - canCreatePipeline: pipelinesData.canCreatePipeline, - hasCi: pipelinesData.hasCi, - ciLintPath: pipelinesData.ciLintPath, - resetCachePath: pipelinesData.resetCachePath, + // Start with loading state to avoid a glitch when the empty state will be rendered + isLoading: true, state: this.store.state, scope: getParameterByName('scope') || 'all', page: getParameterByName('page') || '1', requestData: {}, + isResetCacheButtonLoading: false, }; }, - computed: { - canCreatePipelineParsed() { - return convertPermissionToBoolean(this.canCreatePipeline); - }, + stateMap: { + // with tabs + loading: 'loading', + tableList: 'tableList', + error: 'error', + emptyTab: 'emptyTab', + // without tabs + emptyState: 'emptyState', + }, + scopes: { + all: 'all', + pending: 'pending', + running: 'running', + finished: 'finished', + branches: 'branches', + tags: 'tags', + }, + computed: { /** - * The empty state should only be rendered when the request is made to fetch all pipelines - * and none is returned. - * - * @return {Boolean} - */ - shouldRenderEmptyState() { - return !this.isLoading && - !this.hasError && - this.hasMadeRequest && - !this.state.pipelines.length && - (this.scope === 'all' || this.scope === null); + * `hasGitlabCi` handles both internal and external CI. + * The order on which the checks are made in this method is + * important to guarantee we handle all the corner cases. + */ + stateToRender() { + const { stateMap } = this.$options; + + if (this.isLoading) { + return stateMap.loading; + } + + if (this.hasError) { + return stateMap.error; + } + + if (this.state.pipelines.length) { + return stateMap.tableList; + } + + if ((this.scope !== 'all' && this.scope !== null) || this.hasGitlabCi) { + return stateMap.emptyTab; + } + + return stateMap.emptyState; }, /** - * When a specific scope does not have pipelines we render a message. - * - * @return {Boolean} + * Tabs are rendered in all states except empty state. + * They are not rendered before the first request to avoid a flicker on first load. */ - shouldRenderNoPipelinesMessage() { - return !this.isLoading && - !this.hasError && - !this.state.pipelines.length && - this.scope !== 'all' && - this.scope !== null; + shouldRenderTabs() { + const { stateMap } = this.$options; + return this.hasMadeRequest && + [ + stateMap.loading, + stateMap.tableList, + stateMap.error, + stateMap.emptyTab, + ].includes(this.stateToRender); }, - shouldRenderTable() { - return !this.hasError && - !this.isLoading && this.state.pipelines.length; + shouldRenderButtons() { + return (this.newPipelinePath || + this.resetCachePath || + this.ciLintPath) && this.shouldRenderTabs; }, - /** - * Pagination should only be rendered when there is more than one page. - * - * @return {Boolean} - */ + shouldRenderPagination() { return !this.isLoading && this.state.pipelines.length && this.state.pageInfo.total > this.state.pageInfo.perPage; }, - hasCiEnabled() { - return this.hasCi !== undefined; + + emptyTabMessage() { + const { scopes } = this.$options; + const possibleScopes = [scopes.pending, scopes.running, scopes.finished]; + + if (possibleScopes.includes(this.scope)) { + return sprintf(s__('Pipelines|There are currently no %{scope} pipelines.'), { + scope: this.scope, + }); + } + + return s__('Pipelines|There are currently no pipelines.'); }, tabs() { const { count } = this.state; + const { scopes } = this.$options; + return [ { - name: 'All', - scope: 'all', + name: __('All'), + scope: scopes.all, count: count.all, isActive: this.scope === 'all', }, { - name: 'Pending', - scope: 'pending', + name: __('Pending'), + scope: scopes.pending, count: count.pending, isActive: this.scope === 'pending', }, { - name: 'Running', - scope: 'running', + name: __('Running'), + scope: scopes.running, count: count.running, isActive: this.scope === 'running', }, { - name: 'Finished', - scope: 'finished', + name: __('Finished'), + scope: scopes.finished, count: count.finished, isActive: this.scope === 'finished', }, { - name: 'Branches', - scope: 'branches', + name: __('Branches'), + scope: scopes.branches, isActive: this.scope === 'branches', }, { - name: 'Tags', - scope: 'tags', + name: __('Tags'), + scope: scopes.tags, isActive: this.scope === 'tags', }, ]; @@ -187,7 +264,24 @@ this.errorCallback(); // restart polling - this.poll.restart(); + this.poll.restart({ data: this.requestData }); + }); + }, + + handleResetRunnersCache(endpoint) { + this.isResetCacheButtonLoading = true; + + this.service.postAction(endpoint) + .then(() => { + this.isResetCacheButtonLoading = false; + createFlash( + s__('Pipelines|Project cache successfully reset.'), + 'notice', + ); + }) + .catch(() => { + this.isResetCacheButtonLoading = false; + createFlash(s__('Pipelines|Something went wrong while cleaning runners cache.')); }); }, }, @@ -197,69 +291,72 @@ <div class="pipelines-container"> <div class="top-area scrolling-tabs-container inner-page-scroll-tabs" - v-if="!shouldRenderEmptyState" + v-if="shouldRenderTabs || shouldRenderButtons" > <div class="fade-left"> <i class="fa fa-angle-left" - aria-hidden="true"> + aria-hidden="true" + > </i> </div> <div class="fade-right"> <i class="fa fa-angle-right" - aria-hidden="true"> + aria-hidden="true" + > </i> </div> <navigation-tabs + v-if="shouldRenderTabs" :tabs="tabs" @onChangeTab="onChangeTab" scope="pipelines" /> <navigation-controls + v-if="shouldRenderButtons" :new-pipeline-path="newPipelinePath" - :has-ci-enabled="hasCiEnabled" - :help-page-path="helpPagePath" :reset-cache-path="resetCachePath" :ci-lint-path="ciLintPath" - :can-create-pipeline="canCreatePipelineParsed " + @resetRunnersCache="handleResetRunnersCache" + :is-reset-cache-button-loading="isResetCacheButtonLoading" /> </div> <div class="content-list pipelines"> <loading-icon - label="Loading Pipelines" + v-if="stateToRender === $options.stateMap.loading" + :label="s__('Pipelines|Loading Pipelines')" size="3" - v-if="isLoading" class="prepend-top-20" /> <empty-state - v-if="shouldRenderEmptyState" + v-else-if="stateToRender === $options.stateMap.emptyState" :help-page-path="helpPagePath" :empty-state-svg-path="emptyStateSvgPath" + :can-set-ci="canCreatePipeline" /> - <error-state - v-if="shouldRenderErrorState" - :error-state-svg-path="errorStateSvgPath" + <svg-blank-state + v-else-if="stateToRender === $options.stateMap.error" + :svg-path="errorStateSvgPath" + :message="s__(`Pipelines|There was an error fetching the pipelines. + Try again in a few moments or contact your support team.`)" /> - <div - class="blank-state-row" - v-if="shouldRenderNoPipelinesMessage" - > - <div class="blank-state-center"> - <h2 class="blank-state-title js-blank-state-title">No pipelines to show.</h2> - </div> - </div> + <svg-blank-state + v-else-if="stateToRender === $options.stateMap.emptyTab" + :svg-path="noPipelinesSvgPath" + :message="emptyTabMessage" + /> <div class="table-holder" - v-if="shouldRenderTable" + v-else-if="stateToRender === $options.stateMap.tableList" > <pipelines-table-component diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue index ecf2b10486e..8bc7a1f20b2 100644 --- a/app/assets/javascripts/pipelines/components/stage.vue +++ b/app/assets/javascripts/pipelines/components/stage.vue @@ -1,4 +1,5 @@ <script> + import $ from 'jquery'; /** * Renders each stage of the pipeline mini graph. diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js index 50bdf80c3e3..522a4277bd7 100644 --- a/app/assets/javascripts/pipelines/mixins/pipelines.js +++ b/app/assets/javascripts/pipelines/mixins/pipelines.js @@ -1,23 +1,19 @@ import Visibility from 'visibilityjs'; +import { __ } from '../../locale'; import Flash from '../../flash'; import Poll from '../../lib/utils/poll'; -import emptyState from '../components/empty_state.vue'; -import errorState from '../components/error_state.vue'; -import loadingIcon from '../../vue_shared/components/loading_icon.vue'; -import pipelinesTableComponent from '../components/pipelines_table.vue'; +import EmptyState from '../components/empty_state.vue'; +import SvgBlankState from '../components/blank_state.vue'; +import LoadingIcon from '../../vue_shared/components/loading_icon.vue'; +import PipelinesTableComponent from '../components/pipelines_table.vue'; import eventHub from '../event_hub'; export default { components: { - pipelinesTableComponent, - errorState, - emptyState, - loadingIcon, - }, - computed: { - shouldRenderErrorState() { - return this.hasError && !this.isLoading; - }, + PipelinesTableComponent, + SvgBlankState, + EmptyState, + LoadingIcon, }, data() { return { @@ -55,12 +51,10 @@ export default { } }); - eventHub.$on('refreshPipelines', this.fetchPipelines); eventHub.$on('postAction', this.postAction); }, beforeDestroy() { - eventHub.$off('refreshPipelines'); - eventHub.$on('postAction', this.postAction); + eventHub.$off('postAction', this.postAction); }, destroyed() { this.poll.stop(); @@ -85,6 +79,7 @@ export default { this.hasError = true; this.isLoading = false; this.updateGraphDropdown = false; + this.hasMadeRequest = true; }, setIsMakingRequest(isMakingRequest) { this.isMakingRequest = isMakingRequest; @@ -95,8 +90,8 @@ export default { }, postAction(endpoint) { this.service.postAction(endpoint) - .then(() => eventHub.$emit('refreshPipelines')) - .catch(() => new Flash('An error occurred while making the request.')); + .then(() => this.fetchPipelines()) + .catch(() => Flash(__('An error occurred while making the request.'))); }, }, }; diff --git a/app/assets/javascripts/preview_markdown.js b/app/assets/javascripts/preview_markdown.js index 464bfb351e7..246a265ef2b 100644 --- a/app/assets/javascripts/preview_markdown.js +++ b/app/assets/javascripts/preview_markdown.js @@ -1,5 +1,10 @@ /* eslint-disable func-names, no-var, object-shorthand, comma-dangle, prefer-arrow-callback */ +import $ from 'jquery'; +import axios from '~/lib/utils/axios_utils'; +import flash from '~/flash'; +import { __ } from '~/locale'; + // MarkdownPreview // // Handles toggling the "Write" and "Preview" tab clicks, rendering the preview @@ -7,10 +12,6 @@ // more than `x` users are referenced. // -import axios from '~/lib/utils/axios_utils'; -import flash from '~/flash'; -import { __ } from '~/locale'; - var lastTextareaPreviewed; var lastTextareaHeight = null; var markdownPreview; diff --git a/app/assets/javascripts/profile/gl_crop.js b/app/assets/javascripts/profile/gl_crop.js index 4bdda611cfc..8f93156cdd1 100644 --- a/app/assets/javascripts/profile/gl_crop.js +++ b/app/assets/javascripts/profile/gl_crop.js @@ -1,5 +1,6 @@ /* eslint-disable no-useless-escape, max-len, quotes, no-var, no-underscore-dangle, func-names, space-before-function-paren, no-unused-vars, no-return-assign, object-shorthand, one-var, one-var-declaration-per-line, comma-dangle, consistent-return, class-methods-use-this, new-parens */ +import $ from 'jquery'; import 'cropper'; import _ from 'underscore'; diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js index a811781853b..0af34657d72 100644 --- a/app/assets/javascripts/profile/profile.js +++ b/app/assets/javascripts/profile/profile.js @@ -1,5 +1,6 @@ /* eslint-disable comma-dangle, no-unused-vars, class-methods-use-this, quotes, consistent-return, func-names, prefer-arrow-callback, space-before-function-paren, max-len */ -import Cookies from 'js-cookie'; + +import $ from 'jquery'; import axios from '~/lib/utils/axios_utils'; import { __ } from '~/locale'; import flash from '../flash'; @@ -8,7 +9,6 @@ export default class Profile { constructor({ form } = {}) { this.onSubmitForm = this.onSubmitForm.bind(this); this.form = form || $('.edit-user'); - this.newRepoActivated = Cookies.get('new_repo'); this.setRepoRadio(); this.bindEvents(); this.initAvatarGlCrop(); @@ -21,21 +21,28 @@ export default class Profile { modalCrop: '.modal-profile-crop', pickImageEl: '.js-choose-user-avatar-button', uploadImageBtn: '.js-upload-user-avatar', - modalCropImg: '.modal-profile-crop-image' + modalCropImg: '.modal-profile-crop-image', }; - this.avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data('glcrop'); + this.avatarGlCrop = $('.js-user-avatar-input') + .glCrop(cropOpts) + .data('glcrop'); } bindEvents() { - $('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm); - $('input[name="user[multi_file]"]').on('change', this.setNewRepoCookie); + $('.js-preferences-form').on( + 'change.preference', + 'input[type=radio]', + this.submitForm, + ); $('#user_notification_email').on('change', this.submitForm); $('#user_notified_of_own_activity').on('change', this.submitForm); this.form.on('submit', this.onSubmitForm); } submitForm() { - return $(this).parents('form').submit(); + return $(this) + .parents('form') + .submit(); } onSubmitForm(e) { @@ -57,21 +64,13 @@ export default class Profile { url: this.form.attr('action'), data: formData, }) - .then(({ data }) => flash(data.message, 'notice')) - .then(() => { - window.scrollTo(0, 0); - // Enable submit button after requests ends - self.form.find(':input[disabled]').enable(); - }) - .catch(error => flash(error.message)); - } - - setNewRepoCookie() { - if (this.value === 'off') { - Cookies.remove('new_repo'); - } else { - Cookies.set('new_repo', true, { expires_in: 365 }); - } + .then(({ data }) => flash(data.message, 'notice')) + .then(() => { + window.scrollTo(0, 0); + // Enable submit button after requests ends + self.form.find(':input[disabled]').enable(); + }) + .catch(error => flash(error.message)); } setRepoRadio() { diff --git a/app/assets/javascripts/project_edit.js b/app/assets/javascripts/project_edit.js index 7572fec15e0..47bf2226781 100644 --- a/app/assets/javascripts/project_edit.js +++ b/app/assets/javascripts/project_edit.js @@ -1,3 +1,5 @@ +import $ from 'jquery'; + export default function setupProjectEdit() { const $transferForm = $('.js-project-transfer-form'); const $selectNamespace = $transferForm.find('select.select2'); diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js index 4fd639cce8e..4c4acd487f8 100644 --- a/app/assets/javascripts/project_find_file.js +++ b/app/assets/javascripts/project_find_file.js @@ -1,5 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, consistent-return, one-var, one-var-declaration-per-line, no-cond-assign, max-len, object-shorthand, no-param-reassign, comma-dangle, prefer-template, no-unused-vars, no-return-assign */ +import $ from 'jquery'; import fuzzaldrinPlus from 'fuzzaldrin-plus'; import axios from '~/lib/utils/axios_utils'; import flash from '~/flash'; diff --git a/app/assets/javascripts/project_fork.js b/app/assets/javascripts/project_fork.js index 65d46fa9a73..6fedd94a6a9 100644 --- a/app/assets/javascripts/project_fork.js +++ b/app/assets/javascripts/project_fork.js @@ -1,3 +1,5 @@ +import $ from 'jquery'; + export default () => { $('.js-fork-thumbnail').on('click', function forkThumbnailClicked() { if ($(this).hasClass('disabled')) return false; diff --git a/app/assets/javascripts/project_label_subscription.js b/app/assets/javascripts/project_label_subscription.js index 64b7dd540f9..f31beb4dc78 100644 --- a/app/assets/javascripts/project_label_subscription.js +++ b/app/assets/javascripts/project_label_subscription.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import { __ } from './locale'; import axios from './lib/utils/axios_utils'; import flash from './flash'; diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js index 412aca7bfed..cb2e6855d1d 100644 --- a/app/assets/javascripts/project_select.js +++ b/app/assets/javascripts/project_select.js @@ -1,4 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-var, comma-dangle, object-shorthand, one-var, one-var-declaration-per-line, no-else-return, quotes, max-len */ + +import $ from 'jquery'; import Api from './api'; import ProjectSelectComboButton from './project_select_combo_button'; diff --git a/app/assets/javascripts/project_select_combo_button.js b/app/assets/javascripts/project_select_combo_button.js index 99cea683d9a..9b404896e86 100644 --- a/app/assets/javascripts/project_select_combo_button.js +++ b/app/assets/javascripts/project_select_combo_button.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import AccessorUtilities from './lib/utils/accessor'; export default class ProjectSelectComboButton { diff --git a/app/assets/javascripts/project_visibility.js b/app/assets/javascripts/project_visibility.js index c3f5e8cb907..7c95c71e239 100644 --- a/app/assets/javascripts/project_visibility.js +++ b/app/assets/javascripts/project_visibility.js @@ -1,3 +1,5 @@ +import $ from 'jquery'; + function setVisibilityOptions(namespaceSelector) { if (!namespaceSelector || !('selectedIndex' in namespaceSelector)) { return; diff --git a/app/assets/javascripts/projects/project_import_gitlab_project.js b/app/assets/javascripts/projects/project_import_gitlab_project.js index d2c7d77bb2d..4e20fce1460 100644 --- a/app/assets/javascripts/projects/project_import_gitlab_project.js +++ b/app/assets/javascripts/projects/project_import_gitlab_project.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import { getParameterValues } from '../lib/utils/url_utility'; export default () => { diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js index 8da37d14f0b..93603dfc14d 100644 --- a/app/assets/javascripts/projects/project_new.js +++ b/app/assets/javascripts/projects/project_new.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import { addSelectOnFocusBehaviour } from '../lib/utils/common_utils'; let hasUserDefinedProjectPath = false; diff --git a/app/assets/javascripts/projects_dropdown/index.js b/app/assets/javascripts/projects_dropdown/index.js index e78ebce2923..e1ca70c51a6 100644 --- a/app/assets/javascripts/projects_dropdown/index.js +++ b/app/assets/javascripts/projects_dropdown/index.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import Vue from 'vue'; import Translate from '../vue_shared/translate'; diff --git a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js index e8126ac573d..0a60f4845b2 100644 --- a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js +++ b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js @@ -1,3 +1,6 @@ +import $ from 'jquery'; +import _ from 'underscore'; +import { s__, n__, sprintf } from '~/locale'; import axios from '../lib/utils/axios_utils'; import PANEL_STATE from './constants'; import { backOff } from '../lib/utils/common_utils'; @@ -20,6 +23,7 @@ export default class PrometheusMetrics { this.$missingEnvVarMetricsList = this.$missingEnvVarPanel.find('.js-missing-var-metrics-list'); this.activeMetricsEndpoint = this.$monitoredMetricsPanel.data('activeMetrics'); + this.helpMetricsPath = this.$monitoredMetricsPanel.data('metrics-help-path'); this.$panelToggle.on('click', e => this.handlePanelToggle(e)); } @@ -59,23 +63,39 @@ export default class PrometheusMetrics { populateActiveMetrics(metrics) { let totalMonitoredMetrics = 0; let totalMissingEnvVarMetrics = 0; + let totalExporters = 0; metrics.forEach((metric) => { - this.$monitoredMetricsList.append(`<li>${metric.group}<span class="badge">${metric.active_metrics}</span></li>`); - totalMonitoredMetrics += metric.active_metrics; - if (metric.metrics_missing_requirements > 0) { - this.$missingEnvVarMetricsList.append(`<li>${metric.group}</li>`); - totalMissingEnvVarMetrics += 1; + if (metric.active_metrics > 0) { + totalExporters += 1; + this.$monitoredMetricsList.append(`<li>${_.escape(metric.group)}<span class="badge">${_.escape(metric.active_metrics)}</span></li>`); + totalMonitoredMetrics += metric.active_metrics; + if (metric.metrics_missing_requirements > 0) { + this.$missingEnvVarMetricsList.append(`<li>${_.escape(metric.group)}</li>`); + totalMissingEnvVarMetrics += 1; + } } }); - this.$monitoredMetricsCount.text(totalMonitoredMetrics); - this.showMonitoringMetricsPanelState(PANEL_STATE.LIST); + if (totalMonitoredMetrics === 0) { + const emptyCommonMetricsText = sprintf(s__('PrometheusService|<p class="text-tertiary">No <a href="%{docsUrl}">common metrics</a> were found</p>'), { + docsUrl: this.helpMetricsPath, + }, false); + this.$monitoredMetricsEmpty.empty(); + this.$monitoredMetricsEmpty.append(emptyCommonMetricsText); + this.showMonitoringMetricsPanelState(PANEL_STATE.EMPTY); + } else { + const metricsCountText = sprintf(s__('PrometheusService|%{exporters} with %{metrics} were found'), { + exporters: n__('%d exporter', '%d exporters', totalExporters), + metrics: n__('%d metric', '%d metrics', totalMonitoredMetrics), + }); + this.$monitoredMetricsCount.text(metricsCountText); + this.showMonitoringMetricsPanelState(PANEL_STATE.LIST); - if (totalMissingEnvVarMetrics > 0) { - this.$missingEnvVarPanel.removeClass('hidden'); - this.$missingEnvVarPanel.find('.flash-container').off('click'); - this.$missingEnvVarMetricCount.text(totalMissingEnvVarMetrics); + if (totalMissingEnvVarMetrics > 0) { + this.$missingEnvVarPanel.removeClass('hidden'); + this.$missingEnvVarMetricCount.text(totalMissingEnvVarMetrics); + } } } @@ -97,15 +117,15 @@ export default class PrometheusMetrics { }) .catch(stop); }) - .then((res) => { - if (res && res.data && res.data.length) { - this.populateActiveMetrics(res.data); - } else { + .then((res) => { + if (res && res.data && res.data.length) { + this.populateActiveMetrics(res.data); + } else { + this.showMonitoringMetricsPanelState(PANEL_STATE.EMPTY); + } + }) + .catch(() => { this.showMonitoringMetricsPanelState(PANEL_STATE.EMPTY); - } - }) - .catch(() => { - this.showMonitoringMetricsPanelState(PANEL_STATE.EMPTY); - }); + }); } } diff --git a/app/assets/javascripts/protected_branches/index.js b/app/assets/javascripts/protected_branches/index.js deleted file mode 100644 index c9e7af127d2..00000000000 --- a/app/assets/javascripts/protected_branches/index.js +++ /dev/null @@ -1,9 +0,0 @@ -/* eslint-disable no-unused-vars */ - -import ProtectedBranchCreate from './protected_branch_create'; -import ProtectedBranchEditList from './protected_branch_edit_list'; - -$(() => { - const protectedBranchCreate = new ProtectedBranchCreate(); - const protectedBranchEditList = new ProtectedBranchEditList(); -}); diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js b/app/assets/javascripts/protected_branches/protected_branch_create.js index 8fc87633e18..7c61c070a35 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_create.js +++ b/app/assets/javascripts/protected_branches/protected_branch_create.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import _ from 'underscore'; import ProtectedBranchAccessDropdown from './protected_branch_access_dropdown'; import CreateItemDropdown from '../create_item_dropdown'; diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit_list.js b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js index b40d3827c30..10253c0febc 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_edit_list.js +++ b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js @@ -1,5 +1,6 @@ /* eslint-disable no-new */ +import $ from 'jquery'; import ProtectedBranchEdit from './protected_branch_edit'; export default class ProtectedBranchEditList { diff --git a/app/assets/javascripts/protected_tags/protected_tag_create.js b/app/assets/javascripts/protected_tags/protected_tag_create.js index 2f94ffe2507..2f8116df0d2 100644 --- a/app/assets/javascripts/protected_tags/protected_tag_create.js +++ b/app/assets/javascripts/protected_tags/protected_tag_create.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import ProtectedTagAccessDropdown from './protected_tag_access_dropdown'; import CreateItemDropdown from '../create_item_dropdown'; diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit_list.js b/app/assets/javascripts/protected_tags/protected_tag_edit_list.js index bd9fc872266..b35bf4d4606 100644 --- a/app/assets/javascripts/protected_tags/protected_tag_edit_list.js +++ b/app/assets/javascripts/protected_tags/protected_tag_edit_list.js @@ -1,5 +1,6 @@ /* eslint-disable no-new */ +import $ from 'jquery'; import ProtectedTagEdit from './protected_tag_edit'; export default class ProtectedTagEditList { diff --git a/app/assets/javascripts/ref_select_dropdown.js b/app/assets/javascripts/ref_select_dropdown.js index 56c25a35e6d..95c5cf7b345 100644 --- a/app/assets/javascripts/ref_select_dropdown.js +++ b/app/assets/javascripts/ref_select_dropdown.js @@ -1,3 +1,5 @@ +import $ from 'jquery'; + class RefSelectDropdown { constructor($dropdownButton, availableRefs) { const availableRefsValue = availableRefs || JSON.parse(document.getElementById('availableRefs').innerHTML); diff --git a/app/assets/javascripts/registry/components/collapsible_container.vue b/app/assets/javascripts/registry/components/collapsible_container.vue index b4906ba4ee5..a03180e80e6 100644 --- a/app/assets/javascripts/registry/components/collapsible_container.vue +++ b/app/assets/javascripts/registry/components/collapsible_container.vue @@ -86,6 +86,7 @@ v-if="repo.location" :text="clipboardText" :title="repo.location" + css-class="btn-default btn-transparent btn-clipboard" /> <div class="controls hidden-xs pull-right"> diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue index bef850eddc0..ee4eb3581f3 100644 --- a/app/assets/javascripts/registry/components/table_registry.vue +++ b/app/assets/javascripts/registry/components/table_registry.vue @@ -90,6 +90,7 @@ v-if="item.location" :title="item.location" :text="clipboardText(item.location)" + css-class="btn-default btn-transparent btn-clipboard" /> </td> <td> diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js index 8d3cc849f81..2088a49590a 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -1,5 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-vars, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, object-shorthand, comma-dangle, no-else-return, no-param-reassign, max-len */ +import $ from 'jquery'; import _ from 'underscore'; import Cookies from 'js-cookie'; import flash from './flash'; diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js index fdfa4f28aba..7dd3e9858c6 100644 --- a/app/assets/javascripts/search_autocomplete.js +++ b/app/assets/javascripts/search_autocomplete.js @@ -1,4 +1,6 @@ /* eslint-disable no-return-assign, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-unused-vars, no-cond-assign, consistent-return, object-shorthand, prefer-arrow-callback, func-names, space-before-function-paren, prefer-template, quotes, class-methods-use-this, no-sequences, wrap-iife, no-lonely-if, no-else-return, no-param-reassign, vars-on-top, max-len */ + +import $ from 'jquery'; import axios from './lib/utils/axios_utils'; import DropdownUtils from './filtered_search/dropdown_utils'; import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from './lib/utils/common_utils'; diff --git a/app/assets/javascripts/settings_panels.js b/app/assets/javascripts/settings_panels.js index d0e4f533d8a..eecde4550f9 100644 --- a/app/assets/javascripts/settings_panels.js +++ b/app/assets/javascripts/settings_panels.js @@ -1,3 +1,5 @@ +import $ from 'jquery'; + function expandSection($section) { $section.find('.js-settings-toggle').text('Collapse'); $section.find('.settings-content').off('scroll.expandSection').scrollTop(0); diff --git a/app/assets/javascripts/shared/milestones/form.js b/app/assets/javascripts/shared/milestones/form.js index db466f722c4..2f974d6ff9d 100644 --- a/app/assets/javascripts/shared/milestones/form.js +++ b/app/assets/javascripts/shared/milestones/form.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import ZenMode from '../../zen_mode'; import DueDateSelectors from '../../due_date_select'; import GLForm from '../../gl_form'; diff --git a/app/assets/javascripts/shared/sessions/u2f.js b/app/assets/javascripts/shared/sessions/u2f.js index 1d075f7e872..6ae9faf1dde 100644 --- a/app/assets/javascripts/shared/sessions/u2f.js +++ b/app/assets/javascripts/shared/sessions/u2f.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import U2FAuthenticate from '../../u2f/authenticate'; export default () => { diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js index c5dddd001bb..e31e067033f 100644 --- a/app/assets/javascripts/shortcuts.js +++ b/app/assets/javascripts/shortcuts.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import Cookies from 'js-cookie'; import Mousetrap from 'mousetrap'; import axios from './lib/utils/axios_utils'; diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js index 14545824e74..193788f754f 100644 --- a/app/assets/javascripts/shortcuts_issuable.js +++ b/app/assets/javascripts/shortcuts_issuable.js @@ -1,8 +1,9 @@ +import $ from 'jquery'; import Mousetrap from 'mousetrap'; import _ from 'underscore'; import Sidebar from './right_sidebar'; import Shortcuts from './shortcuts'; -import { CopyAsGFM } from './behaviors/copy_as_gfm'; +import { CopyAsGFM } from './behaviors/markdown/copy_as_gfm'; export default class ShortcutsIssuable extends Shortcuts { constructor(isMergeRequest) { diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_title.js b/app/assets/javascripts/sidebar/components/assignees/assignee_title.js deleted file mode 100644 index 129ba2e4e89..00000000000 --- a/app/assets/javascripts/sidebar/components/assignees/assignee_title.js +++ /dev/null @@ -1,59 +0,0 @@ -export default { - name: 'AssigneeTitle', - props: { - loading: { - type: Boolean, - required: false, - default: false, - }, - numberOfAssignees: { - type: Number, - required: true, - }, - editable: { - type: Boolean, - required: true, - }, - showToggle: { - type: Boolean, - required: false, - default: false, - }, - }, - computed: { - assigneeTitle() { - const assignees = this.numberOfAssignees; - return assignees > 1 ? `${assignees} Assignees` : 'Assignee'; - }, - }, - template: ` - <div class="title hide-collapsed"> - {{assigneeTitle}} - <i - v-if="loading" - aria-hidden="true" - class="fa fa-spinner fa-spin block-loading" - /> - <a - v-if="editable" - class="js-sidebar-dropdown-toggle edit-link pull-right" - href="#" - > - {{ __('Edit') }} - </a> - <a - v-if="showToggle" - aria-label="Toggle sidebar" - class="gutter-toggle pull-right js-sidebar-toggle" - href="#" - role="button" - > - <i - aria-hidden="true" - data-hidden="true" - class="fa fa-angle-double-right" - /> - </a> - </div> - `, -}; diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue new file mode 100644 index 00000000000..5eeb2a41bae --- /dev/null +++ b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue @@ -0,0 +1,64 @@ +<script> +export default { + name: 'AssigneeTitle', + props: { + loading: { + type: Boolean, + required: false, + default: false, + }, + numberOfAssignees: { + type: Number, + required: true, + }, + editable: { + type: Boolean, + required: true, + }, + showToggle: { + type: Boolean, + required: false, + default: false, + }, + }, + computed: { + assigneeTitle() { + const assignees = this.numberOfAssignees; + return assignees > 1 ? `${assignees} Assignees` : 'Assignee'; + }, + }, +}; +</script> +<template> + <div class="title hide-collapsed"> + {{ assigneeTitle }} + <i + v-if="loading" + aria-hidden="true" + class="fa fa-spinner fa-spin block-loading" + > + + </i> + <a + v-if="editable" + class="js-sidebar-dropdown-toggle edit-link pull-right" + href="#" + > + {{ __('Edit') }} + </a> + <a + v-if="showToggle" + aria-label="Toggle sidebar" + class="gutter-toggle pull-right js-sidebar-toggle" + href="#" + role="button" + > + <i + aria-hidden="true" + data-hidden="true" + class="fa fa-angle-double-right" + > + </i> + </a> + </div> +</template> diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue index 8269fe1281d..3c6b9c27814 100644 --- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js +++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue @@ -1,16 +1,15 @@ +<script> import Flash from '../../../flash'; -import AssigneeTitle from './assignee_title'; +import AssigneeTitle from './assignee_title.vue'; import Assignees from './assignees.vue'; import Store from '../../stores/sidebar_store'; import eventHub from '../../event_hub'; export default { name: 'SidebarAssignees', - data() { - return { - store: new Store(), - loading: false, - }; + components: { + AssigneeTitle, + Assignees, }, props: { mediator: { @@ -27,9 +26,28 @@ export default { default: false, }, }, - components: { - AssigneeTitle, - Assignees, + data() { + return { + store: new Store(), + loading: false, + }; + }, + created() { + this.removeAssignee = this.store.removeAssignee.bind(this.store); + this.addAssignee = this.store.addAssignee.bind(this.store); + this.removeAllAssignees = this.store.removeAllAssignees.bind(this.store); + + // Get events from glDropdown + eventHub.$on('sidebar.removeAssignee', this.removeAssignee); + eventHub.$on('sidebar.addAssignee', this.addAssignee); + eventHub.$on('sidebar.removeAllAssignees', this.removeAllAssignees); + eventHub.$on('sidebar.saveAssignees', this.saveAssignees); + }, + beforeDestroy() { + eventHub.$off('sidebar.removeAssignee', this.removeAssignee); + eventHub.$off('sidebar.addAssignee', this.addAssignee); + eventHub.$off('sidebar.removeAllAssignees', this.removeAllAssignees); + eventHub.$off('sidebar.saveAssignees', this.saveAssignees); }, methods: { assignSelf() { @@ -54,39 +72,24 @@ export default { }); }, }, - created() { - this.removeAssignee = this.store.removeAssignee.bind(this.store); - this.addAssignee = this.store.addAssignee.bind(this.store); - this.removeAllAssignees = this.store.removeAllAssignees.bind(this.store); - - // Get events from glDropdown - eventHub.$on('sidebar.removeAssignee', this.removeAssignee); - eventHub.$on('sidebar.addAssignee', this.addAssignee); - eventHub.$on('sidebar.removeAllAssignees', this.removeAllAssignees); - eventHub.$on('sidebar.saveAssignees', this.saveAssignees); - }, - beforeDestroy() { - eventHub.$off('sidebar.removeAssignee', this.removeAssignee); - eventHub.$off('sidebar.addAssignee', this.addAssignee); - eventHub.$off('sidebar.removeAllAssignees', this.removeAllAssignees); - eventHub.$off('sidebar.saveAssignees', this.saveAssignees); - }, - template: ` - <div> - <assignee-title - :number-of-assignees="store.assignees.length" - :loading="loading || store.isFetching.assignees" - :editable="store.editable" - :show-toggle="!signedIn" - /> - <assignees - v-if="!store.isFetching.assignees" - class="value" - :root-path="store.rootPath" - :users="store.assignees" - :editable="store.editable" - @assign-self="assignSelf" - /> - </div> - `, }; +</script> + +<template> + <div> + <assignee-title + :number-of-assignees="store.assignees.length" + :loading="loading || store.isFetching.assignees" + :editable="store.editable" + :show-toggle="!signedIn" + /> + <assignees + v-if="!store.isFetching.assignees" + class="value" + :root-path="store.rootPath" + :users="store.assignees" + :editable="store.editable" + @assign-self="assignSelf" + /> + </div> +</template> diff --git a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.js b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.js deleted file mode 100644 index a9fbc7f1a2f..00000000000 --- a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.js +++ /dev/null @@ -1,96 +0,0 @@ -import stopwatchSvg from 'icons/_icon_stopwatch.svg'; -import { abbreviateTime } from '../../../lib/utils/pretty_time'; - -export default { - name: 'time-tracking-collapsed-state', - props: { - showComparisonState: { - type: Boolean, - required: true, - }, - showSpentOnlyState: { - type: Boolean, - required: true, - }, - showEstimateOnlyState: { - type: Boolean, - required: true, - }, - showNoTimeTrackingState: { - type: Boolean, - required: true, - }, - timeSpentHumanReadable: { - type: String, - required: false, - default: '', - }, - timeEstimateHumanReadable: { - type: String, - required: false, - default: '', - }, - }, - computed: { - timeSpent() { - return this.abbreviateTime(this.timeSpentHumanReadable); - }, - timeEstimate() { - return this.abbreviateTime(this.timeEstimateHumanReadable); - }, - divClass() { - if (this.showComparisonState) { - return 'compare'; - } else if (this.showEstimateOnlyState) { - return 'estimate-only'; - } else if (this.showSpentOnlyState) { - return 'spend-only'; - } else if (this.showNoTimeTrackingState) { - return 'no-tracking'; - } - - return ''; - }, - spanClass() { - if (this.showComparisonState) { - return ''; - } else if (this.showEstimateOnlyState || this.showSpentOnlyState) { - return 'bold'; - } else if (this.showNoTimeTrackingState) { - return 'no-value'; - } - - return ''; - }, - text() { - if (this.showComparisonState) { - return `${this.timeSpent} / ${this.timeEstimate}`; - } else if (this.showEstimateOnlyState) { - return `-- / ${this.timeEstimate}`; - } else if (this.showSpentOnlyState) { - return `${this.timeSpent} / --`; - } else if (this.showNoTimeTrackingState) { - return 'None'; - } - - return ''; - }, - }, - methods: { - abbreviateTime(timeStr) { - return abbreviateTime(timeStr); - }, - }, - template: ` - <div class="sidebar-collapsed-icon"> - ${stopwatchSvg} - <div class="time-tracking-collapsed-summary"> - <div :class="divClass"> - <span :class="spanClass"> - {{ text }} - </span> - </div> - </div> - </div> - `, -}; diff --git a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue new file mode 100644 index 00000000000..3b86f1145d1 --- /dev/null +++ b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue @@ -0,0 +1,102 @@ +<script> + import icon from '../../../vue_shared/components/icon.vue'; + import { abbreviateTime } from '../../../lib/utils/pretty_time'; + + export default { + name: 'TimeTrackingCollapsedState', + components: { + icon, + }, + props: { + showComparisonState: { + type: Boolean, + required: true, + }, + showSpentOnlyState: { + type: Boolean, + required: true, + }, + showEstimateOnlyState: { + type: Boolean, + required: true, + }, + showNoTimeTrackingState: { + type: Boolean, + required: true, + }, + timeSpentHumanReadable: { + type: String, + required: false, + default: '', + }, + timeEstimateHumanReadable: { + type: String, + required: false, + default: '', + }, + }, + computed: { + timeSpent() { + return this.abbreviateTime(this.timeSpentHumanReadable); + }, + timeEstimate() { + return this.abbreviateTime(this.timeEstimateHumanReadable); + }, + divClass() { + if (this.showComparisonState) { + return 'compare'; + } else if (this.showEstimateOnlyState) { + return 'estimate-only'; + } else if (this.showSpentOnlyState) { + return 'spend-only'; + } else if (this.showNoTimeTrackingState) { + return 'no-tracking'; + } + + return ''; + }, + spanClass() { + if (this.showComparisonState) { + return ''; + } else if (this.showEstimateOnlyState || this.showSpentOnlyState) { + return 'bold'; + } else if (this.showNoTimeTrackingState) { + return 'no-value'; + } + + return ''; + }, + text() { + if (this.showComparisonState) { + return `${this.timeSpent} / ${this.timeEstimate}`; + } else if (this.showEstimateOnlyState) { + return `-- / ${this.timeEstimate}`; + } else if (this.showSpentOnlyState) { + return `${this.timeSpent} / --`; + } else if (this.showNoTimeTrackingState) { + return 'None'; + } + + return ''; + }, + }, + methods: { + abbreviateTime(timeStr) { + return abbreviateTime(timeStr); + }, + }, + }; +</script> + +<template> + <div class="sidebar-collapsed-icon"> + <icon name="timer" /> + <div class="time-tracking-collapsed-summary"> + <div :class="divClass"> + <span :class="spanClass"> + {{ text }} + </span> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.js b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.js index 782e4ba4fad..5626cccc022 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.js +++ b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import _ from 'underscore'; import '~/smart_interval'; 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 230736a56b8..28240468d2c 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue @@ -1,6 +1,6 @@ <script> import timeTrackingHelpState from './help_state'; -import timeTrackingCollapsedState from './collapsed_state'; +import TimeTrackingCollapsedState from './collapsed_state.vue'; import timeTrackingSpentOnlyPane from './spent_only_pane'; import timeTrackingNoTrackingPane from './no_tracking_pane'; import timeTrackingEstimateOnlyPane from './estimate_only_pane'; @@ -11,7 +11,7 @@ import eventHub from '../../event_hub'; export default { name: 'IssuableTimeTracker', components: { - 'time-tracking-collapsed-state': timeTrackingCollapsedState, + TimeTrackingCollapsedState, 'time-tracking-estimate-only-pane': timeTrackingEstimateOnlyPane, 'time-tracking-spent-only-pane': timeTrackingSpentOnlyPane, 'time-tracking-no-tracking-pane': timeTrackingNoTrackingPane, diff --git a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js index b10e2cc60ef..1eadebc7004 100644 --- a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js +++ b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js @@ -1,3 +1,5 @@ +import $ from 'jquery'; + function isValidProjectId(id) { return id > 0; } diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js index 56cc78ca0ca..9f5d852260e 100644 --- a/app/assets/javascripts/sidebar/mount_sidebar.js +++ b/app/assets/javascripts/sidebar/mount_sidebar.js @@ -1,6 +1,7 @@ +import $ from 'jquery'; import Vue from 'vue'; import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking'; -import SidebarAssignees from './components/assignees/sidebar_assignees'; +import SidebarAssignees from './components/assignees/sidebar_assignees.vue'; import ConfidentialIssueSidebar from './components/confidential/confidential_issue_sidebar.vue'; import SidebarMoveIssue from './lib/sidebar_move_issue'; import LockIssueSidebar from './components/lock/lock_issue_sidebar.vue'; diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js index 6142ce6c6a3..1afff0dba38 100644 --- a/app/assets/javascripts/single_file_diff.js +++ b/app/assets/javascripts/single_file_diff.js @@ -1,5 +1,6 @@ /* eslint-disable func-names, prefer-arrow-callback, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, max-len */ +import $ from 'jquery'; import { __ } from './locale'; import axios from './lib/utils/axios_utils'; import createFlash from './flash'; diff --git a/app/assets/javascripts/smart_interval.js b/app/assets/javascripts/smart_interval.js index 8e931995fc6..77ab7c964e6 100644 --- a/app/assets/javascripts/smart_interval.js +++ b/app/assets/javascripts/smart_interval.js @@ -1,3 +1,5 @@ +import $ from 'jquery'; + /** * Instances of SmartInterval extend the functionality of `setInterval`, make it configurable * and controllable by a public API. diff --git a/app/assets/javascripts/snippet/snippet_bundle.js b/app/assets/javascripts/snippet/snippet_bundle.js index ce0fd3f6ff8..dcee17453b8 100644 --- a/app/assets/javascripts/snippet/snippet_bundle.js +++ b/app/assets/javascripts/snippet/snippet_bundle.js @@ -1,5 +1,7 @@ /* global ace */ +import $ from 'jquery'; + export default () => { const editor = ace.edit('editor'); diff --git a/app/assets/javascripts/sortable/sortable_config.js b/app/assets/javascripts/sortable/sortable_config.js new file mode 100644 index 00000000000..43ef5d66422 --- /dev/null +++ b/app/assets/javascripts/sortable/sortable_config.js @@ -0,0 +1,7 @@ +export default { + animation: 200, + forceFallback: true, + fallbackClass: 'is-dragging', + fallbackOnBody: true, + ghostClass: 'is-ghost', +}; diff --git a/app/assets/javascripts/star.js b/app/assets/javascripts/star.js index 3deb629d5f2..f5a7fdae5d7 100644 --- a/app/assets/javascripts/star.js +++ b/app/assets/javascripts/star.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import Flash from './flash'; import { __, s__ } from './locale'; import { spriteIcon } from './lib/utils/common_utils'; diff --git a/app/assets/javascripts/subscription_select.js b/app/assets/javascripts/subscription_select.js index 3ed064f87a9..ebe1c6dd02d 100644 --- a/app/assets/javascripts/subscription_select.js +++ b/app/assets/javascripts/subscription_select.js @@ -1,3 +1,5 @@ +import $ from 'jquery'; + export default function subscriptionSelect() { $('.js-subscription-event').each((i, element) => { const fieldName = $(element).data('fieldName'); diff --git a/app/assets/javascripts/syntax_highlight.js b/app/assets/javascripts/syntax_highlight.js index 62bdef76c55..f52990ba232 100644 --- a/app/assets/javascripts/syntax_highlight.js +++ b/app/assets/javascripts/syntax_highlight.js @@ -1,5 +1,7 @@ /* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-else-return, prefer-arrow-callback, max-len */ +import $ from 'jquery'; + // Syntax Highlighter // // Applies a syntax highlighting color scheme CSS class to any element with the diff --git a/app/assets/javascripts/task_list.js b/app/assets/javascripts/task_list.js index 8fa78b636f8..48782e63b9b 100644 --- a/app/assets/javascripts/task_list.js +++ b/app/assets/javascripts/task_list.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import 'deckar01-task_list'; import axios from './lib/utils/axios_utils'; import Flash from './flash'; diff --git a/app/assets/javascripts/templates/issuable_template_selector.js b/app/assets/javascripts/templates/issuable_template_selector.js index b5b64f44a11..6fea03af46a 100644 --- a/app/assets/javascripts/templates/issuable_template_selector.js +++ b/app/assets/javascripts/templates/issuable_template_selector.js @@ -1,5 +1,6 @@ /* eslint-disable no-useless-return, max-len */ +import $ from 'jquery'; import Api from '../api'; import TemplateSelector from '../blob/template_selector'; diff --git a/app/assets/javascripts/templates/issuable_template_selectors.js b/app/assets/javascripts/templates/issuable_template_selectors.js index 66d868c5839..50e58ec5c46 100644 --- a/app/assets/javascripts/templates/issuable_template_selectors.js +++ b/app/assets/javascripts/templates/issuable_template_selectors.js @@ -1,4 +1,6 @@ /* eslint-disable no-new, class-methods-use-this */ + +import $ from 'jquery'; import IssuableTemplateSelector from './issuable_template_selector'; export default class IssuableTemplateSelectors { diff --git a/app/assets/javascripts/terminal/terminal_bundle.js b/app/assets/javascripts/terminal/index.js index 134522ef961..1a75e072c4e 100644 --- a/app/assets/javascripts/terminal/terminal_bundle.js +++ b/app/assets/javascripts/terminal/index.js @@ -6,4 +6,4 @@ import './terminal'; window.Terminal = Terminal; -$(() => new gl.Terminal({ selector: '#terminal' })); +export default () => new gl.Terminal({ selector: '#terminal' }); diff --git a/app/assets/javascripts/terminal/terminal.js b/app/assets/javascripts/terminal/terminal.js index 6b9422b1816..caffcddf3b0 100644 --- a/app/assets/javascripts/terminal/terminal.js +++ b/app/assets/javascripts/terminal/terminal.js @@ -1,13 +1,21 @@ /* global Terminal */ +import $ from 'jquery'; + (() => { class GLTerminal { constructor(options) { this.options = options || {}; - this.options.cursorBlink = options.cursorBlink || true; - this.options.screenKeys = options.screenKeys || true; + if (!Object.prototype.hasOwnProperty.call(this.options, 'cursorBlink')) { + this.options.cursorBlink = true; + } + + if (!Object.prototype.hasOwnProperty.call(this.options, 'screenKeys')) { + this.options.screenKeys = true; + } + this.container = document.querySelector(options.selector); this.setSocketUrl(); diff --git a/app/assets/javascripts/test.js b/app/assets/javascripts/test.js deleted file mode 100644 index c4c7918a68f..00000000000 --- a/app/assets/javascripts/test.js +++ /dev/null @@ -1 +0,0 @@ -$.fx.off = true; diff --git a/app/assets/javascripts/tree.js b/app/assets/javascripts/tree.js index 1a0b2c0415b..afbb958d058 100644 --- a/app/assets/javascripts/tree.js +++ b/app/assets/javascripts/tree.js @@ -1,4 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, quotes, consistent-return, no-var, one-var, one-var-declaration-per-line, no-else-return, prefer-arrow-callback, class-methods-use-this */ + +import $ from 'jquery'; import { visitUrl } from './lib/utils/url_utility'; export default class TreeView { diff --git a/app/assets/javascripts/u2f/authenticate.js b/app/assets/javascripts/u2f/authenticate.js index fd42f9c3baa..96af6d2fcca 100644 --- a/app/assets/javascripts/u2f/authenticate.js +++ b/app/assets/javascripts/u2f/authenticate.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import _ from 'underscore'; import importU2FLibrary from './util'; import U2FError from './error'; diff --git a/app/assets/javascripts/u2f/register.js b/app/assets/javascripts/u2f/register.js index 869fac658e8..01e259a741d 100644 --- a/app/assets/javascripts/u2f/register.js +++ b/app/assets/javascripts/u2f/register.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import _ from 'underscore'; import importU2FLibrary from './util'; import U2FError from './error'; diff --git a/app/assets/javascripts/ui_development_kit.js b/app/assets/javascripts/ui_development_kit.js index 78dda172ee6..9b242ea779d 100644 --- a/app/assets/javascripts/ui_development_kit.js +++ b/app/assets/javascripts/ui_development_kit.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import Api from './api'; export default () => { diff --git a/app/assets/javascripts/user_callout.js b/app/assets/javascripts/user_callout.js index a783122d500..97d5cf96bcb 100644 --- a/app/assets/javascripts/user_callout.js +++ b/app/assets/javascripts/user_callout.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import Cookies from 'js-cookie'; export default class UserCallout { diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index 3385aba0279..f3b961eb109 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -1,6 +1,8 @@ /* eslint-disable func-names, space-before-function-paren, one-var, no-var, prefer-rest-params, wrap-iife, quotes, max-len, one-var-declaration-per-line, vars-on-top, prefer-arrow-callback, consistent-return, comma-dangle, object-shorthand, no-shadow, no-unused-vars, no-else-return, no-self-compare, prefer-template, no-unused-expressions, no-lonely-if, yoda, prefer-spread, no-void, camelcase, no-param-reassign */ /* global Issuable */ /* global emitSidebarEvent */ + +import $ from 'jquery'; import _ from 'underscore'; import axios from './lib/utils/axios_utils'; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue new file mode 100644 index 00000000000..7bef2e97349 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue @@ -0,0 +1,144 @@ +<script> +import timeagoMixin from '../../vue_shared/mixins/timeago'; +import tooltip from '../../vue_shared/directives/tooltip'; +import LoadingButton from '../../vue_shared/components/loading_button.vue'; +import { visitUrl } from '../../lib/utils/url_utility'; +import createFlash from '../../flash'; +import MemoryUsage from './memory_usage.vue'; +import StatusIcon from './mr_widget_status_icon.vue'; +import MRWidgetService from '../services/mr_widget_service'; + +export default { + name: 'Deployment', + components: { + LoadingButton, + MemoryUsage, + StatusIcon, + }, + directives: { + tooltip, + }, + mixins: [ + timeagoMixin, + ], + props: { + deployment: { + type: Object, + required: true, + }, + }, + data() { + return { + isStopping: false, + }; + }, + computed: { + deployTimeago() { + return this.timeFormated(this.deployment.deployed_at); + }, + hasExternalUrls() { + return !!(this.deployment.external_url && this.deployment.external_url_formatted); + }, + hasDeploymentTime() { + return !!(this.deployment.deployed_at && this.deployment.deployed_at_formatted); + }, + hasDeploymentMeta() { + return !!(this.deployment.url && this.deployment.name); + }, + hasMetrics() { + return !!(this.deployment.metrics_url); + }, + }, + methods: { + stopEnvironment() { + const msg = 'Are you sure you want to stop this environment?'; + const isConfirmed = confirm(msg); // eslint-disable-line + + if (isConfirmed) { + this.isStopping = true; + + MRWidgetService.stopEnvironment(this.deployment.stop_url) + .then(res => res.data) + .then((data) => { + if (data.redirect_url) { + visitUrl(data.redirect_url); + } + + this.isStopping = false; + }) + .catch(() => { + createFlash('Something went wrong while stopping this environment. Please try again.'); + this.isStopping = false; + }); + } + }, + }, +}; +</script> + +<template> + <div class="mr-widget-heading deploy-heading"> + <div class="ci-widget media"> + <div class="ci-status-icon ci-status-icon-success"> + <span class="js-icon-link icon-link"> + <status-icon status="success" /> + </span> + </div> + <div class="media-body"> + <div class="deploy-body"> + <template v-if="hasDeploymentMeta"> + <span> + Deployed to + </span> + <a + :href="deployment.url" + target="_blank" + rel="noopener noreferrer nofollow" + class="deploy-link js-deploy-meta" + > + {{ deployment.name }} + </a> + </template> + <template v-if="hasExternalUrls"> + <span> + on + </span> + <a + :href="deployment.external_url" + target="_blank" + rel="noopener noreferrer nofollow" + class="deploy-link js-deploy-url" + > + <i + class="fa fa-external-link" + aria-hidden="true" + > + </i> + {{ deployment.external_url_formatted }} + </a> + </template> + <span + v-if="hasDeploymentTime" + v-tooltip + :title="deployment.deployed_at_formatted" + class="js-deploy-time" + > + {{ deployTimeago }} + </span> + <loading-button + v-if="deployment.stop_url" + container-class="btn btn-default btn-xs prepend-left-default" + label="Stop environment" + :loading="isStopping" + @click="stopEnvironment" + /> + </div> + <memory-usage + v-if="hasMetrics" + :metrics-url="deployment.metrics_url" + :metrics-monitoring-url="deployment.metrics_monitoring_url" + /> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js b/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue index 69e70ba1dd6..a16f9055a6d 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue @@ -1,11 +1,15 @@ +<script> import statusCodes from '../../lib/utils/http_status'; import { bytesToMiB } from '../../lib/utils/number_utils'; import { backOff } from '../../lib/utils/common_utils'; -import MemoryGraph from '../../vue_shared/components/memory_graph'; +import MemoryGraph from '../../vue_shared/components/memory_graph.vue'; import MRWidgetService from '../services/mr_widget_service'; export default { name: 'MemoryUsage', + components: { + MemoryGraph, + }, props: { metricsUrl: { type: String, @@ -28,9 +32,6 @@ export default { backOffRequestCounter: 0, }; }, - components: { - 'mr-memory-graph': MemoryGraph, - }, computed: { shouldShowLoading() { return this.loadingMetrics && !this.hasMetrics && !this.loadFailed; @@ -57,6 +58,10 @@ export default { return 'unchanged'; }, }, + mounted() { + this.loadingMetrics = true; + this.loadMetrics(); + }, methods: { getMegabytes(bytesString) { const valueInBytes = Number(bytesString).toFixed(2); @@ -114,40 +119,42 @@ export default { }); }, }, - mounted() { - this.loadingMetrics = true; - this.loadMetrics(); - }, - 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" />Loading deployment statistics - </p> - <p - v-if="shouldShowMemoryGraph" - class="usage-info js-usage-info"> - <a :href="metricsMonitoringUrl">Memory</a> usage <b>{{memoryChangeType}}</b> from {{memoryFrom}}MB to {{memoryTo}}MB - </p> - <p - v-if="shouldShowLoadFailure" - class="usage-info js-usage-info usage-info-failed"> - Failed to load deployment statistics - </p> - <p - v-if="shouldShowMetricsUnavailable" - class="usage-info js-usage-info usage-info-unavailable"> - Deployment statistics are not available currently - </p> - <mr-memory-graph - v-if="shouldShowMemoryGraph" - :metrics="memoryMetrics" - :deploymentTime="deploymentTime" - height="25" - width="100" /> - </div> - `, }; +</script> + +<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>Loading deployment statistics + </p> + <p + v-if="shouldShowMemoryGraph" + class="usage-info js-usage-info"> + <a + :href="metricsMonitoringUrl" + >Memory</a> usage <b>{{ memoryChangeType }}</b> from {{ memoryFrom }}MB to {{ memoryTo }}MB + </p> + <p + v-if="shouldShowLoadFailure" + class="usage-info js-usage-info usage-info-failed"> + Failed to load deployment statistics + </p> + <p + v-if="shouldShowMetricsUnavailable" + class="usage-info js-usage-info usage-info-unavailable"> + Deployment statistics are not available currently + </p> + <memory-graph + v-if="shouldShowMemoryGraph" + :metrics="memoryMetrics" + :deployment-time="deploymentTime" + height="25" + width="100" + /> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js deleted file mode 100644 index d174a900f63..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js +++ /dev/null @@ -1,113 +0,0 @@ -import { getTimeago } from '~/lib/utils/datetime_utility'; -import { visitUrl } from '../../lib/utils/url_utility'; -import Flash from '../../flash'; -import MemoryUsage from './mr_widget_memory_usage'; -import StatusIcon from './mr_widget_status_icon.vue'; -import MRWidgetService from '../services/mr_widget_service'; - -export default { - name: 'MRWidgetDeployment', - props: { - mr: { type: Object, required: true }, - service: { type: Object, required: true }, - }, - components: { - 'mr-widget-memory-usage': MemoryUsage, - 'status-icon': StatusIcon, - }, - methods: { - formatDate(date) { - return getTimeago().format(date); - }, - hasExternalUrls(deployment = {}) { - return deployment.external_url && deployment.external_url_formatted; - }, - hasDeploymentTime(deployment = {}) { - return deployment.deployed_at && deployment.deployed_at_formatted; - }, - hasDeploymentMeta(deployment = {}) { - return deployment.url && deployment.name; - }, - stopEnvironment(deployment) { - const msg = 'Are you sure you want to stop this environment?'; - const isConfirmed = confirm(msg); // eslint-disable-line - - if (isConfirmed) { - MRWidgetService.stopEnvironment(deployment.stop_url) - .then(res => res.data) - .then((data) => { - if (data.redirect_url) { - visitUrl(data.redirect_url); - } - }) - .catch(() => { - new Flash('Something went wrong while stopping this environment. Please try again.'); // eslint-disable-line - }); - } - }, - }, - template: ` - <div class="mr-widget-heading deploy-heading"> - <div v-for="deployment in mr.deployments"> - <div class="ci-widget media"> - <div class="ci-status-icon ci-status-icon-success"> - <span class="js-icon-link icon-link"> - <status-icon status="success" /> - </span> - </div> - <div class="media-body space-children"> - <span> - <span - v-if="hasDeploymentMeta(deployment)"> - Deployed to - </span> - <a - v-if="hasDeploymentMeta(deployment)" - :href="deployment.url" - target="_blank" - rel="noopener noreferrer nofollow" - class="js-deploy-meta inline"> - {{deployment.name}} - </a> - <span - v-if="hasExternalUrls(deployment)"> - on - </span> - <a - v-if="hasExternalUrls(deployment)" - :href="deployment.external_url" - target="_blank" - rel="noopener noreferrer nofollow" - class="js-deploy-url inline"> - <i - class="fa fa-external-link" - aria-hidden="true" /> - {{deployment.external_url_formatted}} - </a> - <span - v-if="hasDeploymentTime(deployment)" - :data-title="deployment.deployed_at_formatted" - class="js-deploy-time" - data-toggle="tooltip" - data-placement="top"> - {{formatDate(deployment.deployed_at)}} - </span> - </span> - <button - type="button" - v-if="deployment.stop_url" - @click="stopEnvironment(deployment)" - class="btn btn-default btn-xs"> - Stop environment - </button> - <mr-widget-memory-usage - v-if="deployment.metrics_url" - :metrics-url="deployment.metrics_url" - :metrics-monitoring-url="deployment.metrics_monitoring_url" - /> - </div> - </div> - </div> - </div> - `, -}; 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 18a3787857d..3d886e7d628 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 @@ -67,6 +67,7 @@ <clipboard-button :text="branchNameClipboardData" :title="__('Copy branch name to clipboard')" + css-class="btn-default btn-transparent btn-clipboard" /> {{ s__("mrWidget|into") }} diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_maintainer_edit.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_maintainer_edit.vue new file mode 100644 index 00000000000..f0298f732ea --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_maintainer_edit.vue @@ -0,0 +1,20 @@ +<script> + export default { + name: 'MRWidgetMaintainerEdit', + props: { + maintainerEditAllowed: { + type: Boolean, + default: false, + required: false, + }, + }, + }; +</script> + +<template> + <section class="mr-info-list mr-links"> + <p v-if="maintainerEditAllowed"> + {{ s__("mrWidget|Allows edits from maintainers") }} + </p> + </section> +</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 new file mode 100644 index 00000000000..460437ceeff --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue @@ -0,0 +1,34 @@ +<script> + import tooltip from '../../vue_shared/directives/tooltip'; + import { __ } from '../../locale'; + + export default { + directives: { + tooltip, + }, + created() { + this.removesBranchText = __('<strong>Removes</strong> source branch'); + this.tooltipTitle = __('A user with write access to the source branch selected this option'); + }, + }; +</script> + +<template> + <p + v-once + class="mr-info-list mr-links source-branch-removal-status append-bottom-0" + > + <span + class="status-text" + v-html="removesBranchText" + > + </span> + <i + v-tooltip + class="fa fa-question-circle" + :title="tooltipTitle" + :aria-label="tooltipTitle" + > + </i> + </p> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue index de98a77be6f..7ff7fc7988a 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue @@ -63,7 +63,7 @@ }; this.isRemovingSourceBranch = true; - this.service.mergeResource.save(options) + this.service.merge(options) .then(res => res.data) .then((data) => { if (data.status === 'merge_when_pipeline_succeeds') { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_nothing_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_nothing_to_merge.js deleted file mode 100644 index ebfd6765934..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_nothing_to_merge.js +++ /dev/null @@ -1,44 +0,0 @@ -import emptyStateSVG from 'icons/_mr_widget_empty_state.svg'; - -export default { - name: 'MRWidgetNothingToMerge', - props: { - mr: { - type: Object, - required: true, - }, - }, - data() { - return { emptyStateSVG }; - }, - template: ` - <div class="mr-widget-body mr-widget-empty-state"> - <div class="row"> - <div class="artwork col-sm-5 col-sm-push-7 col-xs-12 text-center"> - <span v-html="emptyStateSVG"></span> - </div> - <div class="text col-sm-7 col-sm-pull-5 col-xs-12"> - <span> - Merge requests are a place to propose changes you have made to a project - and discuss those changes with others. - </span> - <p> - Interested parties can even contribute by pushing commits if they want to. - </p> - <p> - Currently there are no changes in this merge request's source branch. - Please push new commits or use a different branch. - </p> - <div> - <a - v-if="mr.newBlobPath" - :href="mr.newBlobPath" - class="btn btn-inverted btn-save"> - Create file - </a> - </div> - </div> - </div> - </div> - `, -}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js index 162f048aac7..3c781ccddc8 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js @@ -93,7 +93,7 @@ export default { || this.mr.preventMerge); }, isRemoveSourceBranchButtonDisabled() { - return this.isMergeButtonDisabled || !this.mr.canRemoveSourceBranch; + return this.isMergeButtonDisabled; }, shouldShowSquashBeforeMerge() { const { commitsCount, enableSquashBeforeMerge } = this.mr; @@ -282,7 +282,7 @@ export default { </span> <div class="media-body-wrap space-children"> <template v-if="shouldShowMergeControls()"> - <label> + <label v-if="mr.canRemoveSourceBranch"> <input id="remove-source-branch-input" v-model="removeSourceBranch" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js deleted file mode 100644 index 142ddf477f1..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js +++ /dev/null @@ -1,18 +0,0 @@ -import statusIcon from '../mr_widget_status_icon.vue'; - -export default { - name: 'MRWidgetSHAMismatch', - components: { - statusIcon, - }, - template: ` - <div class="mr-widget-body media"> - <status-icon status="warning" :show-disabled-button="true" /> - <div class="media-body space-children"> - <span class="bold"> - The source branch HEAD has recently changed. Please reload the page and review the changes before merging - </span> - </div> - </div> - `, -}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js deleted file mode 100644 index 67b271c69ca..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js +++ /dev/null @@ -1,27 +0,0 @@ -import statusIcon from '../mr_widget_status_icon.vue'; - -export default { - name: 'MRWidgetUnresolvedDiscussions', - props: { - mr: { type: Object, required: true }, - }, - components: { - statusIcon, - }, - template: ` - <div class="mr-widget-body media"> - <status-icon status="warning" :show-disabled-button="true" /> - <div class="media-body space-children"> - <span class="bold"> - There are unresolved discussions. Please resolve these discussions - </span> - <a - v-if="mr.createIssueToResolveDiscussionsPath" - :href="mr.createIssueToResolveDiscussionsPath" - class="btn btn-default btn-xs js-create-issue"> - Create an issue to resolve them later - </a> - </div> - </div> - `, -}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js index bbca641f65e..44e1a616a19 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import statusIcon from '../mr_widget_status_icon.vue'; import tooltip from '../../../vue_shared/directives/tooltip'; import eventHub from '../../event_hub'; 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 new file mode 100644 index 00000000000..3d9161f6926 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue @@ -0,0 +1,47 @@ +<script> +import emptyStateSVG from 'icons/_mr_widget_empty_state.svg'; + +export default { + name: 'MRWidgetNothingToMerge', + props: { + mr: { + type: Object, + required: true, + }, + }, + data() { + return { emptyStateSVG }; + }, +}; +</script> + +<template> + <div class="mr-widget-body mr-widget-empty-state"> + <div class="row"> + <div class="artwork col-sm-5 col-sm-push-7 col-xs-12 text-center"> + <span v-html="emptyStateSVG"></span> + </div> + <div class="text col-sm-7 col-sm-pull-5 col-xs-12"> + <span> + Merge requests are a place to propose changes you have made to a project + and discuss those changes with others. + </span> + <p> + Interested parties can even contribute by pushing commits if they want to. + </p> + <p> + Currently there are no changes in this merge request's source branch. + Please push new commits or use a different branch. + </p> + <div> + <a + v-if="mr.newBlobPath" + :href="mr.newBlobPath" + class="btn btn-inverted btn-save"> + Create file + </a> + </div> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue new file mode 100644 index 00000000000..04100871a94 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue @@ -0,0 +1,25 @@ +<script> +import statusIcon from '../mr_widget_status_icon.vue'; + +export default { + name: 'ShaMismatch', + components: { + statusIcon, + }, +}; +</script> + +<template> + <div class="mr-widget-body media"> + <status-icon + status="warning" + :show-disabled-button="true" + /> + <div class="media-body space-children"> + <span class="bold"> + The source branch HEAD has recently changed. + Please reload the page and review the changes before merging. + </span> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue new file mode 100644 index 00000000000..9ade6a91747 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue @@ -0,0 +1,33 @@ +<script> +import statusIcon from '../mr_widget_status_icon.vue'; + +export default { + name: 'UnresolvedDiscussions', + components: { + statusIcon, + }, + props: { + mr: { type: Object, required: true }, + }, +}; +</script> + +<template> + <div class="mr-widget-body media"> + <status-icon + status="warning" + :show-disabled-button="true" + /> + <div class="media-body space-children"> + <span class="bold"> + There are unresolved discussions. Please resolve these discussions + </span> + <a + v-if="mr.createIssueToResolveDiscussionsPath" + :href="mr.createIssueToResolveDiscussionsPath" + class="btn btn-default btn-xs js-create-issue"> + Create an issue to resolve them later + </a> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/dependencies.js b/app/assets/javascripts/vue_merge_request_widget/dependencies.js index edb3baa39e4..ed15fc6ab0f 100644 --- a/app/assets/javascripts/vue_merge_request_widget/dependencies.js +++ b/app/assets/javascripts/vue_merge_request_widget/dependencies.js @@ -14,7 +14,8 @@ export { default as SmartInterval } from '~/smart_interval'; export { default as WidgetHeader } from './components/mr_widget_header.vue'; export { default as WidgetMergeHelp } from './components/mr_widget_merge_help.vue'; export { default as WidgetPipeline } from './components/mr_widget_pipeline.vue'; -export { default as WidgetDeployment } from './components/mr_widget_deployment'; +export { default as Deployment } from './components/deployment.vue'; +export { default as WidgetMaintainerEdit } from './components/mr_widget_maintainer_edit.vue'; export { default as WidgetRelatedLinks } from './components/mr_widget_related_links.vue'; export { default as MergedState } from './components/states/mr_widget_merged.vue'; export { default as FailedToMerge } from './components/states/mr_widget_failed_to_merge.vue'; @@ -23,12 +24,12 @@ export { default as MergingState } from './components/states/mr_widget_merging.v export { default as WipState } from './components/states/mr_widget_wip'; export { default as ArchivedState } from './components/states/mr_widget_archived.vue'; export { default as ConflictsState } from './components/states/mr_widget_conflicts.vue'; -export { default as NothingToMergeState } from './components/states/mr_widget_nothing_to_merge'; +export { default as NothingToMergeState } from './components/states/nothing_to_merge.vue'; export { default as MissingBranchState } from './components/states/mr_widget_missing_branch.vue'; export { default as NotAllowedState } from './components/states/mr_widget_not_allowed.vue'; export { default as ReadyToMergeState } from './components/states/mr_widget_ready_to_merge'; -export { default as SHAMismatchState } from './components/states/mr_widget_sha_mismatch'; -export { default as UnresolvedDiscussionsState } from './components/states/mr_widget_unresolved_discussions'; +export { default as ShaMismatchState } from './components/states/sha_mismatch.vue'; +export { default as UnresolvedDiscussionsState } from './components/states/unresolved_discussions.vue'; export { default as PipelineBlockedState } from './components/states/mr_widget_pipeline_blocked.vue'; export { default as PipelineFailedState } from './components/states/mr_widget_pipeline_failed'; export { default as MergeWhenPipelineSucceedsState } from './components/states/mr_widget_merge_when_pipeline_succeeds.vue'; @@ -39,7 +40,9 @@ export { default as MRWidgetStore } from './stores/mr_widget_store'; export { default as MRWidgetService } from './services/mr_widget_service'; export { default as eventHub } from './event_hub'; export { default as getStateKey } from './stores/get_state_key'; -export { default as mrWidgetOptions } from './mr_widget_options'; export { default as stateMaps } from './stores/state_maps'; export { default as SquashBeforeMerge } from './components/states/mr_widget_squash_before_merge'; export { default as notify } from '../lib/utils/notify'; +export { default as SourceBranchRemovalStatus } from './components/source_branch_removal_status.vue'; + +export { default as mrWidgetOptions } from './mr_widget_options'; diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js index 797f0f6ec0f..0be5d9e5a55 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js @@ -5,7 +5,8 @@ import { WidgetHeader, WidgetMergeHelp, WidgetPipeline, - WidgetDeployment, + Deployment, + WidgetMaintainerEdit, WidgetRelatedLinks, MergedState, ClosedState, @@ -18,7 +19,7 @@ import { MissingBranchState, NotAllowedState, ReadyToMergeState, - SHAMismatchState, + ShaMismatchState, UnresolvedDiscussionsState, PipelineBlockedState, PipelineFailedState, @@ -32,6 +33,7 @@ import { stateMaps, SquashBeforeMerge, notify, + SourceBranchRemovalStatus, } from './dependencies'; import { setFavicon } from '../lib/utils/common_utils'; @@ -65,8 +67,9 @@ export default { shouldRenderRelatedLinks() { return !!this.mr.relatedLinks && !this.mr.isNothingToMergeState; }, - shouldRenderDeployments() { - return this.mr.deployments.length; + shouldRenderSourceBranchRemovalStatus() { + return !this.mr.canRemoveSourceBranch && this.mr.shouldRemoveSourceBranch && + (!this.mr.isNothingToMergeState && !this.mr.isMergedState); }, }, methods: { @@ -210,7 +213,8 @@ export default { 'mr-widget-header': WidgetHeader, 'mr-widget-merge-help': WidgetMergeHelp, 'mr-widget-pipeline': WidgetPipeline, - 'mr-widget-deployment': WidgetDeployment, + Deployment, + 'mr-widget-maintainer-edit': WidgetMaintainerEdit, 'mr-widget-related-links': WidgetRelatedLinks, 'mr-widget-merged': MergedState, 'mr-widget-closed': ClosedState, @@ -223,7 +227,7 @@ export default { 'mr-widget-not-allowed': NotAllowedState, 'mr-widget-missing-branch': MissingBranchState, 'mr-widget-ready-to-merge': ReadyToMergeState, - 'mr-widget-sha-mismatch': SHAMismatchState, + 'mr-widget-sha-mismatch': ShaMismatchState, 'mr-widget-squash-before-merge': SquashBeforeMerge, 'mr-widget-checking': CheckingState, 'mr-widget-unresolved-discussions': UnresolvedDiscussionsState, @@ -232,6 +236,7 @@ export default { 'mr-widget-merge-when-pipeline-succeeds': MergeWhenPipelineSucceedsState, 'mr-widget-auto-merge-failed': AutoMergeFailed, 'mr-widget-rebase': RebaseState, + SourceBranchRemovalStatus, }, template: ` <div class="mr-state-widget prepend-top-default"> @@ -242,20 +247,25 @@ export default { :ci-status="mr.ciStatus" :has-ci="mr.hasCI" /> - <mr-widget-deployment - v-if="shouldRenderDeployments" - :mr="mr" - :service="service" /> + <deployment + v-for="deployment in mr.deployments" + :key="deployment.id" + :deployment="deployment" + /> <div class="mr-widget-section"> <component :is="componentName" :mr="mr" :service="service" /> + <mr-widget-maintainer-edit + :maintainerEditAllowed="mr.maintainerEditAllowed" /> <mr-widget-related-links v-if="shouldRenderRelatedLinks" :state="mr.state" - :related-links="mr.relatedLinks" - /> + :related-links="mr.relatedLinks" /> + <source-branch-removal-status + v-if="shouldRenderSourceBranchRemovalStatus" + /> </div> <div class="mr-widget-footer" 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 9a750ce42bd..a47ca9fae86 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 @@ -76,6 +76,7 @@ export default class MergeRequestStore { this.canBeMerged = data.can_be_merged || false; this.isMergeAllowed = data.mergeable || false; this.mergeOngoing = data.merge_ongoing; + this.maintainerEditAllowed = data.allow_maintainer_to_push; // Cherry-pick and Revert actions related this.canCherryPickInCurrentMR = currentUser.can_cherry_pick_on_current_merge_request || false; @@ -124,6 +125,10 @@ export default class MergeRequestStore { return this.state === stateKey.nothingToMerge; } + get isMergedState() { + return this.state === stateKey.merged; + } + initRebase(data) { this.canPushToSourceBranch = data.can_push_to_source_branch; this.rebaseInProgress = data.rebase_in_progress; diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js b/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js index 29d5bd4a1da..e080ce5c229 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js @@ -16,7 +16,7 @@ const stateToComponentMap = { mergeWhenPipelineSucceeds: 'mr-widget-merge-when-pipeline-succeeds', failedToMerge: 'mr-widget-failed-to-merge', autoMergeFailed: 'mr-widget-auto-merge-failed', - shaMismatch: 'mr-widget-sha-mismatch', + shaMismatch: 'sha-mismatch', rebase: 'mr-widget-rebase', }; @@ -49,6 +49,7 @@ export const stateKey = { notAllowedToMerge: 'notAllowedToMerge', readyToMerge: 'readyToMerge', rebase: 'rebase', + merged: 'merged', }; export default { diff --git a/app/assets/javascripts/vue_shared/components/clipboard_button.vue b/app/assets/javascripts/vue_shared/components/clipboard_button.vue index 3b6c2da1664..cab126a7eca 100644 --- a/app/assets/javascripts/vue_shared/components/clipboard_button.vue +++ b/app/assets/javascripts/vue_shared/components/clipboard_button.vue @@ -31,7 +31,7 @@ cssClass: { type: String, required: false, - default: 'btn btn-default btn-transparent btn-clipboard', + default: 'btn-default', }, }, }; @@ -40,6 +40,7 @@ <template> <button type="button" + class="btn" :class="cssClass" :title="title" :data-clipboard-text="text" diff --git a/app/assets/javascripts/vue_shared/components/file_icon.vue b/app/assets/javascripts/vue_shared/components/file_icon.vue index c9d7c0f4999..ee1c3498748 100644 --- a/app/assets/javascripts/vue_shared/components/file_icon.vue +++ b/app/assets/javascripts/vue_shared/components/file_icon.vue @@ -62,8 +62,7 @@ return `${gon.sprite_file_icons}#${iconName}`; }, folderIconName() { - // We don't have a open folder icon yet - return this.opened ? 'folder' : 'folder'; + return this.opened ? 'folder-open' : 'folder'; }, iconSizeClass() { return this.size ? `s${this.size}` : ''; diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index d2e968a8419..12c7d125062 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -1,4 +1,5 @@ <script> + import $ from 'jquery'; import Flash from '../../../flash'; import GLForm from '../../../gl_form'; import markdownHeader from './header.vue'; diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index 177d2cfc8da..d91fe3cf0c5 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -1,4 +1,5 @@ <script> + import $ from 'jquery'; import tooltip from '../../directives/tooltip'; import toolbarButton from './toolbar_button.vue'; import icon from '../icon.vue'; diff --git a/app/assets/javascripts/vue_shared/components/memory_graph.js b/app/assets/javascripts/vue_shared/components/memory_graph.vue index f37ef1a5ca3..b07f6b07afe 100644 --- a/app/assets/javascripts/vue_shared/components/memory_graph.js +++ b/app/assets/javascripts/vue_shared/components/memory_graph.vue @@ -1,3 +1,4 @@ +<script> import { getTimeago } from '../../lib/utils/datetime_utility'; export default { @@ -22,6 +23,9 @@ export default { return `Deployed ${deployedSince}`; }, }, + mounted() { + this.renderGraph(this.deploymentTime, this.metrics); + }, methods: { /** * Returns metric value index in metrics array @@ -103,15 +107,27 @@ export default { this.dotY = dotY; }, }, - mounted() { - this.renderGraph(this.deploymentTime, this.metrics); - }, - template: ` - <div class="memory-graph-container"> - <svg class="has-tooltip" :title="getFormattedMedian" :width="width" :height="height" xmlns="http://www.w3.org/2000/svg"> - <path :d="pathD" :viewBox="pathViewBox" /> - <circle r="1.5" :cx="dotX" :cy="dotY" tranform="translate(0 -1)" /> - </svg> - </div> - `, }; +</script> + +<template> + <div class="memory-graph-container"> + <svg + class="has-tooltip" + :title="getFormattedMedian" + :width="width" + :height="height" + xmlns="http://www.w3.org/2000/svg"> + <path + :d="pathD" + :viewBox="pathViewBox" + /> + <circle + r="1.5" + :cx="dotX" + :cy="dotY" + tranform="translate(0 -1)" + /> + </svg> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue index 63d8329e495..b33a0101dbf 100644 --- a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue +++ b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue @@ -1,4 +1,6 @@ <script> + import $ from 'jquery'; + /** * Given an array of tabs, renders non linked bootstrap tabs. * When a tab is clicked it will trigger an event and provide the clicked scope. 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 new file mode 100644 index 00000000000..5ede53d8d01 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue @@ -0,0 +1,173 @@ +<script> +import { __ } from '~/locale'; +import LabelsSelect from '~/labels_select'; +import LoadingIcon from '../../loading_icon.vue'; + +import DropdownTitle from './dropdown_title.vue'; +import DropdownValue from './dropdown_value.vue'; +import DropdownValueCollapsed from './dropdown_value_collapsed.vue'; +import DropdownButton from './dropdown_button.vue'; +import DropdownHiddenInput from './dropdown_hidden_input.vue'; +import DropdownHeader from './dropdown_header.vue'; +import DropdownSearchInput from './dropdown_search_input.vue'; +import DropdownFooter from './dropdown_footer.vue'; +import DropdownCreateLabel from './dropdown_create_label.vue'; + +export default { + components: { + LoadingIcon, + DropdownTitle, + DropdownValue, + DropdownValueCollapsed, + DropdownButton, + DropdownHiddenInput, + DropdownHeader, + DropdownSearchInput, + DropdownFooter, + DropdownCreateLabel, + }, + props: { + showCreate: { + type: Boolean, + required: false, + default: false, + }, + isProject: { + type: Boolean, + required: false, + default: false, + }, + abilityName: { + type: String, + required: true, + }, + context: { + type: Object, + required: true, + }, + namespace: { + type: String, + required: false, + default: '', + }, + updatePath: { + type: String, + required: false, + default: '', + }, + labelsPath: { + type: String, + required: true, + }, + labelsWebUrl: { + type: String, + required: false, + default: '', + }, + labelFilterBasePath: { + type: String, + required: false, + default: '', + }, + canEdit: { + type: Boolean, + required: false, + default: false, + }, + }, + computed: { + hiddenInputName() { + return this.showCreate ? `${this.abilityName}[label_names][]` : 'label_id[]'; + }, + createLabelTitle() { + if (this.isProject) { + return __('Create project label'); + } + + return __('Create group label'); + }, + manageLabelsTitle() { + if (this.isProject) { + return __('Manage project labels'); + } + + return __('Manage group labels'); + }, + }, + mounted() { + this.labelsDropdown = new LabelsSelect(this.$refs.dropdownButton, { + handleClick: this.handleClick, + }); + }, + methods: { + handleClick(label) { + this.$emit('onLabelClick', label); + }, + }, +}; +</script> + +<template> + <div class="block labels js-labels-block"> + <dropdown-value-collapsed + v-if="showCreate" + :labels="context.labels" + /> + <dropdown-title + :can-edit="canEdit" + /> + <dropdown-value + :labels="context.labels" + :label-filter-base-path="labelFilterBasePath" + > + <slot></slot> + </dropdown-value> + <div + v-if="canEdit" + class="selectbox js-selectbox" + style="display: none;" + > + <dropdown-hidden-input + v-for="label in context.labels" + :key="label.id" + :name="hiddenInputName" + :label="label" + /> + <div class="dropdown"> + <dropdown-button + :ability-name="abilityName" + :field-name="hiddenInputName" + :update-path="updatePath" + :labels-path="labelsPath" + :namespace="namespace" + :labels="context.labels" + :show-extra-options="!showCreate" + /> + <div + 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-search-input/> + <div class="dropdown-content"></div> + <div class="dropdown-loading"> + <loading-icon /> + </div> + <dropdown-footer + v-if="showCreate" + :labels-web-url="labelsWebUrl" + :create-label-title="createLabelTitle" + :manage-labels-title="manageLabelsTitle" + /> + </div> + <dropdown-create-label + v-if="showCreate" + :is-project="isProject" + :header-title="createLabelTitle" + /> + </div> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue new file mode 100644 index 00000000000..47497c1de98 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue @@ -0,0 +1,78 @@ +<script> +import { __, s__, sprintf } from '~/locale'; + +export default { + props: { + abilityName: { + type: String, + required: true, + }, + fieldName: { + type: String, + required: true, + }, + updatePath: { + type: String, + required: true, + }, + labelsPath: { + type: String, + required: true, + }, + namespace: { + type: String, + required: true, + }, + labels: { + type: Array, + required: true, + }, + showExtraOptions: { + type: Boolean, + required: true, + }, + }, + computed: { + dropdownToggleText() { + if (this.labels.length === 0) { + return __('Label'); + } + + if (this.labels.length > 1) { + return sprintf(s__('LabelSelect|%{firstLabelName} +%{remainingLabelCount} more'), { + firstLabelName: this.labels[0].title, + remainingLabelCount: this.labels.length - 1, + }); + } + + return this.labels[0].title; + }, + }, +}; +</script> + +<template> + <button + type="button" + ref="dropdownButton" + class="dropdown-menu-toggle wide js-label-select js-multiselect js-context-config-modal" + data-toggle="dropdown" + :class="{ 'js-extra-options': showExtraOptions }" + :data-ability-name="abilityName" + :data-field-name="fieldName" + :data-issue-update="updatePath" + :data-labels="labelsPath" + :data-namespace-path="namespace" + :data-show-any="showExtraOptions" + > + <span class="dropdown-toggle-text"> + {{ dropdownToggleText }} + </span> + <i + aria-hidden="true" + class="fa fa-chevron-down" + data-hidden="true" + > + </i> + </button> +</template> 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 new file mode 100644 index 00000000000..34a07f33a23 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue @@ -0,0 +1,93 @@ +<script> +import { __ } from '~/locale'; + +export default { + props: { + headerTitle: { + type: String, + required: false, + default: () => __('Create new label'), + }, + }, + created() { + this.suggestedColors = gon.suggested_label_colors; + }, +}; +</script> + +<template> + <div class="dropdown-page-two dropdown-new-label"> + <div class="dropdown-title"> + <button + type="button" + class="dropdown-title-button dropdown-menu-back" + :aria-label="__('Go back')" + > + <i + aria-hidden="true" + class="fa fa-arrow-left" + data-hidden="true" + > + </i> + </button> + {{ headerTitle }} + <button + type="button" + class="dropdown-title-button dropdown-menu-close" + :aria-label="__('Close')" + > + <i + aria-hidden="true" + class="fa fa-times dropdown-menu-close-icon" + data-hidden="true" + > + </i> + </button> + </div> + <div class="dropdown-content"> + <div class="dropdown-labels-error js-label-error"></div> + <input + id="new_label_name" + type="text" + class="default-dropdown-input" + :placeholder="__('Name new label')" + /> + <div class="suggest-colors suggest-colors-dropdown"> + <a + v-for="(color, index) in suggestedColors" + href="#" + :key="index" + :data-color="color" + :style="{ + backgroundColor: color, + }" + > + + </a> + </div> + <div class="dropdown-label-color-input"> + <div class="dropdown-label-color-preview js-dropdown-label-color-preview"></div> + <input + id="new_label_color" + type="text" + class="default-dropdown-input" + :placeholder="__('Assign custom color like #FF0000')" + /> + </div> + <div class="clearfix"> + <button + type="button" + class="btn btn-primary pull-left js-new-label-btn disabled" + > + {{ __('Create') }} + </button> + <button + type="button" + class="btn btn-default pull-right js-cancel-label-btn" + > + {{ __('Cancel') }} + </button> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer.vue new file mode 100644 index 00000000000..5f61e9fbe80 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer.vue @@ -0,0 +1,46 @@ +<script> +import { __ } from '~/locale'; + +export default { + props: { + labelsWebUrl: { + type: String, + required: true, + }, + createLabelTitle: { + type: String, + required: false, + default: () => __('Create new label'), + }, + manageLabelsTitle: { + type: String, + required: false, + default: () => __('Manage labels'), + }, + }, +}; +</script> + +<template> + <div class="dropdown-footer"> + <ul class="dropdown-footer-list"> + <li> + <a + href="#" + class="dropdown-toggle-page" + > + {{ createLabelTitle }} + </a> + </li> + <li> + <a + data-is-link="true" + class="dropdown-external-link" + :href="labelsWebUrl" + > + {{ manageLabelsTitle }} + </a> + </li> + </ul> + </div> +</template> 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 new file mode 100644 index 00000000000..7664acdf19c --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue @@ -0,0 +1,21 @@ +<script> +export default {}; +</script> + +<template> + <div class="dropdown-title"> + <span>{{ __('Assign labels') }}</span> + <button + type="button" + class="dropdown-title-button dropdown-menu-close" + :aria-label="__('Close')" + > + <i + aria-hidden="true" + class="fa fa-times dropdown-menu-close-icon" + data-hidden="true" + > + </i> + </button> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input.vue new file mode 100644 index 00000000000..1832c3c1757 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input.vue @@ -0,0 +1,22 @@ +<script> +export default { + props: { + name: { + type: String, + required: true, + }, + label: { + type: Object, + required: true, + }, + }, +}; +</script> + +<template> + <input + type="hidden" + :name="name" + :value="label.id" + /> +</template> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue new file mode 100644 index 00000000000..ae633460c95 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue @@ -0,0 +1,27 @@ +<script> +export default {}; +</script> + +<template> + <div class="dropdown-input"> + <input + autocomplete="off" + class="dropdown-input-field" + type="search" + :placeholder="__('Search')" + /> + <i + aria-hidden="true" + class="fa fa-search dropdown-input-search" + data-hidden="true" + > + </i> + <i + aria-hidden="true" + class="fa fa-times dropdown-input-clear js-dropdown-input-clear" + data-hidden="true" + role="button" + > + </i> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue new file mode 100644 index 00000000000..7da82e90e29 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue @@ -0,0 +1,30 @@ +<script> +export default { + props: { + canEdit: { + type: Boolean, + required: true, + }, + }, +}; +</script> + +<template> + <div class="title hide-collapsed append-bottom-10"> + {{ __('Labels') }} + <template v-if="canEdit"> + <i + aria-hidden="true" + class="fa fa-spinner fa-spin block-loading" + data-hidden="true" + > + </i> + <button + type="button" + class="edit-link btn btn-blank pull-right js-sidebar-dropdown-toggle" + > + {{ __('Edit') }} + </button> + </template> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue new file mode 100644 index 00000000000..69d588eb25d --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue @@ -0,0 +1,63 @@ +<script> +import tooltip from '~/vue_shared/directives/tooltip'; + +export default { + directives: { + tooltip, + }, + props: { + labels: { + type: Array, + required: true, + }, + labelFilterBasePath: { + type: String, + required: true, + }, + }, + computed: { + isEmpty() { + return this.labels.length === 0; + }, + }, + methods: { + labelFilterUrl(label) { + return `${this.labelFilterBasePath}?label_name[]=${encodeURIComponent(label.title)}`; + }, + labelStyle(label) { + return { + color: label.textColor, + backgroundColor: label.color, + }; + }, + }, +}; +</script> + +<template> + <div class="hide-collapsed value issuable-show-labels js-value"> + <span + v-if="isEmpty" + class="text-secondary" + > + <slot>{{ __('None') }}</slot> + </span> + <a + v-else + v-for="label in labels" + :key="label.id" + :href="labelFilterUrl(label)" + > + <span + v-tooltip + class="label color-label" + data-placement="bottom" + data-container="body" + :style="labelStyle(label)" + :title="label.description" + > + {{ label.title }} + </span> + </a> + </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 new file mode 100644 index 00000000000..5cf728fe050 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue @@ -0,0 +1,48 @@ +<script> +import { s__, sprintf } from '~/locale'; +import tooltip from '~/vue_shared/directives/tooltip'; + +export default { + directives: { + tooltip, + }, + props: { + labels: { + type: Array, + required: true, + }, + }, + computed: { + labelsList() { + const labelsString = this.labels.slice(0, 5).map(label => label.title).join(', '); + + if (this.labels.length > 5) { + return sprintf(s__('LabelSelect|%{labelsString}, and %{remainingLabelCount} more'), { + labelsString, + remainingLabelCount: this.labels.length - 5, + }); + } + + return labelsString; + }, + }, +}; +</script> + +<template> + <div + v-tooltip + class="sidebar-collapsed-icon" + data-placement="left" + data-container="body" + :title="labelsList" + > + <i + aria-hidden="true" + data-hidden="true" + class="fa fa-tags" + > + </i> + <span>{{ labels.length }}</span> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/directives/popover.js b/app/assets/javascripts/vue_shared/directives/popover.js index 05fa563cbd0..eb35294906b 100644 --- a/app/assets/javascripts/vue_shared/directives/popover.js +++ b/app/assets/javascripts/vue_shared/directives/popover.js @@ -1,3 +1,5 @@ +import $ from 'jquery'; + /** * Helper to user bootstrap popover in vue.js. * Follow docs for html attributes: https://getbootstrap.com/docs/3.3/javascript/#static-popover diff --git a/app/assets/javascripts/vue_shared/directives/tooltip.js b/app/assets/javascripts/vue_shared/directives/tooltip.js index dc896cf5c7d..b7f7e9fec15 100644 --- a/app/assets/javascripts/vue_shared/directives/tooltip.js +++ b/app/assets/javascripts/vue_shared/directives/tooltip.js @@ -1,3 +1,5 @@ +import $ from 'jquery'; + export default { bind(el) { $(el).tooltip(); diff --git a/app/assets/javascripts/boards/models/label.js b/app/assets/javascripts/vue_shared/models/label.js index 98c1ec014c4..70b9efe0c68 100644 --- a/app/assets/javascripts/boards/models/label.js +++ b/app/assets/javascripts/vue_shared/models/label.js @@ -1,7 +1,5 @@ -/* eslint-disable no-unused-vars, space-before-function-paren */ - class ListLabel { - constructor (obj) { + constructor(obj) { this.id = obj.id; this.title = obj.title; this.type = obj.type; diff --git a/app/assets/javascripts/zen_mode.js b/app/assets/javascripts/zen_mode.js index 4592003f57e..f68a4f28714 100644 --- a/app/assets/javascripts/zen_mode.js +++ b/app/assets/javascripts/zen_mode.js @@ -5,6 +5,7 @@ /*= provides zen_mode:enter */ /*= provides zen_mode:leave */ +import $ from 'jquery'; import 'vendor/jquery.scrollTo'; import Dropzone from 'dropzone'; import Mousetrap from 'mousetrap'; diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index ae517c41cb2..37d33320445 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -14,6 +14,10 @@ color: $gl-text-color-secondary; } +.text-tertiary { + color: $gl-text-color-tertiary; +} + .text-primary, .text-primary:hover { color: $brand-primary; diff --git a/app/assets/stylesheets/framework/contextual_sidebar.scss b/app/assets/stylesheets/framework/contextual_sidebar.scss index 1acde98c3ae..e2d97d0298f 100644 --- a/app/assets/stylesheets/framework/contextual_sidebar.scss +++ b/app/assets/stylesheets/framework/contextual_sidebar.scss @@ -9,7 +9,8 @@ padding-left: $contextual-sidebar-width; } - .issues-bulk-update.right-sidebar.right-sidebar-expanded .issuable-sidebar-header { + .issues-bulk-update.right-sidebar.right-sidebar-expanded + .issuable-sidebar-header { padding: 10px 0 15px; } } @@ -61,7 +62,8 @@ } .nav-sidebar { - transition: width $sidebar-transition-duration, left $sidebar-transition-duration; + transition: width $sidebar-transition-duration, + left $sidebar-transition-duration; position: fixed; z-index: 400; width: $contextual-sidebar-width; @@ -75,7 +77,7 @@ &:not(.sidebar-collapsed-desktop) { @media (min-width: $screen-sm-min) and (max-width: $screen-md-max) { box-shadow: inset -2px 0 0 $border-color, - 2px 1px 3px $dropdown-shadow-color; + 2px 1px 3px $dropdown-shadow-color; } } @@ -234,7 +236,7 @@ border-radius: 0 3px 3px 0; &::before { - content: ""; + content: ''; position: absolute; top: -30px; bottom: -30px; @@ -305,7 +307,6 @@ } } - // Collapsed nav .toggle-sidebar-button, @@ -454,18 +455,3 @@ z-index: 300; } } - - -// Make issue boards full-height now that sub-nav is gone - -.boards-list { - height: calc(100vh - #{$header-height}); - - @media (min-width: $screen-sm-min) { - height: calc(100vh - 180px); - } -} - -.with-performance-bar .boards-list { - height: calc(100vh - #{$header-height} - #{$performance-bar-height}); -} diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 1d7b0b602cc..6397757bf88 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -272,7 +272,7 @@ .divider { height: 1px; - margin: 6px 0; + margin: #{$grid-size / 2} 0; padding: 0; background-color: $dropdown-divider-color; @@ -501,10 +501,8 @@ -moz-osx-font-smoothing: grayscale; } - &.dropdown-menu-user-link { - &::before { - top: 50%; - } + &.dropdown-menu-user-link::before { + top: 50%; } } diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss index 88ce119ee3a..cb2f71b0033 100644 --- a/app/assets/stylesheets/framework/flash.scss +++ b/app/assets/stylesheets/framework/flash.scss @@ -12,6 +12,12 @@ margin: 0; } + .flash-warning { + @extend .alert; + @extend .alert-warning; + margin: 0; + } + .flash-alert { @extend .alert; @extend .alert-danger; diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 634593aefd0..0136af76a13 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -1,60 +1,24 @@ .navbar-gitlab { - &.navbar-gitlab { - padding: 0 16px; - z-index: 1000; - margin-bottom: 0; - min-height: $header-height; - border: 0; - border-bottom: 1px solid $border-color; - position: fixed; - top: 0; - left: 0; - right: 0; - border-radius: 0; - - .logo-text { - line-height: initial; - - svg { - width: 55px; - height: 14px; - margin: 0; - fill: $white-light; - } - } - - .container-fluid { - padding: 0; - - .user-counter { - svg { - margin-right: 3px; - } - } - - .navbar-toggle { - right: -10px; - border-radius: 0; - min-width: 45px; - padding: 0; - margin-right: -7px; - font-size: 14px; - text-align: center; - color: currentColor; - - &:hover, - &:focus, - &.active { - color: currentColor; - background-color: transparent; - } - - .more-icon, - .close-icon { - fill: $white-light; - margin: auto; - } - } + padding: 0 16px; + z-index: 1000; + margin-bottom: 0; + min-height: $header-height; + border: 0; + border-bottom: 1px solid $border-color; + position: fixed; + top: 0; + left: 0; + right: 0; + border-radius: 0; + + .logo-text { + line-height: initial; + + svg { + width: 55px; + height: 14px; + margin: 0; + fill: $white-light; } } @@ -184,6 +148,37 @@ } .container-fluid { + padding: 0; + + .user-counter { + svg { + margin-right: 3px; + } + } + + .navbar-toggle { + right: -10px; + border-radius: 0; + min-width: 45px; + padding: 0; + margin-right: -7px; + font-size: 14px; + text-align: center; + color: currentColor; + + &:hover, + &:focus, + &.active { + color: currentColor; + background-color: transparent; + } + + .more-icon, + .close-icon { + fill: $white-light; + margin: auto; + } + } .navbar-nav { @media (max-width: $screen-xs-max) { @@ -337,7 +332,7 @@ .breadcrumbs { display: -webkit-flex; display: flex; - min-height: 48px; + min-height: $breadcrumb-min-height; color: $gl-text-color; } @@ -466,7 +461,7 @@ padding: 0 5px; line-height: 12px; border-radius: 7px; - box-shadow: 0 1px 0 rgba($gl-header-color, .2); + box-shadow: 0 1px 0 rgba($gl-header-color, 0.2); &.issues-count { background-color: $green-500; diff --git a/app/assets/stylesheets/framework/images.scss b/app/assets/stylesheets/framework/images.scss index 2d015ef086b..df1cafc9f8e 100644 --- a/app/assets/stylesheets/framework/images.scss +++ b/app/assets/stylesheets/framework/images.scss @@ -20,7 +20,7 @@ width: 100%; } - $image-widths: 250 306 394 430; + $image-widths: 80 250 306 394 430; @each $width in $image-widths { &.svg-#{$width} { img, @@ -39,12 +39,35 @@ svg { fill: currentColor; - &.s8 { @include svg-size(8px); } - &.s12 { @include svg-size(12px); } - &.s16 { @include svg-size(16px); } - &.s18 { @include svg-size(18px); } - &.s24 { @include svg-size(24px); } - &.s32 { @include svg-size(32px); } - &.s48 { @include svg-size(48px); } - &.s72 { @include svg-size(72px); } + &.s8 { + @include svg-size(8px); + } + + &.s12 { + @include svg-size(12px); + } + + &.s16 { + @include svg-size(16px); + } + + &.s18 { + @include svg-size(18px); + } + + &.s24 { + @include svg-size(24px); + } + + &.s32 { + @include svg-size(32px); + } + + &.s48 { + @include svg-size(48px); + } + + &.s72 { + @include svg-size(72px); + } } diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index ddd9dbb2be4..e12b5aab381 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -17,8 +17,6 @@ */ @mixin markdown-table { width: auto; - display: block; - overflow-x: auto; } /* diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss index a6b1bf9b099..48b981dd31f 100644 --- a/app/assets/stylesheets/framework/modal.scss +++ b/app/assets/stylesheets/framework/modal.scss @@ -2,14 +2,17 @@ background-color: $modal-body-bg; padding: #{3 * $grid-size} #{2 * $grid-size}; - .page-title { - margin-top: 0; - + .page-title, + .modal-title { .color-label { font-size: $gl-font-size; padding: $gl-vert-padding $label-padding-modal; } } + + .page-title { + margin-top: 0; + } } .modal-body { diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index a5a8f6d2206..a81904d5338 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -5,9 +5,9 @@ $grid-size: 8px; $gutter_collapsed_width: 62px; $gutter_width: 290px; $gutter_inner_width: 250px; -$sidebar-transition-duration: .3s; +$sidebar-transition-duration: 0.3s; $sidebar-breakpoint: 1024px; -$default-transition-duration: .15s; +$default-transition-duration: 0.15s; $contextual-sidebar-width: 220px; $contextual-sidebar-collapsed-width: 50px; @@ -129,7 +129,6 @@ $theme-green-800: #145d33; $theme-green-900: #0d4524; $theme-green-950: #072d16; - $black: #000; $black-transparent: rgba(0, 0, 0, 0.3); $almost-black: #242424; @@ -163,7 +162,7 @@ $gl-text-color-secondary: #707070; $gl-text-color-tertiary: #949494; $gl-text-color-quaternary: #d6d6d6; $gl-text-color-inverted: rgba(255, 255, 255, 1); -$gl-text-color-secondary-inverted: rgba(255, 255, 255, .85); +$gl-text-color-secondary-inverted: rgba(255, 255, 255, 0.85); $gl-text-color-disabled: #919191; $gl-text-green: $green-600; $gl-text-green-hover: $green-700; @@ -262,6 +261,7 @@ $highlight-changes-color: rgb(235, 255, 232); $performance-bar-height: 35px; $flash-height: 52px; $context-header-height: 60px; +$breadcrumb-min-height: 48px; /* * Common component specific colors @@ -296,7 +296,7 @@ $tanuki-yellow: #fca326; */ $gl-primary: $blue-500; $gl-success: $green-500; -$gl-success-focus: rgba($gl-success, .4); +$gl-success-focus: rgba($gl-success, 0.4); $gl-info: $blue-500; $gl-warning: $orange-500; $gl-danger: $red-500; @@ -331,8 +331,11 @@ $diff-jagged-border-gradient-color: darken($white-normal, 8%); /* * Fonts */ -$monospace_font: 'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace; -$regular_font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; +$monospace_font: 'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', + 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace; +$regular_font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, + Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif, + 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; /* * Dropdowns @@ -343,16 +346,16 @@ $dropdown-max-height: 312px; $dropdown-vertical-offset: 4px; $dropdown-link-color: #555; $dropdown-link-hover-bg: $row-hover; -$dropdown-empty-row-bg: rgba(#000, .04); +$dropdown-empty-row-bg: rgba(#000, 0.04); $dropdown-border-color: $border-color; -$dropdown-shadow-color: rgba(#000, .1); -$dropdown-divider-color: rgba(#000, .1); +$dropdown-shadow-color: rgba(#000, 0.1); +$dropdown-divider-color: rgba(#000, 0.1); $dropdown-title-btn-color: #bfbfbf; $dropdown-input-color: #555; $dropdown-input-fa-color: #c7c7c7; $dropdown-input-focus-border: $focus-border-color; -$dropdown-input-focus-shadow: rgba($dropdown-input-focus-border, .4); -$dropdown-loading-bg: rgba(#fff, .6); +$dropdown-input-focus-shadow: rgba($dropdown-input-focus-border, 0.4); +$dropdown-loading-bg: rgba(#fff, 0.6); $dropdown-chevron-size: 10px; $dropdown-toggle-active-border-color: darken($border-color, 14%); $dropdown-item-hover-bg: $gray-darker; @@ -367,9 +370,9 @@ $dropdown-hover-color: $blue-400; /* * Contextual Sidebar */ -$link-active-background: rgba(0, 0, 0, .04); -$link-hover-background: rgba(0, 0, 0, .06); -$inactive-badge-background: rgba(0, 0, 0, .08); +$link-active-background: rgba(0, 0, 0, 0.04); +$link-hover-background: rgba(0, 0, 0, 0.06); +$inactive-badge-background: rgba(0, 0, 0, 0.08); /* * Buttons @@ -397,14 +400,14 @@ $status-icon-margin: $gl-btn-padding; /* * Award emoji */ -$award-emoji-menu-shadow: rgba(0, 0, 0, .175); +$award-emoji-menu-shadow: rgba(0, 0, 0, 0.175); $award-emoji-positive-add-bg: #fed159; $award-emoji-positive-add-lines: #bb9c13; /* * Search Box */ -$search-input-border-color: rgba($blue-400, .8); +$search-input-border-color: rgba($blue-400, 0.8); $search-input-focus-shadow-color: $dropdown-input-focus-shadow; $search-input-width: 220px; $location-badge-active-bg: $blue-500; @@ -429,7 +432,7 @@ $zen-control-color: #555; * Calendar */ $calendar-hover-bg: #ecf3fe; -$calendar-border-color: rgba(#000, .1); +$calendar-border-color: rgba(#000, 0.1); $calendar-user-contrib-text: #959494; /* @@ -452,6 +455,17 @@ $ci-skipped-color: #888; */ $issue-boards-font-size: 14px; $issue-boards-card-shadow: rgba(186, 186, 186, 0.5); +/* + The following heights are used in boards.scss and are used for calculation of the board height. + They probably should be derived in a smarter way. +*/ +$issue-boards-filter-height: 68px; +$issue-boards-breadcrumbs-height-xs: 63px; +$issue-board-list-difference-xs: $header-height + + $issue-boards-breadcrumbs-height-xs; +$issue-board-list-difference-sm: $header-height + $breadcrumb-min-height; +$issue-board-list-difference-md: $issue-board-list-difference-sm + + $issue-boards-filter-height; /* * Avatar @@ -567,14 +581,14 @@ $label-padding: 7px; $label-padding-modal: 10px; $label-gray-bg: #f8fafc; $label-inverse-bg: #333; -$label-remove-border: rgba(0, 0, 0, .1); +$label-remove-border: rgba(0, 0, 0, 0.1); $label-border-radius: 100px; /* * Animation */ $fade-in-duration: 200ms; -$fade-mask-transition-duration: .1s; +$fade-mask-transition-duration: 0.1s; $fade-mask-transition-curve: ease-in-out; /* @@ -642,7 +656,6 @@ $stat-graph-selection-stroke: #333; $select2-drop-shadow1: rgba(76, 86, 103, 0.247059); $select2-drop-shadow2: rgba(31, 37, 50, 0.317647); - /* * Todo */ @@ -679,7 +692,6 @@ CI variable lists */ $ci-variable-remove-button-width: calc(1em + #{2 * $gl-padding}); - /* Filtered Search */ @@ -706,7 +718,14 @@ Repo editor */ $repo-editor-grey: #f6f7f9; $repo-editor-grey-darker: #e9ebee; -$repo-editor-linear-gradient: linear-gradient(to right, $repo-editor-grey 0%, $repo-editor-grey-darker, 20%, $repo-editor-grey 40%, $repo-editor-grey 100%); +$repo-editor-linear-gradient: linear-gradient( + to right, + $repo-editor-grey 0%, + $repo-editor-grey-darker, + 20%, + $repo-editor-grey 40%, + $repo-editor-grey 100% +); /* Performance Bar @@ -717,8 +736,8 @@ $perf-bar-staging: #291430; $perf-bar-development: #4c1210; $perf-bar-bucket-bg: #111; $perf-bar-bucket-color: #ccc; -$perf-bar-bucket-box-shadow-from: rgba($white-light, .2); -$perf-bar-bucket-box-shadow-to: rgba($black, .25); +$perf-bar-bucket-box-shadow-from: rgba($white-light, 0.2); +$perf-bar-bucket-box-shadow-to: rgba($black, 0.25); /* Issuable warning diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 2803144ef1d..c03d4c2eebf 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -1,4 +1,4 @@ -@import "./issues/issue_count_badge"; +@import './issues/issue_count_badge'; [v-cloak] { display: none; @@ -72,22 +72,37 @@ } .boards-list { - height: calc(100vh - 105px); + height: calc(100vh - #{$issue-board-list-difference-xs}); width: 100%; - padding-top: 25px; - padding-bottom: 25px; - padding-right: ($gl-padding / 2); - padding-left: ($gl-padding / 2); + padding: $gl-padding ($gl-padding / 2); overflow-x: scroll; white-space: nowrap; + min-height: 200px; @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { - height: calc(100vh - 90px); + height: calc(100vh - #{$issue-board-list-difference-sm}); } @media (min-width: $screen-md-min) { - height: calc(100vh - 160px); - min-height: 475px; + height: calc(100vh - #{$issue-board-list-difference-md}); + } + + .with-performance-bar & { + height: calc( + 100vh - #{$issue-board-list-difference-xs} - #{$performance-bar-height} + ); + + @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { + height: calc( + 100vh - #{$issue-board-list-difference-sm} - #{$performance-bar-height} + ); + } + + @media (min-width: $screen-md-min) { + height: calc( + 100vh - #{$issue-board-list-difference-md} - #{$performance-bar-height} + ); + } } } @@ -454,7 +469,7 @@ &.boards-sidebar-slide-enter-active, &.boards-sidebar-slide-leave-active { transition: width $sidebar-transition-duration, - padding $sidebar-transition-duration; + padding $sidebar-transition-duration; } &.boards-sidebar-slide-enter, @@ -473,7 +488,7 @@ right: 0; bottom: 0; left: 0; - background-color: rgba($black, .3); + background-color: rgba($black, 0.3); z-index: 9999; } @@ -490,7 +505,7 @@ padding: 25px 15px 0; background-color: $white-light; border-radius: $border-radius-default; - box-shadow: 0 2px 12px rgba($black, .5); + box-shadow: 0 2px 12px rgba($black, 0.5); .empty-state { display: -webkit-flex; @@ -568,7 +583,7 @@ .card { border: 1px solid $border-gray-dark; - box-shadow: 0 1px 2px rgba($issue-boards-card-shadow, .3); + box-shadow: 0 1px 2px rgba($issue-boards-card-shadow, 0.3); cursor: pointer; } } diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 8b680c2dc52..b487f6278c2 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -194,8 +194,6 @@ .commit-actions { @media (min-width: $screen-sm-min) { - font-size: 0; - .fa-spinner { font-size: 12px; } @@ -204,7 +202,7 @@ .ci-status-link { display: inline-block; position: relative; - top: 1px; + top: 2px; } .btn-clipboard, @@ -226,7 +224,7 @@ .ci-status-icon { position: relative; - top: 1px; + top: 2px; } } diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 884665d35c7..58700661142 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -369,7 +369,8 @@ } > text { - font-size: 12px; + fill: $theme-gray-600; + font-size: 10px; } } diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 4c9732c26d9..e21a9f0afc9 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -137,12 +137,22 @@ z-index: 200; overflow: hidden; - a:not(.btn-retry), - .btn-link { + a:not(.btn) { color: inherit; + + &:hover { + color: $gl-link-hover-color; + + .avatar { + border-color: rgba($avatar-border, .2); + } + + } + } .btn-link { + color: inherit; outline: none; } @@ -214,7 +224,7 @@ &:hover { text-decoration: underline; - color: $md-link-color; + color: $gl-link-hover-color; } } } @@ -486,16 +496,6 @@ } } - a:not(.btn-retry) { - &:hover { - color: $md-link-color; - - .avatar { - border-color: rgba($avatar-border, .2); - } - } - } - .dropdown-menu-toggle { width: 100%; padding-top: 6px; @@ -503,6 +503,20 @@ .dropdown-menu { width: 100%; + + /* + * Overwrite hover style for dropdown items, so that they are not blue + * This should be removed during dev of https://gitlab.com/gitlab-org/gitlab-ce/issues/44040 + */ + li a { + &:hover, + &:active, + &:focus, + &.is-focused { + @include dropdown-item-hover; + } + } + } } diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index f887a11004f..4692d0fb873 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -718,6 +718,8 @@ } .mr-memory-usage { + width: 100%; + p.usage-info-loading .usage-info-load-spinner { margin-right: 10px; font-size: 16px; @@ -727,3 +729,36 @@ .fork-sprite { margin-right: -5px; } + +.deploy-heading { + .media-body { + min-width: 0; + } +} + +.deploy-body { + display: flex; + flex-wrap: wrap; + + @media (min-width: $screen-xs) { + flex-wrap: nowrap; + white-space: nowrap; + } + + > *:not(:last-child) { + margin-right: .3em; + } +} + +.deploy-link { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + min-width: 100px; + max-width: 150px; + + @media (min-width: $screen-xs) { + min-width: 0; + max-width: 100%; + } +} diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 3c565837383..81e98f358a8 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -16,7 +16,7 @@ ul.notes { .note-created-ago, .note-updated-at { - white-space: nowrap; + white-space: normal; } .discussion-body { @@ -140,12 +140,6 @@ ul.notes { @include bulleted-list; word-wrap: break-word; - ul.task-list { - ul:not(.task-list) { - padding-left: 1.3em; - } - } - table { @include markdown-table; } diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss index 8265b8370f7..7a8fbfc517d 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/pages/repo.scss @@ -19,6 +19,7 @@ .ide-view { display: flex; height: calc(100vh - #{$header-height}); + margin-top: 40px; color: $almost-black; border-top: 1px solid $white-dark; border-bottom: 1px solid $white-dark; @@ -28,6 +29,11 @@ max-width: 250px; } } + + .file-status-icon { + width: 10px; + height: 10px; + } } .ide-file-list { @@ -40,31 +46,41 @@ background: $white-normal; } - .repo-file-name { + .ide-file-name { + flex: 1; white-space: nowrap; text-overflow: ellipsis; + + svg { + vertical-align: middle; + margin-right: 2px; + } + + .loading-container { + margin-right: 4px; + display: inline-block; + } } - .unsaved-icon { - color: $indigo-700; - float: right; - font-size: smaller; - line-height: 20px; + .ide-file-changed-icon { + margin-left: auto; } - .repo-new-btn { + .ide-new-btn { display: none; - margin-top: -4px; margin-bottom: -4px; + margin-right: -8px; } &:hover { - .repo-new-btn { + .ide-new-btn { display: block; } + } - .unsaved-icon { - display: none; + &.folder { + svg { + fill: $gl-text-color-secondary; } } } @@ -79,10 +95,10 @@ } } -.multi-file-table-name, -.multi-file-table-col-commit-message { +.file-name, +.file-col-commit-message { + display: flex; overflow: visible; - max-width: 0; padding: 6px 12px; } @@ -99,21 +115,6 @@ } } -table.table tr td.multi-file-table-name { - width: 350px; - padding: 6px 12px; - - svg { - vertical-align: middle; - margin-right: 2px; - } - - .loading-container { - margin-right: 4px; - display: inline-block; - } -} - .multi-file-table-col-commit-message { white-space: nowrap; width: 50%; @@ -129,13 +130,35 @@ table.table tr td.multi-file-table-name { .multi-file-tabs { display: flex; - overflow-x: auto; background-color: $white-normal; box-shadow: inset 0 -1px $white-dark; - > li { + > ul { + display: flex; + overflow-x: auto; + } + + li { position: relative; } + + .dropdown { + display: flex; + margin-left: auto; + margin-bottom: 1px; + padding: 0 $grid-size; + border-left: 1px solid $white-dark; + background-color: $white-light; + + &.shadow { + box-shadow: 0 0 10px $dropdown-shadow-color; + } + + .btn { + margin-top: auto; + margin-bottom: auto; + } + } } .multi-file-tab { @@ -160,20 +183,32 @@ table.table tr td.multi-file-table-name { position: absolute; right: 8px; top: 50%; + width: 16px; + height: 16px; padding: 0; background: none; border: 0; - font-size: $gl-font-size; - color: $gray-darkest; + border-radius: $border-radius-default; + color: $theme-gray-900; transform: translateY(-50%); - &:not(.modified):hover, - &:not(.modified):focus { - color: $hint-color; + svg { + position: relative; + top: -1px; } - &.modified { - color: $indigo-700; + &:hover { + background-color: $theme-gray-200; + } + + &:focus { + background-color: $blue-500; + color: $white-light; + outline: 0; + + svg { + fill: currentColor; + } } } @@ -192,6 +227,70 @@ table.table tr td.multi-file-table-name { .vertical-center { min-height: auto; } + + .monaco-editor .lines-content .cigr { + display: none; + } + + .monaco-diff-editor.vs { + .editor.modified { + box-shadow: none; + } + + .diagonal-fill { + display: none !important; + } + + .diffOverview { + background-color: $white-light; + border-left: 1px solid $white-dark; + cursor: ns-resize; + } + + .diffViewport { + display: none; + } + + .char-insert { + background-color: $line-added-dark; + } + + .char-delete { + background-color: $line-removed-dark; + } + + .line-numbers { + color: $black-transparent; + } + + .view-overlays { + .line-insert { + background-color: $line-added; + } + + .line-delete { + background-color: $line-removed; + } + } + + .margin { + background-color: $gray-light; + border-right: 1px solid $white-normal; + + .line-insert { + border-right: 1px solid $line-added-dark; + } + + .line-delete { + border-right: 1px solid $line-removed-dark; + } + } + + .margin-view-overlays .insert-sign, + .margin-view-overlays .delete-sign { + opacity: 0.4; + } + } } .multi-file-editor-holder { @@ -252,7 +351,7 @@ table.table tr td.multi-file-table-name { display: flex; position: relative; flex-direction: column; - width: 290px; + width: 340px; padding: 0; background-color: $gray-light; padding-right: 3px; @@ -350,6 +449,11 @@ table.table tr td.multi-file-table-name { flex: 1; } +.multi-file-commit-empty-state-container { + align-items: center; + justify-content: center; +} + .multi-file-commit-panel-header { display: flex; align-items: center; @@ -376,7 +480,7 @@ table.table tr td.multi-file-table-name { .multi-file-commit-panel-header-title { display: flex; flex: 1; - padding: $gl-btn-padding; + padding: 0 $gl-btn-padding; svg { margin-right: $gl-btn-padding; @@ -390,12 +494,34 @@ table.table tr td.multi-file-table-name { .multi-file-commit-list { flex: 1; overflow: auto; - padding: $gl-padding; + padding: $gl-padding 0; + min-height: 60px; } .multi-file-commit-list-item { display: flex; + padding: 0; align-items: center; + + .multi-file-discard-btn { + display: none; + margin-left: auto; + color: $gl-link-color; + padding: 0 2px; + + &:focus, + &:hover { + text-decoration: underline; + } + } + + &:hover { + background: $white-normal; + + .multi-file-discard-btn { + display: block; + } + } } .multi-file-addition { @@ -414,29 +540,58 @@ table.table tr td.multi-file-table-name { margin-left: auto; margin-right: auto; } + + .file-status-icon { + width: 10px; + height: 10px; + margin-left: 3px; + } } .multi-file-commit-list-path { + padding: $grid-size / 2; + padding-left: $gl-padding; + background: none; + border: 0; + text-align: left; + width: 100%; + min-width: 0; + + svg { + min-width: 16px; + vertical-align: middle; + display: inline-block; + } + + &:hover, + &:focus { + outline: 0; + } +} + +.multi-file-commit-list-file-path { @include str-truncated(100%); + + &:hover { + text-decoration: underline; + } + + &:active { + text-decoration: none; + } } .multi-file-commit-form { padding: $gl-padding; border-top: 1px solid $white-dark; -} - -.multi-file-commit-fieldset { - display: flex; - align-items: center; - padding-bottom: 12px; .btn { - flex: 1; + font-size: $gl-font-size; } } .multi-file-commit-message.form-control { - height: 80px; + height: 160px; resize: none; } @@ -468,7 +623,7 @@ table.table tr td.multi-file-table-name { top: 0; width: 100px; height: 1px; - background-color: rgba($red-500, .5); + background-color: rgba($red-500, 0.5); } } } @@ -487,7 +642,7 @@ table.table tr td.multi-file-table-name { justify-content: center; } -.repo-new-btn { +.ide-new-btn { .dropdown-toggle svg { margin-top: -2px; margin-bottom: 2px; @@ -505,36 +660,39 @@ table.table tr td.multi-file-table-name { } } -.ide.nav-only { - .flash-container { - margin-top: $header-height; - margin-bottom: 0; - } - - .alert-wrapper .flash-container .flash-alert:last-child, - .alert-wrapper .flash-container .flash-notice:last-child { - margin-bottom: 0; - } +.ide { + overflow: hidden; - .content { - margin-top: $header-height; - } + &.nav-only { + .flash-container { + margin-top: $header-height; + margin-bottom: 0; + } - .multi-file-commit-panel .multi-file-commit-panel-inner-scroll { - max-height: calc(100vh - #{$header-height + $context-header-height}); - } + .alert-wrapper .flash-container .flash-alert:last-child, + .alert-wrapper .flash-container .flash-notice:last-child { + margin-bottom: 0; + } - &.flash-shown { - .content { - margin-top: 0; + .content-wrapper { + margin-top: $header-height; + padding-bottom: 0; } - .ide-view { - height: calc(100vh - #{$header-height + $flash-height}); + &.flash-shown { + .content-wrapper { + margin-top: 0; + } + + .ide-view { + height: calc(100vh - #{$header-height + $flash-height}); + } } - .multi-file-commit-panel .multi-file-commit-panel-inner-scroll { - max-height: calc(100vh - #{$header-height + $flash-height + $context-header-height}); + .projects-sidebar { + .multi-file-commit-panel-inner-scroll { + flex: 1; + } } } } @@ -544,34 +702,28 @@ table.table tr td.multi-file-table-name { margin-top: #{$header-height + $performance-bar-height}; } - .content { + .content-wrapper { margin-top: #{$header-height + $performance-bar-height}; + padding-bottom: 0; } .ide-view { height: calc(100vh - #{$header-height + $performance-bar-height}); } - .multi-file-commit-panel .multi-file-commit-panel-inner-scroll { - max-height: calc(100vh - #{$header-height + $performance-bar-height + 60}); - } - &.flash-shown { - .content { + .content-wrapper { margin-top: 0; } .ide-view { - height: calc(100vh - #{$header-height + $performance-bar-height + $flash-height}); - } - - .multi-file-commit-panel .multi-file-commit-panel-inner-scroll { - max-height: calc(100vh - #{$header-height + $performance-bar-height + $flash-height + $context-header-height}); + height: calc( + 100vh - #{$header-height + $performance-bar-height + $flash-height} + ); } } } - .dragHandle { position: absolute; top: 0; @@ -587,3 +739,44 @@ table.table tr td.multi-file-table-name { left: 0; } } + +.ide-commit-radios { + label { + font-weight: normal; + } + + .help-block { + margin-top: 0; + line-height: 0; + } +} + +.ide-commit-new-branch { + margin-left: 25px; +} + +.ide-external-links { + p { + margin: 0; + } +} + +.ide-sidebar-link { + padding: $gl-padding-8 $gl-padding; + background: $indigo-700; + color: $white-light; + text-decoration: none; + display: flex; + align-items: center; + + &:focus, + &:hover { + color: $white-light; + text-decoration: underline; + background: $indigo-500; + } + + &:active { + background: $indigo-800; + } +} diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss index 47672783d5a..a6ca8ed5016 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -205,7 +205,8 @@ } .badge { - font-size: inherit; + font-size: 12px; + line-height: 12px; } .panel-heading .badge-count { diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss index e70a57c2a67..9a0ec936979 100644 --- a/app/assets/stylesheets/pages/wiki.scss +++ b/app/assets/stylesheets/pages/wiki.scss @@ -180,6 +180,11 @@ ul.wiki-pages-list.content-list { } } +.wiki-holder { + overflow-x: auto; + overflow-y: hidden; +} + .wiki { table { @include markdown-table; diff --git a/app/assets/stylesheets/performance_bar.scss b/app/assets/stylesheets/performance_bar.scss index 6e539e39ca1..5d1a9489aad 100644 --- a/app/assets/stylesheets/performance_bar.scss +++ b/app/assets/stylesheets/performance_bar.scss @@ -1,8 +1,8 @@ -@import "framework/variables"; -@import "peek/views/performance_bar"; -@import "peek/views/rblineprof"; +@import 'framework/variables'; +@import 'peek/views/performance_bar'; +@import 'peek/views/rblineprof'; -#peek { +#js-peek { position: fixed; left: 0; top: 0; @@ -21,20 +21,26 @@ &.production { background-color: $perf-bar-production; + + select { + background: $perf-bar-production; + } } &.staging { background-color: $perf-bar-staging; + + select { + background: $perf-bar-staging; + } } &.development { background-color: $perf-bar-development; - } - .wrapper { - width: 80%; - height: $performance-bar-height; - margin: 0 auto; + select { + background: $perf-bar-development; + } } // UI Elements @@ -42,11 +48,12 @@ background: $perf-bar-bucket-bg; display: inline-block; padding: 4px 6px; - font-family: Consolas, "Liberation Mono", Courier, monospace; + font-family: Consolas, 'Liberation Mono', Courier, monospace; line-height: 1; color: $perf-bar-bucket-color; border-radius: 3px; - box-shadow: 0 1px 0 $perf-bar-bucket-box-shadow-from, inset 0 1px 2px $perf-bar-bucket-box-shadow-to; + box-shadow: 0 1px 0 $perf-bar-bucket-box-shadow-from, + inset 0 1px 2px $perf-bar-bucket-box-shadow-to; .hidden { display: none; @@ -94,6 +101,10 @@ max-width: 10000px !important; } } + + .performance-bar-modal .modal-footer { + display: none; + } } #modal-peek-pg-queries-content { diff --git a/app/controllers/admin/impersonation_tokens_controller.rb b/app/controllers/admin/impersonation_tokens_controller.rb index 7a2c7234a1e..a7b562b1d8e 100644 --- a/app/controllers/admin/impersonation_tokens_controller.rb +++ b/app/controllers/admin/impersonation_tokens_controller.rb @@ -9,7 +9,6 @@ class Admin::ImpersonationTokensController < Admin::ApplicationController @impersonation_token = finder.build(impersonation_token_params) if @impersonation_token.save - flash[:impersonation_token] = @impersonation_token.token redirect_to admin_user_impersonation_tokens_path, notice: "A new impersonation token has been created." else set_index_vars diff --git a/app/controllers/boards/issues_controller.rb b/app/controllers/boards/issues_controller.rb index 352f12a89fd..19dbee84c11 100644 --- a/app/controllers/boards/issues_controller.rb +++ b/app/controllers/boards/issues_controller.rb @@ -1,6 +1,9 @@ module Boards class IssuesController < Boards::ApplicationController include BoardsResponses + include ControllerWithCrossProjectAccessCheck + + requires_cross_project_access if: -> { board&.group_board? } before_action :whitelist_query_limiting, only: [:index, :update] before_action :authorize_read_issue, only: [:index] @@ -64,11 +67,19 @@ module Boards end def issues_finder - IssuesFinder.new(current_user, project_id: board_parent.id) + if board.group_board? + IssuesFinder.new(current_user, group_id: board_parent.id) + else + IssuesFinder.new(current_user, project_id: board_parent.id) + end end def project - board_parent + @project ||= if board.group_board? + Project.find(issue_params[:project_id]) + else + board_parent + end end def move_params diff --git a/app/controllers/concerns/authenticates_with_two_factor.rb b/app/controllers/concerns/authenticates_with_two_factor.rb index db8c362f125..2753f83c3cf 100644 --- a/app/controllers/concerns/authenticates_with_two_factor.rb +++ b/app/controllers/concerns/authenticates_with_two_factor.rb @@ -56,6 +56,7 @@ module AuthenticatesWithTwoFactor session.delete(:otp_user_id) remember_me(user) if user_params[:remember_me] == '1' + user.save! sign_in(user) else user.increment_failed_attempts! diff --git a/app/controllers/concerns/boards_responses.rb b/app/controllers/concerns/boards_responses.rb index a145049dc7d..da830ec2cb1 100644 --- a/app/controllers/concerns/boards_responses.rb +++ b/app/controllers/concerns/boards_responses.rb @@ -1,10 +1,46 @@ module BoardsResponses + include Gitlab::Utils::StrongMemoize + + def board_params + params.require(:board).permit(:name, :weight, :milestone_id, :assignee_id, label_ids: []) + end + + def parent + strong_memoize(:parent) do + group? ? group : project + end + end + + def boards_path + if group? + group_boards_path(parent) + else + project_boards_path(parent) + end + end + + def board_path(board) + if group? + group_board_path(parent, board) + else + project_board_path(parent, board) + end + end + + def group? + instance_variable_defined?(:@group) + end + def authorize_read_list - authorize_action_for!(board.parent, :read_list) + ability = board.group_board? ? :read_group : :read_list + + authorize_action_for!(board.parent, ability) end def authorize_read_issue - authorize_action_for!(board.parent, :read_issue) + ability = board.group_board? ? :read_group : :read_issue + + authorize_action_for!(board.parent, ability) end def authorize_update_issue @@ -31,6 +67,10 @@ module BoardsResponses respond_with(@board) # rubocop:disable Gitlab/ModuleWithInstanceVariables end + def serialize_as_json(resource) + resource.as_json(only: [:id]) + end + def respond_with(resource) respond_to do |format| format.html diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index 6f4fdcdaa4f..b26a76d2b62 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -4,7 +4,7 @@ module CreatesCommit # rubocop:disable Gitlab/ModuleWithInstanceVariables def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil) - if can?(current_user, :push_code, @project) + if user_access(@project).can_push_to_branch?(branch_name_or_ref) @project_to_commit_into = @project @branch_name ||= @ref else @@ -50,7 +50,7 @@ module CreatesCommit # rubocop:enable Gitlab/ModuleWithInstanceVariables def authorize_edit_tree! - return if can_collaborate_with_project? + return if can_collaborate_with_project?(project, ref: branch_name_or_ref) access_denied! end @@ -123,4 +123,8 @@ module CreatesCommit params[:create_merge_request].present? && (different_project? || @start_branch != @branch_name) # rubocop:disable Gitlab/ModuleWithInstanceVariables end + + def branch_name_or_ref + @branch_name || @ref # rubocop:disable Gitlab/ModuleWithInstanceVariables + end end diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb index f7ba305a59f..4114ca6bf7c 100644 --- a/app/controllers/concerns/issuable_collections.rb +++ b/app/controllers/concerns/issuable_collections.rb @@ -17,7 +17,7 @@ module IssuableCollections set_pagination return if redirect_out_of_range(@total_pages) - if params[:label_name].present? + if params[:label_name].present? && @project labels_params = { project_id: @project.id, title: params[:label_name] } @labels = LabelsFinder.new(current_user, labels_params).execute end diff --git a/app/controllers/groups/boards_controller.rb b/app/controllers/groups/boards_controller.rb new file mode 100644 index 00000000000..7c2016f0326 --- /dev/null +++ b/app/controllers/groups/boards_controller.rb @@ -0,0 +1,27 @@ +class Groups::BoardsController < Groups::ApplicationController + include BoardsResponses + + before_action :assign_endpoint_vars + + def index + @boards = Boards::ListService.new(group, current_user).execute + + respond_with_boards + end + + def show + @board = group.boards.find(params[:id]) + + respond_with_board + end + + def assign_endpoint_vars + @boards_endpoint = group_boards_url(group) + @namespace_path = group.to_param + @labels_endpoint = group_labels_url(group) + end + + def serialize_as_json(resource) + resource.as_json(only: [:id]) + end +end diff --git a/app/controllers/groups/labels_controller.rb b/app/controllers/groups/labels_controller.rb index f3a9e591c3e..58be330f466 100644 --- a/app/controllers/groups/labels_controller.rb +++ b/app/controllers/groups/labels_controller.rb @@ -14,7 +14,14 @@ class Groups::LabelsController < Groups::ApplicationController end format.json do - available_labels = LabelsFinder.new(current_user, group_id: @group.id).execute + available_labels = LabelsFinder.new( + current_user, + group_id: @group.id, + only_group_labels: params[:only_group_labels], + include_ancestor_groups: params[:include_ancestor_groups], + include_descendant_groups: params[:include_descendant_groups] + ).execute + render json: LabelSerializer.new.represent_appearance(available_labels) end end @@ -28,10 +35,18 @@ class Groups::LabelsController < Groups::ApplicationController def create @label = Labels::CreateService.new(label_params).execute(group: group) - if @label.valid? - redirect_to group_labels_path(@group) - else - render :new + respond_to do |format| + format.html do + if @label.valid? + redirect_to group_labels_path(@group) + else + render :new + end + end + + format.json do + render json: LabelSerializer.new.represent_appearance(@label) + end end end diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb index 69fb8121ded..eb7d5fca367 100644 --- a/app/controllers/import/github_controller.rb +++ b/app/controllers/import/github_controller.rb @@ -42,7 +42,9 @@ class Import::GithubController < Import::BaseController target_namespace = find_or_create_namespace(namespace_path, current_user.namespace_path) if can?(current_user, :create_projects, target_namespace) - project = Gitlab::LegacyGithubImport::ProjectCreator.new(repo, project_name, target_namespace, current_user, access_params, type: provider).execute + project = Gitlab::LegacyGithubImport::ProjectCreator + .new(repo, project_name, target_namespace, current_user, access_params, type: provider) + .execute(extra_project_attrs) if project.persisted? render json: ProjectSerializer.new.represent(project) @@ -73,15 +75,15 @@ class Import::GithubController < Import::BaseController end def new_import_url - public_send("new_import_#{provider}_url") # rubocop:disable GitlabSecurity/PublicSend + public_send("new_import_#{provider}_url", extra_import_params) # rubocop:disable GitlabSecurity/PublicSend end def status_import_url - public_send("status_import_#{provider}_url") # rubocop:disable GitlabSecurity/PublicSend + public_send("status_import_#{provider}_url", extra_import_params) # rubocop:disable GitlabSecurity/PublicSend end def callback_import_url - public_send("callback_import_#{provider}_url") # rubocop:disable GitlabSecurity/PublicSend + public_send("callback_import_#{provider}_url", extra_import_params) # rubocop:disable GitlabSecurity/PublicSend end def provider_unauthorized @@ -116,4 +118,12 @@ class Import::GithubController < Import::BaseController def client_options {} end + + def extra_project_attrs + {} + end + + def extra_import_params + {} + end end diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb index 52430ea771f..025d8270b7c 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -62,7 +62,7 @@ class InvitesController < ApplicationController case source when Project project = member.source - label = "project #{project.name_with_namespace}" + label = "project #{project.full_name}" path = project_path(project) when Group group = member.source diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index 6025a40348b..6d9b42a2c04 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -6,7 +6,7 @@ class Projects::ApplicationController < ApplicationController before_action :repository layout 'project' - helper_method :repository, :can_collaborate_with_project? + helper_method :repository, :can_collaborate_with_project?, :user_access private @@ -31,11 +31,12 @@ class Projects::ApplicationController < ApplicationController @repository ||= project.repository end - def can_collaborate_with_project?(project = nil) + def can_collaborate_with_project?(project = nil, ref: nil) project ||= @project can?(current_user, :push_code, project) || - (current_user && current_user.already_forked?(project)) + (current_user && current_user.already_forked?(project)) || + user_access(project).can_push_to_branch?(ref) end def authorize_action!(action) @@ -90,4 +91,9 @@ class Projects::ApplicationController < ApplicationController def check_issues_available! return render_404 unless @project.feature_available?(:issues, current_user) end + + def user_access(project) + @user_access ||= {} + @user_access[project] ||= Gitlab::UserAccess.new(current_user, project: project) + end end diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 74c25505e36..0c1c286a0a4 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -9,8 +9,12 @@ class Projects::BlobController < Projects::ApplicationController before_action :require_non_empty_project, except: [:new, :create] before_action :authorize_download_code! - before_action :authorize_edit_tree!, only: [:new, :create, :update, :destroy] + + # We need to assign the blob vars before `authorize_edit_tree!` so we can + # validate access to a specific ref. before_action :assign_blob_vars + before_action :authorize_edit_tree!, only: [:new, :create, :update, :destroy] + before_action :commit, except: [:new, :create] before_action :blob, except: [:new, :create] before_action :require_branch_head, only: [:edit, :update] @@ -38,7 +42,7 @@ class Projects::BlobController < Projects::ApplicationController end format.json do - page_title @blob.path, @ref, @project.name_with_namespace + page_title @blob.path, @ref, @project.full_name show_json end @@ -46,7 +50,7 @@ class Projects::BlobController < Projects::ApplicationController end def edit - if can_collaborate_with_project? + if can_collaborate_with_project?(project, ref: @ref) blob.load_all_data! else redirect_to action: 'show' diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index cabafe26357..965cece600e 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -7,13 +7,19 @@ class Projects::BranchesController < Projects::ApplicationController before_action :authorize_download_code! before_action :authorize_push_code!, only: [:new, :create, :destroy, :destroy_all_merged] - def index - @sort = params[:sort].presence || sort_value_recently_updated - @branches = BranchesFinder.new(@repository, params.merge(sort: @sort)).execute - @branches = Kaminari.paginate_array(@branches).page(params[:page]) + # Support legacy URLs + before_action :redirect_for_legacy_index_sort_or_search, only: [:index] + def index respond_to do |format| format.html do + @sort = params[:sort].presence || sort_value_recently_updated + @mode = params[:state].presence || 'overview' + @overview_max_branches = 5 + + # Fetch branches for the specified mode + fetch_branches_by_mode + @refs_pipelines = @project.pipelines.latest_successful_for_refs(@branches.map(&:name)) @merged_branch_names = repository.merged_branch_names(@branches.map(&:name)) @@ -28,7 +34,9 @@ class Projects::BranchesController < Projects::ApplicationController end end format.json do - render json: @branches.map(&:name) + branches = BranchesFinder.new(@repository, params).execute + branches = Kaminari.paginate_array(branches).page(params[:page]) + render json: branches.map(&:name) end end end @@ -123,4 +131,27 @@ class Projects::BranchesController < Projects::ApplicationController context: 'autodeploy' ) end + + def redirect_for_legacy_index_sort_or_search + # Normalize a legacy URL with redirect + if request.format != :json && !params[:state].presence && [:sort, :search, :page].any? { |key| params[key].presence } + redirect_to project_branches_filtered_path(@project, state: 'all'), notice: 'Update your bookmarked URLs as filtered/sorted branches URL has been changed.' + end + end + + def fetch_branches_by_mode + if @mode == 'overview' + # overview mode + @active_branches, @stale_branches = BranchesFinder.new(@repository, sort: sort_value_recently_updated).execute.partition(&:active?) + # Here we get one more branch to indicate if there are more data we're not showing + @active_branches = @active_branches.first(@overview_max_branches + 1) + @stale_branches = @stale_branches.first(@overview_max_branches + 1) + @branches = @active_branches + @stale_branches + else + # active/stale/all view mode + @branches = BranchesFinder.new(@repository, params.merge(sort: @sort)).execute + @branches = @branches.select { |b| b.state.to_s == @mode } if %w[active stale].include?(@mode) + @branches = Kaminari.paginate_array(@branches).page(params[:page]) + end + end end diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb index 1d910e461b1..7b7cb52d7ed 100644 --- a/app/controllers/projects/commits_controller.rb +++ b/app/controllers/projects/commits_controller.rb @@ -14,37 +14,31 @@ class Projects::CommitsController < Projects::ApplicationController @merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened .find_by(source_project: @project, source_branch: @ref, target_branch: @repository.root_ref) - # https://gitlab.com/gitlab-org/gitaly/issues/931 - Gitlab::GitalyClient.allow_n_plus_1_calls do - respond_to do |format| - format.html - format.atom { render layout: 'xml.atom' } + respond_to do |format| + format.html + format.atom { render layout: 'xml.atom' } - format.json do - pager_json( - 'projects/commits/_commits', - @commits.size, - project: @project, - ref: @ref) - end + format.json do + pager_json( + 'projects/commits/_commits', + @commits.size, + project: @project, + ref: @ref) end end end def signatures - # https://gitlab.com/gitlab-org/gitaly/issues/931 - Gitlab::GitalyClient.allow_n_plus_1_calls do - respond_to do |format| - format.json do - render json: { - signatures: @commits.select(&:has_signature?).map do |commit| - { - commit_sha: commit.sha, - html: view_to_html_string('projects/commit/_signature', signature: commit.signature) - } - end - } - end + respond_to do |format| + format.json do + render json: { + signatures: @commits.select(&:has_signature?).map do |commit| + { + commit_sha: commit.sha, + html: view_to_html_string('projects/commit/_signature', signature: commit.signature) + } + end + } end end end diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index 3cb4eb23981..2b0c2ca97c0 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -17,10 +17,8 @@ class Projects::CompareController < Projects::ApplicationController def show apply_diff_view_cookie! - # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37430 - Gitlab::GitalyClient.allow_n_plus_1_calls do - render - end + + render end def diff_for_path diff --git a/app/controllers/projects/deployments_controller.rb b/app/controllers/projects/deployments_controller.rb index 1a418d0f15a..b68cdc39cb8 100644 --- a/app/controllers/projects/deployments_controller.rb +++ b/app/controllers/projects/deployments_controller.rb @@ -24,7 +24,7 @@ class Projects::DeploymentsController < Projects::ApplicationController end def additional_metrics - return render_404 unless deployment.has_additional_metrics? + return render_404 unless deployment.has_metrics? respond_to do |format| format.json do diff --git a/app/controllers/projects/discussions_controller.rb b/app/controllers/projects/discussions_controller.rb index ee507009e50..cba9a53dc4b 100644 --- a/app/controllers/projects/discussions_controller.rb +++ b/app/controllers/projects/discussions_controller.rb @@ -19,6 +19,12 @@ class Projects::DiscussionsController < Projects::ApplicationController render_discussion end + def show + render json: { + discussion_html: view_to_html_string('discussions/_diff_with_notes', discussion: discussion, expanded: true) + } + end + private def render_discussion diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb index e0f4710175f..99790b8e7e8 100644 --- a/app/controllers/projects/labels_controller.rb +++ b/app/controllers/projects/labels_controller.rb @@ -112,12 +112,14 @@ class Projects::LabelsController < Projects::ApplicationController begin return render_404 unless promote_service.execute(@label) + flash[:notice] = "#{@label.title} promoted to group label." respond_to do |format| format.html do - redirect_to(project_labels_path(@project), - notice: 'Label was promoted to a Group Label') + redirect_to(project_labels_path(@project), status: 303) + end + format.json do + render json: { url: project_labels_path(@project) } end - format.js end rescue ActiveRecord::RecordInvalid => e Gitlab::AppLogger.error "Failed to promote label \"#{@label.title}\" to group label" diff --git a/app/controllers/projects/merge_requests/application_controller.rb b/app/controllers/projects/merge_requests/application_controller.rb index 793ae03fb88..67d4ea2ca8f 100644 --- a/app/controllers/projects/merge_requests/application_controller.rb +++ b/app/controllers/projects/merge_requests/application_controller.rb @@ -15,6 +15,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont def merge_request_params_attributes [ + :allow_maintainer_to_push, :assignee_id, :description, :force_remove_source_branch, diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index a1af125547c..54e7d81de6a 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -187,7 +187,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo begin @merge_request.environments_for(current_user).map do |environment| project = environment.project - deployment = environment.first_deployment_for(@merge_request.diff_head_commit) + deployment = environment.first_deployment_for(@merge_request.diff_head_sha) stop_url = if environment.stop_action? && can?(current_user, :create_deployment, environment) diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index 75b17d05e22..ff93147d00f 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -70,9 +70,17 @@ class Projects::MilestonesController < Projects::ApplicationController end def promote - promoted_milestone = Milestones::PromoteService.new(project, current_user).execute(milestone) - flash[:notice] = "Milestone has been promoted to group milestone." - redirect_to group_milestone_path(project.group, promoted_milestone.iid) + Milestones::PromoteService.new(project, current_user).execute(milestone) + + flash[:notice] = "#{milestone.title} promoted to group milestone" + respond_to do |format| + format.html do + redirect_to project_milestones_path(project) + end + format.json do + render json: { url: project_milestones_path(project) } + end + end rescue Milestones::PromoteService::PromoteMilestoneError => error redirect_to milestone, alert: error.message end diff --git a/app/controllers/projects/network_controller.rb b/app/controllers/projects/network_controller.rb index 3b10a93e97f..35fec229db7 100644 --- a/app/controllers/projects/network_controller.rb +++ b/app/controllers/projects/network_controller.rb @@ -9,25 +9,22 @@ class Projects::NetworkController < Projects::ApplicationController before_action :assign_commit def show - # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37602 - Gitlab::GitalyClient.allow_n_plus_1_calls do - @url = project_network_path(@project, @ref, @options.merge(format: :json)) - @commit_url = project_commit_path(@project, 'ae45ca32').gsub("ae45ca32", "%s") - - respond_to do |format| - format.html do - if @options[:extended_sha1] && !@commit - flash.now[:alert] = "Git revision '#{@options[:extended_sha1]}' does not exist." - end - end + @url = project_network_path(@project, @ref, @options.merge(format: :json)) + @commit_url = project_commit_path(@project, 'ae45ca32').gsub("ae45ca32", "%s") - format.json do - @graph = Network::Graph.new(project, @ref, @commit, @options[:filter_ref]) + respond_to do |format| + format.html do + if @options[:extended_sha1] && !@commit + flash.now[:alert] = "Git revision '#{@options[:extended_sha1]}' does not exist." end end - render + format.json do + @graph = Network::Graph.new(project, @ref, @commit, @options[:filter_ref]) + end end + + render end def assign_commit diff --git a/app/controllers/projects/prometheus/metrics_controller.rb b/app/controllers/projects/prometheus/metrics_controller.rb index b739d0f0f90..1dd886409a5 100644 --- a/app/controllers/projects/prometheus/metrics_controller.rb +++ b/app/controllers/projects/prometheus/metrics_controller.rb @@ -2,11 +2,12 @@ module Projects module Prometheus class MetricsController < Projects::ApplicationController before_action :authorize_admin_project! + before_action :require_prometheus_metrics! def active_common respond_to do |format| format.json do - matched_metrics = prometheus_service.matched_metrics || {} + matched_metrics = prometheus_adapter.query(:matched_metrics) || {} if matched_metrics.any? render json: matched_metrics @@ -19,8 +20,12 @@ module Projects private - def prometheus_service - @prometheus_service ||= project.find_or_initialize_service('prometheus') + def prometheus_adapter + @prometheus_adapter ||= ::Prometheus::AdapterService.new(project).prometheus_adapter + end + + def require_prometheus_metrics! + render_404 unless prometheus_adapter.can_query? end end end diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index daa5c88aae0..f14cb5f6a9f 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -3,7 +3,8 @@ class Projects::ServicesController < Projects::ApplicationController # Authorize before_action :authorize_admin_project! - before_action :service, only: [:edit, :update, :test] + before_action :ensure_service_enabled + before_action :service respond_to :html @@ -23,26 +24,30 @@ class Projects::ServicesController < Projects::ApplicationController end def test - message = {} + if @service.can_test? + render json: service_test_response, status: :ok + else + render json: {}, status: :not_found + end + end - if @service.can_test? && @service.update_attributes(service_params[:service]) + private + + def service_test_response + if @service.update_attributes(service_params[:service]) data = @service.test_data(project, current_user) outcome = @service.test(data) - unless outcome[:success] - message = { error: true, message: 'Test failed.', service_response: outcome[:result].to_s } + if outcome[:success] + {} + else + { error: true, message: 'Test failed.', service_response: outcome[:result].to_s } end - - status = :ok else - status = :not_found + { error: true, message: 'Validations failed.', service_response: @service.errors.full_messages.join(',') } end - - render json: message, status: status end - private - def success_message if @service.active? "#{@service.title} activated." @@ -54,4 +59,8 @@ class Projects::ServicesController < Projects::ApplicationController def service @service ||= @project.find_or_initialize_service(params[:id]) end + + def ensure_service_enabled + render_404 unless service + end end diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb index 86717bb7242..259809f3429 100644 --- a/app/controllers/projects/settings/ci_cd_controller.rb +++ b/app/controllers/projects/settings/ci_cd_controller.rb @@ -13,12 +13,14 @@ module Projects def reset_cache if ResetProjectCacheService.new(@project, current_user).execute - flash[:notice] = _("Project cache successfully reset.") + respond_to do |format| + format.json { head :ok } + end else - flash[:error] = _("Unable to reset project cache.") + respond_to do |format| + format.json { head :bad_request } + end end - - redirect_to project_pipelines_path(@project) end private diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb index f752a46f828..ee9b5458282 100644 --- a/app/controllers/projects/tree_controller.rb +++ b/app/controllers/projects/tree_controller.rb @@ -36,7 +36,7 @@ class Projects::TreeController < Projects::ApplicationController end format.json do - page_title @path.presence || _("Files"), @ref, @project.name_with_namespace + page_title @path.presence || _("Files"), @ref, @project.full_name # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/38261 Gitlab::GitalyClient.allow_n_plus_1_calls do diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 913689a1e74..ee197c75764 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -41,11 +41,11 @@ class ProjectsController < Projects::ApplicationController cookies[:issue_board_welcome_hidden] = { path: project_path(@project), value: nil, expires: Time.at(0) } redirect_to( - project_path(@project), + project_path(@project, custom_import_params), notice: _("Project '%{project_name}' was successfully created.") % { project_name: @project.name } ) else - render 'new', locals: { active_tab: ('import' if project_params[:import_url].present?) } + render 'new', locals: { active_tab: active_new_project_tab } end end @@ -103,7 +103,7 @@ class ProjectsController < Projects::ApplicationController def show if @project.import_in_progress? - redirect_to project_import_path(@project) + redirect_to project_import_path(@project, custom_import_params) return end @@ -130,7 +130,7 @@ class ProjectsController < Projects::ApplicationController return access_denied! unless can?(current_user, :remove_project, @project) ::Projects::DestroyService.new(@project, current_user, {}).async_execute - flash[:notice] = _("Project '%{project_name}' is in the process of being deleted.") % { project_name: @project.name_with_namespace } + flash[:notice] = _("Project '%{project_name}' is in the process of being deleted.") % { project_name: @project.full_name } redirect_to dashboard_projects_path, status: 302 rescue Projects::DestroyService::DestroyError => ex @@ -359,6 +359,14 @@ class ProjectsController < Projects::ApplicationController ] end + def custom_import_params + {} + end + + def active_new_project_tab + project_params[:import_url].present? ? 'import' : 'blank' + end + def repo_exists? project.repository_exists? && !project.empty_repo? diff --git a/app/finders/admin/projects_finder.rb b/app/finders/admin/projects_finder.rb index d6bcd939522..2c8f21c2400 100644 --- a/app/finders/admin/projects_finder.rb +++ b/app/finders/admin/projects_finder.rb @@ -16,8 +16,8 @@ class Admin::ProjectsFinder items = by_archived(items) items = by_personal(items) items = by_name(items) - items = sort(items) - items.includes(:namespace).order("namespaces.path, projects.name ASC").page(params[:page]) + items = items.includes(namespace: [:owner]) + sort(items).page(params[:page]) end private diff --git a/app/finders/branches_finder.rb b/app/finders/branches_finder.rb index 852eac3647d..8bb1366867c 100644 --- a/app/finders/branches_finder.rb +++ b/app/finders/branches_finder.rb @@ -1,5 +1,5 @@ class BranchesFinder - def initialize(repository, params) + def initialize(repository, params = {}) @repository = repository @params = params end diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 9dd6634b38f..b2d4f9938ff 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -19,6 +19,10 @@ # non_archived: boolean # iids: integer[] # my_reaction_emoji: string +# created_after: datetime +# created_before: datetime +# updated_after: datetime +# updated_before: datetime # class IssuableFinder prepend FinderWithCrossProjectAccess @@ -79,6 +83,7 @@ class IssuableFinder def filter_items(items) items = by_scope(items) items = by_created_at(items) + items = by_updated_at(items) items = by_state(items) items = by_group(items) items = by_search(items) @@ -283,6 +288,13 @@ class IssuableFinder end end + def by_updated_at(items) + items = items.updated_after(params[:updated_after]) if params[:updated_after].present? + items = items.updated_before(params[:updated_before]) if params[:updated_before].present? + + items + end + def by_state(items) case params[:state].to_s when 'closed' diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index d65c620e75a..2a27ff0e386 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -17,6 +17,10 @@ # my_reaction_emoji: string # public_only: boolean # due_date: date or '0', '', 'overdue', 'week', or 'month' +# created_after: datetime +# created_before: datetime +# updated_after: datetime +# updated_before: datetime # class IssuesFinder < IssuableFinder CONFIDENTIAL_ACCESS_LEVEL = Gitlab::Access::REPORTER diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb index 5c9fce211ec..780c0fdb03e 100644 --- a/app/finders/labels_finder.rb +++ b/app/finders/labels_finder.rb @@ -61,12 +61,20 @@ class LabelsFinder < UnionFinder def group_ids strong_memoize(:group_ids) do - group = Group.find(params[:group_id]) - groups = params[:include_ancestor_groups].present? ? group.self_and_ancestors : [group] - groups_user_can_read_labels(groups).map(&:id) + groups_user_can_read_labels(groups_to_include).map(&:id) end end + def groups_to_include + group = Group.find(params[:group_id]) + groups = [group] + + groups += group.ancestors if params[:include_ancestor_groups].present? + groups += group.descendants if params[:include_descendant_groups].present? + + groups + end + def group? params[:group_id].present? end diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb index 068ae7f8c89..64dc1e6af0f 100644 --- a/app/finders/merge_requests_finder.rb +++ b/app/finders/merge_requests_finder.rb @@ -19,6 +19,10 @@ # my_reaction_emoji: string # source_branch: string # target_branch: string +# created_after: datetime +# created_before: datetime +# updated_after: datetime +# updated_before: datetime # class MergeRequestsFinder < IssuableFinder def klass diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb index 33ee1e975b9..35f4ff2f62f 100644 --- a/app/finders/notes_finder.rb +++ b/app/finders/notes_finder.rb @@ -48,11 +48,23 @@ class NotesFinder def init_collection if target notes_on_target + elsif target_type + notes_of_target_type else notes_of_any_type end end + def notes_of_target_type + notes = notes_for_type(target_type) + + search(notes) + end + + def target_type + @params[:target_type] + end + def notes_of_any_type types = %w(commit issue merge_request snippet) note_relations = types.map { |t| notes_for_type(t) } diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb index a73c573736e..d498a2d6d11 100644 --- a/app/finders/snippets_finder.rb +++ b/app/finders/snippets_finder.rb @@ -58,11 +58,37 @@ class SnippetsFinder < UnionFinder .public_or_visible_to_user(current_user) end + # Returns a collection of projects that is either public or visible to the + # logged in user. + # + # A caller must pass in a block to modify individual parts of + # the query, e.g. to apply .with_feature_available_for_user on top of it. + # This is useful for performance as we can stick those additional filters + # at the bottom of e.g. the UNION. + def projects_for_user + return yield(Project.public_to_user) unless current_user + + # If the current_user is allowed to see all projects, + # we can shortcut and just return. + return yield(Project.all) if current_user.full_private_access? + + authorized_projects = yield(Project.where('EXISTS (?)', current_user.authorizations_for_projects)) + + levels = Gitlab::VisibilityLevel.levels_for_user(current_user) + visible_projects = yield(Project.where(visibility_level: levels)) + + # We use a UNION here instead of OR clauses since this results in better + # performance. + union = Gitlab::SQL::Union.new([authorized_projects.select('projects.id'), visible_projects.select('projects.id')]) + + Project.from("(#{union.to_sql}) AS #{Project.table_name}") + end + def feature_available_projects # Don't return any project related snippets if the user cannot read cross project return table[:id].eq(nil) unless Ability.allowed?(current_user, :read_cross_project) - projects = Project.public_or_visible_to_user(current_user, use_where_in: false) do |part| + projects = projects_for_user do |part| part.with_feature_available_for_user(:snippets, current_user) end.select(:id) diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb index edb17843002..150f4c7688b 100644 --- a/app/finders/todos_finder.rb +++ b/app/finders/todos_finder.rb @@ -110,10 +110,6 @@ class TodosFinder ids end - def projects(items) - ProjectsFinder.new(current_user: current_user, project_ids_relation: project_ids(items)).execute - end - def type? type.present? && %w(Issue MergeRequest).include?(type) end @@ -152,13 +148,12 @@ class TodosFinder def by_project(items) if project? - items = items.where(project: project) + items.where(project: project) else - item_projects = projects(items) - items = items.merge(item_projects).joins(:project) - end + projects = Project.public_or_visible_to_user(current_user) - items + items.joins(:project).merge(projects) + end end def by_state(items) diff --git a/app/finders/user_recent_events_finder.rb b/app/finders/user_recent_events_finder.rb index 6f7f7c30d92..65d6e019746 100644 --- a/app/finders/user_recent_events_finder.rb +++ b/app/finders/user_recent_events_finder.rb @@ -12,6 +12,8 @@ class UserRecentEventsFinder attr_reader :current_user, :target_user, :params + LIMIT = 20 + def initialize(current_user, target_user, params = {}) @current_user = current_user @target_user = target_user @@ -19,15 +21,44 @@ class UserRecentEventsFinder end def execute - target_user - .recent_events - .merge(projects_for_current_user) - .references(:project) + recent_events(params[:offset] || 0) + .joins(:project) .with_associations - .limit_recent(20, params[:offset]) + .limit_recent(LIMIT, params[:offset]) + end + + private + + def recent_events(offset) + sql = <<~SQL + (#{projects}) AS projects_for_join + JOIN (#{target_events.to_sql}) AS #{Event.table_name} + ON #{Event.table_name}.project_id = projects_for_join.id + SQL + + # Workaround for https://github.com/rails/rails/issues/24193 + Event.from([Arel.sql(sql)]) end - def projects_for_current_user - ProjectsFinder.new(current_user: current_user).execute + def target_events + Event.where(author: target_user) + end + + def projects + # Compile a list of projects `current_user` interacted with + # and `target_user` is allowed to see. + + authorized = target_user + .project_interactions + .joins(:project_authorizations) + .where(project_authorizations: { user: current_user }) + .select(:id) + + visible = target_user + .project_interactions + .where(visibility_level: [Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PUBLIC]) + .select(:id) + + Gitlab::SQL::Union.new([authorized, visible]).to_sql end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 475341cf9b1..3ddf8eb3369 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -300,7 +300,7 @@ module ApplicationHelper def linkedin_url(user) name = user.linkedin - if name =~ %r{\Ahttps?:\/\/(www\.)?linkedin\.com\/in\/} + if name =~ %r{\Ahttps?://(www\.)?linkedin\.com/in/} name else "https://www.linkedin.com/in/#{name}" @@ -309,10 +309,10 @@ module ApplicationHelper def twitter_url(user) name = user.twitter - if name =~ %r{\Ahttps?:\/\/(www\.)?twitter\.com\/} + if name =~ %r{\Ahttps?://(www\.)?twitter\.com/} name else - "https://www.twitter.com/#{name}" + "https://twitter.com/#{name}" end end @@ -320,10 +320,6 @@ module ApplicationHelper cookies["sidebar_collapsed"] == "true" end - def show_new_ide? - cookies["new_repo"] == "true" && body_data_page != 'projects:show' - end - def locale_path asset_path("locale/#{Gitlab::I18n.locale}/app.js") end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 0e806d16bc5..2b440e4d584 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -34,13 +34,10 @@ module BlobHelper end def ide_edit_button(project = @project, ref = @ref, path = @path, options = {}) - return unless show_new_ide? return unless blob = readable_blob(options, path, project, ref) - common_classes = "btn js-edit-ide #{options[:extra_class]}" - edit_button_tag(blob, - common_classes, + 'btn btn-default', _('Web IDE'), ide_edit_path(project, ref, path, options), project, diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb index 12b3d9bac1a..275e892b2e6 100644 --- a/app/helpers/boards_helper.rb +++ b/app/helpers/boards_helper.rb @@ -17,23 +17,35 @@ module BoardsHelper end def build_issue_link_base - project_issues_path(@project) + if board.group_board? + "#{group_path(@board.group)}/:project_path/issues" + else + project_issues_path(@project) + end end def board_base_url - project_boards_path(@project) + if board.group_board? + group_boards_url(@group) + else + project_boards_path(@project) + end end def multiple_boards_available? - current_board_parent.multiple_issue_boards_available?(current_user) + current_board_parent.multiple_issue_boards_available? end def current_board_path(board) - @current_board_path ||= project_board_path(current_board_parent, board) + @current_board_path ||= if board.group_board? + group_board_path(current_board_parent, board) + else + project_board_path(current_board_parent, board) + end end def current_board_parent - @current_board_parent ||= @project + @current_board_parent ||= @group || @project end def can_admin_issue? @@ -47,7 +59,8 @@ module BoardsHelper labels: labels_filter_path(true), labels_endpoint: @labels_endpoint, namespace_path: @namespace_path, - project_path: @project&.try(:path) + project_path: @project&.path, + group_path: @group&.path } end @@ -59,7 +72,8 @@ module BoardsHelper field_name: 'issue[assignee_ids][]', first_user: current_user&.username, current_user: 'true', - project_id: @project&.try(:id), + project_id: @project&.id, + group_id: @group&.id, null_user: 'true', multi_select: 'true', 'dropdown-header': dropdown_options[:data][:'dropdown-header'], diff --git a/app/helpers/branches_helper.rb b/app/helpers/branches_helper.rb index 00b9a0e00eb..07b1fc3d7cf 100644 --- a/app/helpers/branches_helper.rb +++ b/app/helpers/branches_helper.rb @@ -1,15 +1,4 @@ module BranchesHelper - def filter_branches_path(options = {}) - exist_opts = { - search: params[:search], - sort: params[:sort] - } - - options = exist_opts.merge(options) - - project_branches_path(@project, @id, options) - end - def project_branches options_for_select(@project.repository.branch_names, @project.default_branch) end diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb index e26ce6da030..905e2002592 100644 --- a/app/helpers/form_helper.rb +++ b/app/helpers/form_helper.rb @@ -27,7 +27,7 @@ module FormHelper first_user: current_user&.username, null_user: true, current_user: true, - project_id: @project.id, + project_id: @project&.id, field_name: 'issue[assignee_ids][]', default_label: 'Unassigned', 'max-select': 1, diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index 7910de73c52..16eceb3f48f 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -129,7 +129,7 @@ module GroupsHelper links = [:overview, :group_members] if can?(current_user, :read_cross_project) - links += [:activity, :issues, :labels, :milestones, :merge_requests] + links += [:activity, :issues, :boards, :labels, :milestones, :merge_requests] end if can?(current_user, :admin_group, @group) diff --git a/app/helpers/import_helper.rb b/app/helpers/import_helper.rb index a18ebfb6030..4664b1728c4 100644 --- a/app/helpers/import_helper.rb +++ b/app/helpers/import_helper.rb @@ -1,29 +1,94 @@ module ImportHelper + include ::Gitlab::Utils::StrongMemoize + + def has_ci_cd_only_params? + false + end + def import_project_target(owner, name) namespace = current_user.can_create_group? ? owner : current_user.namespace_path "#{namespace}/#{name}" end - def provider_project_link(provider, path_with_namespace) - url = __send__("#{provider}_project_url", path_with_namespace) # rubocop:disable GitlabSecurity/PublicSend + def provider_project_link(provider, full_path) + url = __send__("#{provider}_project_url", full_path) # rubocop:disable GitlabSecurity/PublicSend + + link_to full_path, url, target: '_blank', rel: 'noopener noreferrer' + end + + def import_will_timeout_message(_ci_cd_only) + timeout = time_interval_in_words(Gitlab.config.gitlab_shell.git_timeout) + _('The import will time out after %{timeout}. For repositories that take longer, use a clone/push combination.') % { timeout: timeout } + end + + def import_svn_message(_ci_cd_only) + svn_link = link_to _('this document'), help_page_path('user/project/import/svn') + _('To import an SVN repository, check out %{svn_link}.').html_safe % { svn_link: svn_link } + end + + def import_in_progress_title + if @project.forked? + _('Forking in progress') + else + _('Import in progress') + end + end + + def import_wait_and_refresh_message + _('Please wait while we import the repository for you. Refresh at will.') + end + + def import_github_title + _('Import repositories from GitHub') + end + + def import_github_authorize_message + _('To import GitHub repositories, you first need to authorize GitLab to access the list of your GitHub repositories:') + end + + def import_github_personal_access_token_message + personal_access_token_link = link_to _('Personal Access Token'), 'https://github.com/settings/tokens' + + if github_import_configured? + _('Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import.').html_safe % { personal_access_token_link: personal_access_token_link } + else + _('To import GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import.').html_safe % { personal_access_token_link: personal_access_token_link } + end + end + + def import_configure_github_admin_message + github_integration_link = link_to 'GitHub integration', help_page_path('integration/github') + + if current_user.admin? + _('Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token.').html_safe % { github_integration_link: github_integration_link } + else + _('Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token.').html_safe % { github_integration_link: github_integration_link } + end + end + + def import_githubish_choose_repository_message + _('Choose which repositories you want to import.') + end - link_to path_with_namespace, url, target: '_blank', rel: 'noopener noreferrer' + def import_all_githubish_repositories_button_label + _('Import all repositories') end private - def github_project_url(path_with_namespace) - "#{github_root_url}/#{path_with_namespace}" + def github_project_url(full_path) + URI.join(github_root_url, full_path).to_s end def github_root_url - return @github_url if defined?(@github_url) + strong_memoize(:github_url) do + provider = Gitlab::Auth::OAuth::Provider.config_for('github') - provider = Gitlab.config.omniauth.providers.find { |p| p.name == 'github' } - @github_url = provider.fetch('url', 'https://github.com') if provider + provider&.dig('url').presence || 'https://github.com' + end end - def gitea_project_url(path_with_namespace) - "#{@gitea_host_url.sub(%r{/+\z}, '')}/#{path_with_namespace}" + def gitea_project_url(full_path) + URI.join(@gitea_host_url, full_path).to_s end end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 44ecc2212f2..6d6b840f485 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -99,7 +99,7 @@ module IssuablesHelper project = Project.find_by(id: project_id) if project - project.name_with_namespace + project.full_name else default_label end @@ -377,4 +377,11 @@ module IssuablesHelper def parent @project || @group end + + def issuable_milestone_tooltip_title(issuable) + if issuable.milestone + milestone_tooltip = milestone_tooltip_title(issuable.milestone) + _('Milestone') + (milestone_tooltip ? ': ' + milestone_tooltip : '') + end + end end diff --git a/app/helpers/javascript_helper.rb b/app/helpers/javascript_helper.rb index d5e77c7e271..cd4075b340d 100644 --- a/app/helpers/javascript_helper.rb +++ b/app/helpers/javascript_helper.rb @@ -2,9 +2,4 @@ module JavascriptHelper def page_specific_javascript_tag(js) javascript_include_tag asset_path(js) end - - # deprecated; use webpack_bundle_tag directly instead - def page_specific_javascript_bundle_tag(bundle) - webpack_bundle_tag(bundle) - end end diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index c1c19062c91..87ff607dc3f 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -1,4 +1,5 @@ module LabelsHelper + extend self include ActionView::Helpers::TagHelper def show_label_issuables_link?(label, issuables_type, current_user: nil, project: nil) @@ -173,6 +174,39 @@ module LabelsHelper end end + def create_label_title(subject) + case subject + when Group + _('Create group label') + when Project + _('Create project label') + else + _('Create new label') + end + end + + def manage_labels_title(subject) + case subject + when Group + _('Manage group labels') + when Project + _('Manage project labels') + else + _('Manage labels') + end + end + + def view_labels_title(subject) + case subject + when Group + _('View group labels') + when Project + _('View project labels') + else + _('View labels') + end + end + # Required for Banzai::Filter::LabelReferenceFilter module_function :render_colored_label, :text_color_for_bg, :escape_once end diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index ce57422f45d..fb4fe1c40b7 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -125,6 +125,19 @@ module MergeRequestsHelper link_to(url[merge_request.project, merge_request], data: data_attrs, &block) end + def allow_maintainer_push_unavailable_reason(merge_request) + return if merge_request.can_allow_maintainer_to_push?(current_user) + + minimum_visibility = [merge_request.target_project.visibility_level, + merge_request.source_project.visibility_level].min + + if minimum_visibility < Gitlab::VisibilityLevel::INTERNAL + _('Not available for private projects') + elsif ProtectedBranch.protected?(merge_request.source_project, merge_request.source_branch) + _('Not available for protected branches') + end + end + def merge_params_ee(merge_request) {} end diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index e86e43b5ebf..20aed60cb7a 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -11,7 +11,7 @@ module NotesHelper end def note_supports_quick_actions?(note) - Notes::QuickActionsService.supported?(note, current_user) + Notes::QuickActionsService.supported?(note) end def noteable_json(noteable) @@ -169,7 +169,7 @@ module NotesHelper reopenPath: reopen_issuable_path(issuable), notesPath: notes_url, totalNotes: issuable.discussions.length, - lastFetchedAt: Time.now + lastFetchedAt: Time.now.to_i }.to_json end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index cc1c69a1999..da9fe734f1c 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -97,13 +97,13 @@ module ProjectsHelper end def remove_project_message(project) - _("You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?") % - { project_name_with_namespace: project.name_with_namespace } + _("You are going to remove %{project_full_name}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?") % + { project_full_name: project.full_name } end def transfer_project_message(project) - _("You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?") % - { project_name_with_namespace: project.name_with_namespace } + _("You are going to transfer %{project_full_name} to another owner. Are you ABSOLUTELY sure?") % + { project_full_name: project.full_name } end def remove_fork_project_message(project) diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index e6a6496871a..761c1252fc8 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -110,7 +110,7 @@ module SearchHelper category: "Projects", id: p.id, value: "#{search_result_sanitize(p.name)}", - label: "#{search_result_sanitize(p.name_with_namespace)}", + label: "#{search_result_sanitize(p.full_name)}", url: project_path(p) } end diff --git a/app/helpers/services_helper.rb b/app/helpers/services_helper.rb index 240783bc7fd..f435c80c656 100644 --- a/app/helpers/services_helper.rb +++ b/app/helpers/services_helper.rb @@ -1,27 +1,4 @@ module ServicesHelper - def service_event_description(event) - case event - when "push", "push_events" - "Event will be triggered by a push to the repository" - when "tag_push", "tag_push_events" - "Event will be triggered when a new tag is pushed to the repository" - when "note", "note_events" - "Event will be triggered when someone adds a comment" - when "issue", "issue_events" - "Event will be triggered when an issue is created/updated/closed" - when "confidential_issue", "confidential_issue_events" - "Event will be triggered when a confidential issue is created/updated/closed" - when "merge_request", "merge_request_events" - "Event will be triggered when a merge request is created/updated/merged" - when "pipeline", "pipeline_events" - "Event will be triggered when a pipeline status changes" - when "wiki_page", "wiki_page_events" - "Event will be triggered when a wiki page is created/updated" - when "commit", "commit_events" - "Event will be triggered when a commit is created/updated" - end - end - def service_event_field_name(event) event = event.pluralize if %w[merge_request issue confidential_issue].include?(event) "#{event}_events" diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index ddb48371c79..f7620e0b6b8 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -114,7 +114,7 @@ module TodosHelper projects = current_user.authorized_projects.sorted_by_activity.non_archived.with_route projects = projects.map do |project| - { id: project.id, text: project.name_with_namespace } + { id: project.id, text: project.full_name } end projects.unshift({ id: '', text: 'Any Project' }).to_json diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index f6a6d9bebde..b64be89c181 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -49,15 +49,13 @@ module TreeHelper return false unless on_top_of_branch?(project, ref) - can_collaborate_with_project?(project) + can_collaborate_with_project?(project, ref: ref) end def tree_edit_branch(project = @project, ref = @ref) return unless can_edit_tree?(project, ref) - project = project.present(current_user: current_user) - - if project.can_current_user_push_to_branch?(ref) + if user_access(project).can_push_to_branch?(ref) ref else project = tree_edit_project(project) @@ -88,7 +86,16 @@ module TreeHelper end def commit_in_fork_help - "A new branch will be created in your fork and a new merge request will be started." + _("A new branch will be created in your fork and a new merge request will be started.") + end + + def commit_in_single_accessible_branch + branch_name = html_escape(selected_branch) + + message = _("Your changes can be committed to %{branch_name} because a merge "\ + "request is open.") % { branch_name: "<strong>#{branch_name}</strong>" } + + message.html_safe end def path_breadcrumbs(max_links = 6) @@ -125,4 +132,8 @@ module TreeHelper return tree.name end end + + def selected_branch + @branch_name || tree_edit_branch + end end diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index 45d4fb451d8..e4212775956 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -117,7 +117,7 @@ class Notify < BaseMailer if Gitlab::IncomingEmail.enabled? && @sent_notification address = Mail::Address.new(Gitlab::IncomingEmail.reply_address(reply_key)) - address.display_name = @project.name_with_namespace + address.display_name = @project.full_name headers['Reply-To'] = address diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 0dee6df525d..3cbbf8b5dfa 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -347,15 +347,15 @@ class ApplicationSetting < ActiveRecord::Base end def home_page_url_column_exists? - ActiveRecord::Base.connection.column_exists?(:application_settings, :home_page_url) + ::Gitlab::Database.cached_column_exists?(:application_settings, :home_page_url) end def help_page_support_url_column_exists? - ActiveRecord::Base.connection.column_exists?(:application_settings, :help_page_support_url) + ::Gitlab::Database.cached_column_exists?(:application_settings, :help_page_support_url) end def sidekiq_throttling_column_exists? - ActiveRecord::Base.connection.column_exists?(:application_settings, :sidekiq_throttling_enabled) + ::Gitlab::Database.cached_column_exists?(:application_settings, :sidekiq_throttling_enabled) end def domain_whitelist_raw diff --git a/app/models/badge.rb b/app/models/badge.rb new file mode 100644 index 00000000000..f7e10c2ebfc --- /dev/null +++ b/app/models/badge.rb @@ -0,0 +1,51 @@ +class Badge < ActiveRecord::Base + # This structure sets the placeholders that the urls + # can have. This hash also sets which action to ask when + # the placeholder is found. + PLACEHOLDERS = { + 'project_path' => :full_path, + 'project_id' => :id, + 'default_branch' => :default_branch, + 'commit_sha' => ->(project) { project.commit&.sha } + }.freeze + + # This regex is built dynamically using the keys from the PLACEHOLDER struct. + # So, we can easily add new placeholder just by modifying the PLACEHOLDER hash. + # This regex will build the new PLACEHOLDER_REGEX with the new information + PLACEHOLDERS_REGEX = /(#{PLACEHOLDERS.keys.join('|')})/.freeze + + default_scope { order_created_at_asc } + + scope :order_created_at_asc, -> { reorder(created_at: :asc) } + + validates :link_url, :image_url, url_placeholder: { protocols: %w(http https), placeholder_regex: PLACEHOLDERS_REGEX } + validates :type, presence: true + + def rendered_link_url(project = nil) + build_rendered_url(link_url, project) + end + + def rendered_image_url(project = nil) + build_rendered_url(image_url, project) + end + + private + + def build_rendered_url(url, project = nil) + return url unless valid? && project + + Gitlab::StringPlaceholderReplacer.replace_string_placeholders(url, PLACEHOLDERS_REGEX) do |arg| + replace_placeholder_action(PLACEHOLDERS[arg], project) + end + end + + # The action param represents the :symbol or Proc to call in order + # to retrieve the return value from the project. + # This method checks if it is a Proc and use the call method, and if it is + # a symbol just send the action + def replace_placeholder_action(action, project) + return unless project + + action.is_a?(Proc) ? action.call(project) : project.public_send(action) # rubocop:disable GitlabSecurity/PublicSend + end +end diff --git a/app/models/badges/group_badge.rb b/app/models/badges/group_badge.rb new file mode 100644 index 00000000000..f4b2bdecdcc --- /dev/null +++ b/app/models/badges/group_badge.rb @@ -0,0 +1,5 @@ +class GroupBadge < Badge + belongs_to :group + + validates :group, presence: true +end diff --git a/app/models/badges/project_badge.rb b/app/models/badges/project_badge.rb new file mode 100644 index 00000000000..3945b376052 --- /dev/null +++ b/app/models/badges/project_badge.rb @@ -0,0 +1,15 @@ +class ProjectBadge < Badge + belongs_to :project + + validates :project, presence: true + + def rendered_link_url(project = nil) + project ||= self.project + super + end + + def rendered_image_url(project = nil) + project ||= self.project + super + end +end diff --git a/app/models/board.rb b/app/models/board.rb index 5bb7d3d3722..3cede6fc99a 100644 --- a/app/models/board.rb +++ b/app/models/board.rb @@ -1,20 +1,22 @@ class Board < ActiveRecord::Base + belongs_to :group belongs_to :project has_many :lists, -> { order(:list_type, :position) }, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent validates :project, presence: true, if: :project_needed? + validates :group, presence: true, unless: :project def project_needed? - true + !group end def parent - project + @parent ||= group || project end def group_board? - false + group_id.present? end def backlog_list diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index b230b7f47ef..1e066b69c6e 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -41,12 +41,12 @@ module Ci scope :unstarted, ->() { where(runner_id: nil) } scope :ignore_failures, ->() { where(allow_failure: false) } - scope :with_artifacts, ->() do + scope :with_artifacts_archive, ->() do where('(artifacts_file IS NOT NULL AND artifacts_file <> ?) OR EXISTS (?)', - '', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id')) + '', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').archive) end - scope :with_artifacts_not_expired, ->() { with_artifacts.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) } - scope :with_expired_artifacts, ->() { with_artifacts.where('artifacts_expire_at < ?', Time.now) } + scope :with_artifacts_not_expired, ->() { with_artifacts_archive.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) } + scope :with_expired_artifacts, ->() { with_artifacts_archive.where('artifacts_expire_at < ?', Time.now) } scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) } scope :manual_actions, ->() { where(when: :manual, status: COMPLETED_STATUSES + [:manual]) } scope :ref_protected, -> { where(protected: true) } @@ -140,7 +140,11 @@ module Ci next if build.retries_max.zero? if build.retries_count < build.retries_max - Ci::Build.retry(build, build.user) + begin + Ci::Build.retry(build, build.user) + rescue Gitlab::Access::AccessDeniedError => ex + Rails.logger.error "Unable to auto-retry job #{build.id}: #{ex}" + end end end @@ -252,23 +256,23 @@ module Ci # All variables, including those dependent on environment, which could # contain unexpanded variables. def variables(environment: persisted_environment) - variables = predefined_variables - variables += project.predefined_variables - variables += pipeline.predefined_variables - variables += runner.predefined_variables if runner - variables += project.container_registry_variables - variables += project.deployment_variables if has_environment? - variables += project.auto_devops_variables - variables += yaml_variables - variables += user_variables - variables += project.group.secret_variables_for(ref, project).map(&:to_runner_variable) if project.group - variables += secret_variables(environment: environment) - variables += trigger_request.user_variables if trigger_request - variables += pipeline.variables.map(&:to_runner_variable) - variables += pipeline.pipeline_schedule.job_variables if pipeline.pipeline_schedule - variables += persisted_environment_variables if environment - - variables + collection = Gitlab::Ci::Variables::Collection.new.tap do |variables| + variables.concat(predefined_variables) + variables.concat(project.predefined_variables) + variables.concat(pipeline.predefined_variables) + variables.concat(runner.predefined_variables) if runner + variables.concat(project.deployment_variables(environment: environment)) if has_environment? + variables.concat(yaml_variables) + variables.concat(user_variables) + variables.concat(project.group.secret_variables_for(ref, project)) if project.group + variables.concat(secret_variables(environment: environment)) + variables.concat(trigger_request.user_variables) if trigger_request + variables.concat(pipeline.variables) + variables.concat(pipeline.pipeline_schedule.job_variables) if pipeline.pipeline_schedule + variables.concat(persisted_environment_variables) if environment + end + + collection.to_runner_variables end def features @@ -328,8 +332,7 @@ module Ci end def erase_old_trace! - write_attribute(:trace, nil) - save + update_column(:trace, nil) end def needs_touch? @@ -430,14 +433,14 @@ module Ci end def user_variables - return [] if user.blank? + Gitlab::Ci::Variables::Collection.new.tap do |variables| + return variables if user.blank? - [ - { key: 'GITLAB_USER_ID', value: user.id.to_s, public: true }, - { key: 'GITLAB_USER_EMAIL', value: user.email, public: true }, - { key: 'GITLAB_USER_LOGIN', value: user.username, public: true }, - { key: 'GITLAB_USER_NAME', value: user.name, public: true } - ] + variables.append(key: 'GITLAB_USER_ID', value: user.id.to_s) + variables.append(key: 'GITLAB_USER_EMAIL', value: user.email) + variables.append(key: 'GITLAB_USER_LOGIN', value: user.username) + variables.append(key: 'GITLAB_USER_NAME', value: user.name) + end end def secret_variables(environment: persisted_environment) @@ -540,60 +543,57 @@ module Ci CI_REGISTRY_USER = 'gitlab-ci-token'.freeze def predefined_variables - variables = [ - { key: 'CI', value: 'true', public: true }, - { key: 'GITLAB_CI', value: 'true', public: true }, - { key: 'GITLAB_FEATURES', value: project.namespace.features.join(','), public: true }, - { key: 'CI_SERVER_NAME', value: 'GitLab', public: true }, - { key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true }, - { key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true }, - { key: 'CI_JOB_ID', value: id.to_s, public: true }, - { key: 'CI_JOB_NAME', value: name, public: true }, - { key: 'CI_JOB_STAGE', value: stage, public: true }, - { key: 'CI_JOB_TOKEN', value: token, public: false }, - { key: 'CI_COMMIT_SHA', value: sha, public: true }, - { key: 'CI_COMMIT_REF_NAME', value: ref, public: true }, - { key: 'CI_COMMIT_REF_SLUG', value: ref_slug, public: true }, - { key: 'CI_REGISTRY_USER', value: CI_REGISTRY_USER, public: true }, - { key: 'CI_REGISTRY_PASSWORD', value: token, public: false }, - { key: 'CI_REPOSITORY_URL', value: repo_url, public: false } - ] - - variables << { key: "CI_COMMIT_TAG", value: ref, public: true } if tag? - variables << { key: "CI_PIPELINE_TRIGGERED", value: 'true', public: true } if trigger_request - variables << { key: "CI_JOB_MANUAL", value: 'true', public: true } if action? - variables.concat(legacy_variables) + Gitlab::Ci::Variables::Collection.new.tap do |variables| + variables.append(key: 'CI', value: 'true') + variables.append(key: 'GITLAB_CI', value: 'true') + variables.append(key: 'GITLAB_FEATURES', value: project.namespace.features.join(',')) + variables.append(key: 'CI_SERVER_NAME', value: 'GitLab') + variables.append(key: 'CI_SERVER_VERSION', value: Gitlab::VERSION) + variables.append(key: 'CI_SERVER_REVISION', value: Gitlab::REVISION) + variables.append(key: 'CI_JOB_ID', value: id.to_s) + variables.append(key: 'CI_JOB_NAME', value: name) + variables.append(key: 'CI_JOB_STAGE', value: stage) + variables.append(key: 'CI_JOB_TOKEN', value: token, public: false) + variables.append(key: 'CI_COMMIT_SHA', value: sha) + variables.append(key: 'CI_COMMIT_REF_NAME', value: ref) + variables.append(key: 'CI_COMMIT_REF_SLUG', value: ref_slug) + variables.append(key: 'CI_REGISTRY_USER', value: CI_REGISTRY_USER) + variables.append(key: 'CI_REGISTRY_PASSWORD', value: token, public: false) + variables.append(key: 'CI_REPOSITORY_URL', value: repo_url, public: false) + variables.append(key: "CI_COMMIT_TAG", value: ref) if tag? + variables.append(key: "CI_PIPELINE_TRIGGERED", value: 'true') if trigger_request + variables.append(key: "CI_JOB_MANUAL", value: 'true') if action? + variables.concat(legacy_variables) + end end def persisted_environment_variables - return [] unless persisted_environment - - variables = persisted_environment.predefined_variables + Gitlab::Ci::Variables::Collection.new.tap do |variables| + return variables unless persisted_environment - # Here we're passing unexpanded environment_url for runner to expand, - # and we need to make sure that CI_ENVIRONMENT_NAME and - # CI_ENVIRONMENT_SLUG so on are available for the URL be expanded. - variables << { key: 'CI_ENVIRONMENT_URL', value: environment_url, public: true } if environment_url + variables.concat(persisted_environment.predefined_variables) - variables + # Here we're passing unexpanded environment_url for runner to expand, + # and we need to make sure that CI_ENVIRONMENT_NAME and + # CI_ENVIRONMENT_SLUG so on are available for the URL be expanded. + variables.append(key: 'CI_ENVIRONMENT_URL', value: environment_url) if environment_url + end end def legacy_variables - variables = [ - { key: 'CI_BUILD_ID', value: id.to_s, public: true }, - { key: 'CI_BUILD_TOKEN', value: token, public: false }, - { key: 'CI_BUILD_REF', value: sha, public: true }, - { key: 'CI_BUILD_BEFORE_SHA', value: before_sha, public: true }, - { key: 'CI_BUILD_REF_NAME', value: ref, public: true }, - { key: 'CI_BUILD_REF_SLUG', value: ref_slug, public: true }, - { key: 'CI_BUILD_NAME', value: name, public: true }, - { key: 'CI_BUILD_STAGE', value: stage, public: true } - ] - - variables << { key: "CI_BUILD_TAG", value: ref, public: true } if tag? - variables << { key: "CI_BUILD_TRIGGERED", value: 'true', public: true } if trigger_request - variables << { key: "CI_BUILD_MANUAL", value: 'true', public: true } if action? - variables + Gitlab::Ci::Variables::Collection.new.tap do |variables| + variables.append(key: 'CI_BUILD_ID', value: id.to_s) + variables.append(key: 'CI_BUILD_TOKEN', value: token, public: false) + variables.append(key: 'CI_BUILD_REF', value: sha) + variables.append(key: 'CI_BUILD_BEFORE_SHA', value: before_sha) + variables.append(key: 'CI_BUILD_REF_NAME', value: ref) + variables.append(key: 'CI_BUILD_REF_SLUG', value: ref_slug) + variables.append(key: 'CI_BUILD_NAME', value: name) + variables.append(key: 'CI_BUILD_STAGE', value: stage) + variables.append(key: "CI_BUILD_TAG", value: ref) if tag? + variables.append(key: "CI_BUILD_TRIGGERED", value: 'true') if trigger_request + variables.append(key: "CI_BUILD_MANUAL", value: 'true') if action? + end end def environment_url diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 103c36c4668..44f9bdf111e 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -473,11 +473,10 @@ module Ci end def predefined_variables - [ - { key: 'CI_PIPELINE_ID', value: id.to_s, public: true }, - { key: 'CI_CONFIG_PATH', value: ci_yaml_file_path, public: true }, - { key: 'CI_PIPELINE_SOURCE', value: source.to_s, public: true } - ] + Gitlab::Ci::Variables::Collection.new + .append(key: 'CI_PIPELINE_ID', value: id.to_s) + .append(key: 'CI_CONFIG_PATH', value: ci_yaml_file_path) + .append(key: 'CI_PIPELINE_SOURCE', value: source.to_s) end def queued_duration @@ -514,7 +513,7 @@ module Ci # We purposely cast the builds to an Array here. Because we always use the # rows if there are more than 0 this prevents us from having to run two # queries: one to get the count and one to get the rows. - @latest_builds_with_artifacts ||= builds.latest.with_artifacts.to_a + @latest_builds_with_artifacts ||= builds.latest.with_artifacts_archive.to_a end private diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index 609620a62bb..7173f88f1c7 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -132,11 +132,10 @@ module Ci end def predefined_variables - [ - { key: 'CI_RUNNER_ID', value: id.to_s, public: true }, - { key: 'CI_RUNNER_DESCRIPTION', value: description, public: true }, - { key: 'CI_RUNNER_TAGS', value: tag_list.to_s, public: true } - ] + Gitlab::Ci::Variables::Collection.new + .append(key: 'CI_RUNNER_ID', value: id.to_s) + .append(key: 'CI_RUNNER_DESCRIPTION', value: description) + .append(key: 'CI_RUNNER_TAGS', value: tag_list.to_s) end def tick_runner_queue diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb index 89ebd63e605..7b25d8c4089 100644 --- a/app/models/clusters/applications/prometheus.rb +++ b/app/models/clusters/applications/prometheus.rb @@ -1,6 +1,8 @@ module Clusters module Applications class Prometheus < ActiveRecord::Base + include PrometheusAdapter + VERSION = "2.0.0".freeze self.table_name = 'clusters_applications_prometheus' @@ -39,7 +41,7 @@ module Clusters ) end - def proxy_client + def prometheus_client return unless kube_client proxy_url = kube_client.proxy_url('service', service_name, service_port, Gitlab::Kubernetes::Helm::NAMESPACE) diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb index 7adf1663c35..16efe90fa27 100644 --- a/app/models/clusters/applications/runner.rb +++ b/app/models/clusters/applications/runner.rb @@ -56,12 +56,13 @@ module Clusters def specification { "gitlabUrl" => gitlab_url, - "runnerToken" => ensure_runner.token + "runnerToken" => ensure_runner.token, + "runners" => { "privileged" => privileged } } end def content_values - specification.merge(YAML.load_file(chart_values_file)) + YAML.load_file(chart_values_file).deep_merge!(specification) end end end diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index 1c0046107d7..49eb069016a 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -51,9 +51,6 @@ module Clusters scope :enabled, -> { where(enabled: true) } scope :disabled, -> { where(enabled: false) } - scope :for_environment, -> (env) { where(environment_scope: ['*', '', env.slug]) } - scope :for_all_environments, -> { where(environment_scope: ['*', '']) } - def status_name if provider provider.status_name diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index 7ce8befeeeb..ba6552f238f 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -56,19 +56,19 @@ module Clusters def predefined_variables config = YAML.dump(kubeconfig) - variables = [ - { key: 'KUBE_URL', value: api_url, public: true }, - { key: 'KUBE_TOKEN', value: token, public: false }, - { key: 'KUBE_NAMESPACE', value: actual_namespace, public: true }, - { key: 'KUBECONFIG', value: config, public: false, file: true } - ] - - if ca_pem.present? - variables << { key: 'KUBE_CA_PEM', value: ca_pem, public: true } - variables << { key: 'KUBE_CA_PEM_FILE', value: ca_pem, public: true, file: true } + Gitlab::Ci::Variables::Collection.new.tap do |variables| + variables + .append(key: 'KUBE_URL', value: api_url) + .append(key: 'KUBE_TOKEN', value: token, public: false) + .append(key: 'KUBE_NAMESPACE', value: actual_namespace) + .append(key: 'KUBECONFIG', value: config, public: false, file: true) + + if ca_pem.present? + variables + .append(key: 'KUBE_CA_PEM', value: ca_pem) + .append(key: 'KUBE_CA_PEM_FILE', value: ca_pem, file: true) + end end - - variables end # Constructs a list of terminals from the reactive cache @@ -134,7 +134,7 @@ module Clusters kubeclient = build_kubeclient! kubeclient.get_pods(namespace: actual_namespace).as_json - rescue KubeException => err + rescue Kubeclient::HttpError => err raise err unless err.error_code == 404 [] diff --git a/app/models/commit.rb b/app/models/commit.rb index add5fcf0e79..cceae5efb72 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -9,6 +9,7 @@ class Commit include Mentionable include Referable include StaticModel + include ::Gitlab::Utils::StrongMemoize attr_mentionable :safe_message, pipeline: :single_line @@ -19,6 +20,7 @@ class Commit attr_accessor :project, :author attr_accessor :redacted_description_html attr_accessor :redacted_title_html + attr_reader :gpg_commit DIFF_SAFE_LINES = Gitlab::Git::DiffCollection::DEFAULT_LIMITS[:max_lines] @@ -110,6 +112,7 @@ class Commit @raw = raw_commit @project = project @statuses = {} + @gpg_commit = Gitlab::Gpg::Commit.new(self) if project end def id @@ -223,11 +226,13 @@ class Commit end def parents - @parents ||= parent_ids.map { |id| project.commit(id) } + @parents ||= parent_ids.map { |oid| Commit.lazy(project, oid) } end def parent - @parent ||= project.commit(self.parent_id) if self.parent_id + strong_memoize(:parent) do + project.commit_by(oid: self.parent_id) if self.parent_id + end end def notes @@ -452,8 +457,4 @@ class Commit def merged_merge_request_no_cache(user) MergeRequestsFinder.new(user, project_id: project.id).find_by(merge_commit_sha: id) if merge_commit? end - - def gpg_commit - @gpg_commit ||= Gitlab::Gpg::Commit.new(self) - end end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 3469d5d795c..9fb5b7efec6 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -141,7 +141,7 @@ class CommitStatus < ActiveRecord::Base end def group_name - name.to_s.gsub(%r{\d+[\s:/\\]+\d+\s*}, '').strip + name.to_s.gsub(%r{\d+[\.\s:/\\]+\d+\s*}, '').strip end def failed_but_allowed? diff --git a/app/models/compare.rb b/app/models/compare.rb index 3a8bbcb1acd..feb4b89c781 100644 --- a/app/models/compare.rb +++ b/app/models/compare.rb @@ -1,4 +1,6 @@ class Compare + include Gitlab::Utils::StrongMemoize + delegate :same, :head, :base, to: :@compare attr_reader :project @@ -11,9 +13,10 @@ class Compare end end - def initialize(compare, project, straight: false) + def initialize(compare, project, base_sha: nil, straight: false) @compare = compare @project = project + @base_sha = base_sha @straight = straight end @@ -22,40 +25,36 @@ class Compare end def start_commit - return @start_commit if defined?(@start_commit) + strong_memoize(:start_commit) do + commit = @compare.base - commit = @compare.base - @start_commit = commit ? ::Commit.new(commit, project) : nil + ::Commit.new(commit, project) if commit + end end def head_commit - return @head_commit if defined?(@head_commit) + strong_memoize(:head_commit) do + commit = @compare.head - commit = @compare.head - @head_commit = commit ? ::Commit.new(commit, project) : nil + ::Commit.new(commit, project) if commit + end end alias_method :commit, :head_commit - def base_commit - return @base_commit if defined?(@base_commit) - - @base_commit = if start_commit && head_commit - project.merge_base_commit(start_commit.id, head_commit.id) - else - nil - end - end - def start_commit_sha - start_commit.try(:sha) + start_commit&.sha end def base_commit_sha - base_commit.try(:sha) + strong_memoize(:base_commit) do + next unless start_commit && head_commit + + @base_sha || project.merge_base_commit(start_commit.id, head_commit.id)&.sha + end end def head_commit_sha - commit.try(:sha) + commit&.sha end def raw_diffs(*args) diff --git a/app/models/concerns/atomic_internal_id.rb b/app/models/concerns/atomic_internal_id.rb new file mode 100644 index 00000000000..4b66725a3e6 --- /dev/null +++ b/app/models/concerns/atomic_internal_id.rb @@ -0,0 +1,46 @@ +# Include atomic internal id generation scheme for a model +# +# This allows us to atomically generate internal ids that are +# unique within a given scope. +# +# For example, let's generate internal ids for Issue per Project: +# ``` +# class Issue < ActiveRecord::Base +# has_internal_id :iid, scope: :project, init: ->(s) { s.project.issues.maximum(:iid) } +# end +# ``` +# +# This generates unique internal ids per project for newly created issues. +# The generated internal id is saved in the `iid` attribute of `Issue`. +# +# This concern uses InternalId records to facilitate atomicity. +# In the absence of a record for the given scope, one will be created automatically. +# In this situation, the `init` block is called to calculate the initial value. +# In the example above, we calculate the maximum `iid` of all issues +# within the given project. +# +# Note that a model may have more than one internal id associated with possibly +# different scopes. +module AtomicInternalId + extend ActiveSupport::Concern + + module ClassMethods + def has_internal_id(column, scope:, init:) # rubocop:disable Naming/PredicateName + before_validation(on: :create) do + if read_attribute(column).blank? + scope_attrs = { scope => association(scope).reader } + usage = self.class.table_name.to_sym + + new_iid = InternalId.generate_next(self, scope_attrs, usage, init) + write_attribute(column, new_iid) + end + end + + validates column, presence: true, numericality: true + end + end + + def to_param + iid.to_s + end +end diff --git a/app/models/concerns/deployment_platform.rb b/app/models/concerns/deployment_platform.rb index 89d0474a596..faa94204e33 100644 --- a/app/models/concerns/deployment_platform.rb +++ b/app/models/concerns/deployment_platform.rb @@ -1,5 +1,6 @@ module DeploymentPlatform - def deployment_platform + # EE would override this and utilize the extra argument + def deployment_platform(environment: nil) @deployment_platform ||= find_cluster_platform_kubernetes || find_kubernetes_service_integration || diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 7049f340c9d..5a566f3ac02 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -19,6 +19,7 @@ module Issuable include AfterCommitQueue include Sortable include CreatedAtFilterable + include UpdatedAtFilterable # This object is used to gather issuable meta data for displaying # upvotes, downvotes, notes and closing merge requests count for issues and merge requests @@ -222,6 +223,10 @@ module Issuable def to_ability_name model_name.singular end + + def parent_class + ::Project + end end def today? diff --git a/app/models/concerns/internal_id.rb b/app/models/concerns/nonatomic_internal_id.rb index 01079fb8bd6..9d0c9b8512f 100644 --- a/app/models/concerns/internal_id.rb +++ b/app/models/concerns/nonatomic_internal_id.rb @@ -1,4 +1,4 @@ -module InternalId +module NonatomicInternalId extend ActiveSupport::Concern included do diff --git a/app/models/concerns/prometheus_adapter.rb b/app/models/concerns/prometheus_adapter.rb new file mode 100644 index 00000000000..18cbbd871a1 --- /dev/null +++ b/app/models/concerns/prometheus_adapter.rb @@ -0,0 +1,48 @@ +module PrometheusAdapter + extend ActiveSupport::Concern + + included do + include ReactiveCaching + + self.reactive_cache_key = ->(adapter) { [adapter.class.model_name.singular, adapter.id] } + self.reactive_cache_lease_timeout = 30.seconds + self.reactive_cache_refresh_interval = 30.seconds + self.reactive_cache_lifetime = 1.minute + + def prometheus_client + raise NotImplementedError + end + + def prometheus_client_wrapper + Gitlab::PrometheusClient.new(prometheus_client) + end + + def can_query? + prometheus_client.present? + end + + def query(query_name, *args) + return unless can_query? + + query_class = Gitlab::Prometheus::Queries.const_get("#{query_name.to_s.classify}Query") + + args.map!(&:id) + + with_reactive_cache(query_class.name, *args, &query_class.method(:transform_reactive_result)) + end + + # Cache metrics for specific environment + def calculate_reactive_cache(query_class_name, *args) + return unless prometheus_client + + data = Kernel.const_get(query_class_name).new(prometheus_client_wrapper).query(*args) + { + success: true, + data: data, + last_update: Time.now.utc + } + rescue Gitlab::PrometheusClient::Error => err + { success: false, result: err.message } + end + end +end diff --git a/app/models/concerns/storage/legacy_namespace.rb b/app/models/concerns/storage/legacy_namespace.rb index 67a988addbe..f05e606995d 100644 --- a/app/models/concerns/storage/legacy_namespace.rb +++ b/app/models/concerns/storage/legacy_namespace.rb @@ -7,29 +7,24 @@ module Storage raise Gitlab::UpdatePathError.new('Namespace cannot be moved, because at least one project has tags in container registry') end - expires_full_path_cache - - # Move the namespace directory in all storage paths used by member projects - repository_storage_paths.each do |repository_storage_path| - # Ensure old directory exists before moving it - gitlab_shell.add_namespace(repository_storage_path, full_path_was) - - # Ensure new directory exists before moving it (if there's a parent) - gitlab_shell.add_namespace(repository_storage_path, parent.full_path) if parent + parent_was = if parent_changed? && parent_id_was.present? + Namespace.find(parent_id_was) # raise NotFound early if needed + end - unless gitlab_shell.mv_namespace(repository_storage_path, full_path_was, full_path) + expires_full_path_cache - Rails.logger.error "Exception moving path #{repository_storage_path} from #{full_path_was} to #{full_path}" + move_repositories - # if we cannot move namespace directory we should rollback - # db changes in order to prevent out of sync between db and fs - raise Gitlab::UpdatePathError.new('namespace directory cannot be moved') - end + if parent_changed? + former_parent_full_path = parent_was&.full_path + parent_full_path = parent&.full_path + Gitlab::UploadsTransfer.new.move_namespace(path, former_parent_full_path, parent_full_path) + Gitlab::PagesTransfer.new.move_namespace(path, former_parent_full_path, parent_full_path) + else + Gitlab::UploadsTransfer.new.rename_namespace(full_path_was, full_path) + Gitlab::PagesTransfer.new.rename_namespace(full_path_was, full_path) end - Gitlab::UploadsTransfer.new.rename_namespace(full_path_was, full_path) - Gitlab::PagesTransfer.new.rename_namespace(full_path_was, full_path) - remove_exports! # If repositories moved successfully we need to @@ -57,6 +52,26 @@ module Storage private + def move_repositories + # Move the namespace directory in all storage paths used by member projects + repository_storage_paths.each do |repository_storage_path| + # Ensure old directory exists before moving it + gitlab_shell.add_namespace(repository_storage_path, full_path_was) + + # Ensure new directory exists before moving it (if there's a parent) + gitlab_shell.add_namespace(repository_storage_path, parent.full_path) if parent + + unless gitlab_shell.mv_namespace(repository_storage_path, full_path_was, full_path) + + Rails.logger.error "Exception moving path #{repository_storage_path} from #{full_path_was} to #{full_path}" + + # if we cannot move namespace directory we should rollback + # db changes in order to prevent out of sync between db and fs + raise Gitlab::UpdatePathError.new('namespace directory cannot be moved') + end + end + end + def old_repository_storage_paths @old_repository_storage_paths ||= repository_storage_paths end diff --git a/app/models/concerns/updated_at_filterable.rb b/app/models/concerns/updated_at_filterable.rb new file mode 100644 index 00000000000..edb423b7828 --- /dev/null +++ b/app/models/concerns/updated_at_filterable.rb @@ -0,0 +1,12 @@ +module UpdatedAtFilterable + extend ActiveSupport::Concern + + included do + scope :updated_before, ->(date) { where(scoped_table[:updated_at].lteq(date)) } + scope :updated_after, ->(date) { where(scoped_table[:updated_at].gteq(date)) } + + def self.scoped_table + arel_table.alias(table_name) + end + end +end diff --git a/app/models/cycle_analytics/summary.rb b/app/models/cycle_analytics/summary.rb deleted file mode 100644 index e69de29bb2d..00000000000 --- a/app/models/cycle_analytics/summary.rb +++ /dev/null diff --git a/app/models/deployment.rb b/app/models/deployment.rb index b6cf168d60e..e18ea8bfea4 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -1,5 +1,5 @@ class Deployment < ActiveRecord::Base - include InternalId + include NonatomicInternalId belongs_to :project, required: true belongs_to :environment, required: true @@ -98,28 +98,29 @@ class Deployment < ActiveRecord::Base end def has_metrics? - project.monitoring_service.present? + prometheus_adapter&.can_query? end def metrics return {} unless has_metrics? - project.monitoring_service.deployment_metrics(self) - end - - def has_additional_metrics? - project.prometheus_service.present? + metrics = prometheus_adapter.query(:deployment, self) + metrics&.merge(deployment_time: created_at.to_i) || {} end def additional_metrics - return {} unless project.prometheus_service.present? + return {} unless has_metrics? - metrics = project.prometheus_service.additional_deployment_metrics(self) + metrics = prometheus_adapter.query(:additional_metrics_deployment, self) metrics&.merge(deployment_time: created_at.to_i) || {} end private + def prometheus_adapter + environment.prometheus_adapter + end + def ref_path File.join(environment.ref_path, 'deployments', iid.to_s) end diff --git a/app/models/environment.rb b/app/models/environment.rb index f78c21aebe5..9517723d9d9 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -65,10 +65,9 @@ class Environment < ActiveRecord::Base end def predefined_variables - [ - { key: 'CI_ENVIRONMENT_NAME', value: name, public: true }, - { key: 'CI_ENVIRONMENT_SLUG', value: slug, public: true } - ] + Gitlab::Ci::Variables::Collection.new + .append(key: 'CI_ENVIRONMENT_NAME', value: name) + .append(key: 'CI_ENVIRONMENT_SLUG', value: slug) end def recently_updated_on_branch?(ref) @@ -99,8 +98,8 @@ class Environment < ActiveRecord::Base folder_name == "production" end - def first_deployment_for(commit) - ref = project.repository.ref_name_for_sha(ref_path, commit.sha) + def first_deployment_for(commit_sha) + ref = project.repository.ref_name_for_sha(ref_path, commit_sha) return nil unless ref @@ -146,21 +145,19 @@ class Environment < ActiveRecord::Base end def has_metrics? - project.monitoring_service.present? && available? && last_deployment.present? + prometheus_adapter&.can_query? && available? && last_deployment.present? end def metrics - project.monitoring_service.environment_metrics(self) if has_metrics? + prometheus_adapter.query(:environment, self) if has_metrics? end - def has_additional_metrics? - project.prometheus_service.present? && available? && last_deployment.present? + def additional_metrics + prometheus_adapter.query(:additional_metrics_environment, self) if has_metrics? end - def additional_metrics - if has_additional_metrics? - project.prometheus_service.additional_environment_metrics(self) - end + def prometheus_adapter + @prometheus_adapter ||= Prometheus::AdapterService.new(project, deployment_platform).prometheus_adapter end def slug @@ -226,6 +223,10 @@ class Environment < ActiveRecord::Base self.environment_type || self.name end + def deployment_platform + project.deployment_platform(environment: self) + end + private # Slugifying a name may remove the uniqueness guarantee afforded by it being diff --git a/app/models/event.rb b/app/models/event.rb index 75538ba196c..17a198d52c7 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -65,6 +65,7 @@ class Event < ActiveRecord::Base # Callbacks after_create :reset_project_activity after_create :set_last_repository_updated_at, if: :push? + after_create :track_user_interacted_projects # Scopes scope :recent, -> { reorder(id: :desc) } @@ -158,7 +159,7 @@ class Event < ActiveRecord::Base def project_name if project - project.name_with_namespace + project.full_name else "(deleted project)" end @@ -389,4 +390,11 @@ class Event < ActiveRecord::Base Project.unscoped.where(id: project_id) .update_all(last_repository_updated_at: created_at) end + + def track_user_interacted_projects + # Note the call to .available? is due to earlier migrations + # that would otherwise conflict with the call to .track + # (because the table does not exist yet). + UserInteractedProject.track(self) if UserInteractedProject.available? + end end diff --git a/app/models/group.rb b/app/models/group.rb index 75bf013ecd2..f669b1a7009 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -31,6 +31,9 @@ class Group < Namespace has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent + has_many :boards + has_many :badges, class_name: 'GroupBadge' + accepts_nested_attributes_for :variables, allow_destroy: true validate :visibility_level_allowed_by_projects @@ -227,13 +230,13 @@ class Group < Namespace end GroupMember - .active_without_invites + .active_without_invites_and_requests .where(source_id: source_ids) end def members_with_descendants GroupMember - .active_without_invites + .active_without_invites_and_requests .where(source_id: self_and_descendants.reorder(nil).select(:id)) end diff --git a/app/models/internal_id.rb b/app/models/internal_id.rb new file mode 100644 index 00000000000..cbec735c2dd --- /dev/null +++ b/app/models/internal_id.rb @@ -0,0 +1,125 @@ +# An InternalId is a strictly monotone sequence of integers +# generated for a given scope and usage. +# +# For example, issues use their project to scope internal ids: +# In that sense, scope is "project" and usage is "issues". +# Generated internal ids for an issue are unique per project. +# +# See InternalId#usage enum for available usages. +# +# In order to leverage InternalId for other usages, the idea is to +# * Add `usage` value to enum +# * (Optionally) add columns to `internal_ids` if needed for scope. +class InternalId < ActiveRecord::Base + belongs_to :project + + enum usage: { issues: 0 } + + validates :usage, presence: true + + REQUIRED_SCHEMA_VERSION = 20180305095250 + + # Increments #last_value and saves the record + # + # The operation locks the record and gathers a `ROW SHARE` lock (in PostgreSQL). + # As such, the increment is atomic and safe to be called concurrently. + def increment_and_save! + lock! + self.last_value = (last_value || 0) + 1 + save! + last_value + end + + class << self + def generate_next(subject, scope, usage, init) + # Shortcut if `internal_ids` table is not available (yet) + # This can be the case in other (unrelated) migration specs + return (init.call(subject) || 0) + 1 unless available? + + InternalIdGenerator.new(subject, scope, usage, init).generate + end + + def available? + @available_flag ||= ActiveRecord::Migrator.current_version >= REQUIRED_SCHEMA_VERSION # rubocop:disable Gitlab/PredicateMemoization + end + + # Flushes cached information about schema + def reset_column_information + @available_flag = nil + super + end + end + + class InternalIdGenerator + # Generate next internal id for a given scope and usage. + # + # For currently supported usages, see #usage enum. + # + # The method implements a locking scheme that has the following properties: + # 1) Generated sequence of internal ids is unique per (scope and usage) + # 2) The method is thread-safe and may be used in concurrent threads/processes. + # 3) The generated sequence is gapless. + # 4) In the absence of a record in the internal_ids table, one will be created + # and last_value will be calculated on the fly. + # + # subject: The instance we're generating an internal id for. Gets passed to init if called. + # scope: Attributes that define the scope for id generation. + # usage: Symbol to define the usage of the internal id, see InternalId.usages + # init: Block that gets called to initialize InternalId record if not present + # Make sure to not throw exceptions in the absence of records (if this is expected). + attr_reader :subject, :scope, :init, :scope_attrs, :usage + + def initialize(subject, scope, usage, init) + @subject = subject + @scope = scope + @init = init + @usage = usage + + raise ArgumentError, 'Scope is not well-defined, need at least one column for scope (given: 0)' if scope.empty? + + unless InternalId.usages.has_key?(usage.to_s) + raise ArgumentError, "Usage '#{usage}' is unknown. Supported values are #{InternalId.usages.keys} from InternalId.usages" + end + end + + # Generates next internal id and returns it + def generate + subject.transaction do + # Create a record in internal_ids if one does not yet exist + # and increment its last value + # + # Note this will acquire a ROW SHARE lock on the InternalId record + (lookup || create_record).increment_and_save! + end + end + + private + + # Retrieve InternalId record for (project, usage) combination, if it exists + def lookup + InternalId.find_by(**scope, usage: usage_value) + end + + def usage_value + @usage_value ||= InternalId.usages[usage.to_s] + end + + # Create InternalId record for (scope, usage) combination, if it doesn't exist + # + # We blindly insert without synchronization. If another process + # was faster in doing this, we'll realize once we hit the unique key constraint + # violation. We can safely roll-back the nested transaction and perform + # a lookup instead to retrieve the record. + def create_record + subject.transaction(requires_new: true) do + InternalId.create!( + **scope, + usage: usage_value, + last_value: init.call(subject) || 0 + ) + end + rescue ActiveRecord::RecordNotUnique + lookup + end + end +end diff --git a/app/models/issue.rb b/app/models/issue.rb index c81f7e52bb1..7bfc45c1f43 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -1,7 +1,7 @@ require 'carrierwave/orm/activerecord' class Issue < ActiveRecord::Base - include InternalId + include AtomicInternalId include Issuable include Noteable include Referable @@ -24,6 +24,8 @@ class Issue < ActiveRecord::Base belongs_to :project belongs_to :moved_to, class_name: 'Issue' + has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.issues&.maximum(:iid) } + has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :merge_requests_closing_issues, diff --git a/app/models/label.rb b/app/models/label.rb index 7538f2d8718..de7f1d56c64 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -35,6 +35,7 @@ class Label < ActiveRecord::Base scope :templates, -> { where(template: true) } scope :with_title, ->(title) { where(title: title) } scope :with_lists_and_board, -> { joins(lists: :board).merge(List.movable) } + scope :on_group_boards, ->(group_id) { with_lists_and_board.where(boards: { group_id: group_id }) } scope :on_project_boards, ->(project_id) { with_lists_and_board.where(boards: { project_id: project_id }) } def self.prioritized(project) diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb index fc586fa216e..b444812a4cf 100644 --- a/app/models/lfs_object.rb +++ b/app/models/lfs_object.rb @@ -15,4 +15,8 @@ class LfsObject < ActiveRecord::Base .where(lfs_objects_projects: { id: nil }) .destroy_all end + + def self.calculate_oid(path) + Digest::SHA256.file(path).hexdigest + end end diff --git a/app/models/member.rb b/app/models/member.rb index 408e8b2d704..e1a32148538 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -52,10 +52,10 @@ class Member < ActiveRecord::Base end # Like active, but without invites. For when a User is required. - scope :active_without_invites, -> do + scope :active_without_invites_and_requests, -> do left_join_users .where(users: { state: 'active' }) - .where(requested_at: nil) + .non_request .reorder(nil) end @@ -85,6 +85,7 @@ class Member < ActiveRecord::Base after_create :create_notification_setting, unless: [:pending?, :importing?] after_create :post_create_hook, unless: [:pending?, :importing?] after_update :post_update_hook, unless: [:pending?, :importing?] + after_destroy :destroy_notification_setting after_destroy :post_destroy_hook, unless: :pending? after_commit :refresh_member_authorized_projects @@ -315,6 +316,10 @@ class Member < ActiveRecord::Base user.notification_settings.find_or_create_for(source) end + def destroy_notification_setting + notification_setting&.destroy + end + def notification_setting @notification_setting ||= user&.notification_settings_for(source) end diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index b6f1dd272cd..1c7ed4a96df 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -13,8 +13,6 @@ class ProjectMember < Member scope :in_project, ->(project) { where(source_id: project.id) } - before_destroy :delete_member_todos - class << self # Add users to projects with passed access option # @@ -93,10 +91,6 @@ class ProjectMember < Member private - def delete_member_todos - user.todos.where(project_id: source_id).destroy_all if user - end - def send_invite notification_service.invite_project_member(self, @raw_invite_token) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 5bec68ce4f6..7e6d89ec9c7 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -1,5 +1,5 @@ class MergeRequest < ActiveRecord::Base - include InternalId + include NonatomicInternalId include Issuable include Noteable include Referable @@ -375,15 +375,27 @@ class MergeRequest < ActiveRecord::Base end def diff_start_sha - diff_start_commit.try(:sha) + if persisted? + merge_request_diff.start_commit_sha + else + target_branch_head.try(:sha) + end end def diff_base_sha - diff_base_commit.try(:sha) + if persisted? + merge_request_diff.base_commit_sha + else + branch_merge_base_commit.try(:sha) + end end def diff_head_sha - diff_head_commit.try(:sha) + if persisted? + merge_request_diff.head_commit_sha + else + source_branch_head.try(:sha) + end end # When importing a pull request from GitHub, the old and new branches may no @@ -567,9 +579,10 @@ class MergeRequest < ActiveRecord::Base return unless open? old_diff_refs = self.diff_refs + new_diff = create_merge_request_diff + + MergeRequests::MergeRequestDiffCacheService.new.execute(self, new_diff) - create_merge_request_diff - MergeRequests::MergeRequestDiffCacheService.new.execute(self) new_diff_refs = self.diff_refs update_diff_discussion_positions( @@ -646,7 +659,7 @@ class MergeRequest < ActiveRecord::Base !ProtectedBranch.protected?(source_project, source_branch) && !source_project.root_ref?(source_branch) && Ability.allowed?(current_user, :push_code, source_project) && - diff_head_commit == source_branch_head + diff_head_sha == source_branch_head.try(:sha) end def should_remove_source_branch? @@ -853,7 +866,7 @@ class MergeRequest < ActiveRecord::Base def can_be_merged_by?(user) access = ::Gitlab::UserAccess.new(user, project: project) - access.can_push_to_branch?(target_branch) || access.can_merge_to_branch?(target_branch) + access.can_update_branch?(target_branch) end def can_be_merged_via_command_line_by?(user) @@ -1075,4 +1088,22 @@ class MergeRequest < ActiveRecord::Base project.merge_requests.merged.where(author_id: author_id).empty? end + + def allow_maintainer_to_push + maintainer_push_possible? && super + end + + alias_method :allow_maintainer_to_push?, :allow_maintainer_to_push + + def maintainer_push_possible? + source_project.present? && for_fork? && + target_project.visibility_level > Gitlab::VisibilityLevel::PRIVATE && + source_project.visibility_level > Gitlab::VisibilityLevel::PRIVATE && + !ProtectedBranch.protected?(source_project, source_branch) + end + + def can_allow_maintainer_to_push?(user) + maintainer_push_possible? && + Ability.allowed?(user, :push_code, source_project) + end end diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 77c19380e66..e7d397f40f5 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -8,7 +8,7 @@ class Milestone < ActiveRecord::Base Started = MilestoneStruct.new('Started', '#started', -3) include CacheMarkdownField - include InternalId + include NonatomicInternalId include Sortable include Referable include StripAttribute diff --git a/app/models/namespace.rb b/app/models/namespace.rb index db274ea8172..e350b675639 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -222,6 +222,11 @@ class Namespace < ActiveRecord::Base has_parent? end + # Overridden on EE module + def multiple_issue_boards_available? + false + end + def full_path_was if parent_id_was.nil? path_was diff --git a/app/models/network/commit.rb b/app/models/network/commit.rb index 9357e55b419..22d48c9e661 100644 --- a/app/models/network/commit.rb +++ b/app/models/network/commit.rb @@ -24,12 +24,7 @@ module Network end def parents(map) - @commit.parents.map do |p| - if map.include?(p.id) - map[p.id] - end - end - .compact + map.values_at(*@commit.parent_ids).compact end end end diff --git a/app/models/note.rb b/app/models/note.rb index d7a67ec277c..787a80f0196 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -81,7 +81,7 @@ class Note < ActiveRecord::Base validates :author, presence: true validates :discussion_id, presence: true, format: { with: /\A\h{40}\z/ } - validate unless: [:for_commit?, :importing?, :for_personal_snippet?] do |note| + validate unless: [:for_commit?, :importing?, :skip_project_check?] do |note| unless note.noteable.try(:project) == note.project errors.add(:project, 'does not match noteable project') end @@ -228,7 +228,7 @@ class Note < ActiveRecord::Base end def skip_project_check? - for_personal_snippet? + !for_project_noteable? end def commit @@ -308,6 +308,11 @@ class Note < ActiveRecord::Base self.noteable.supports_discussions? && !part_of_discussion? end + def can_create_todo? + # Skip system notes, and notes on project snippet + !system? && !for_snippet? + end + def discussion_class(noteable = nil) # When commit notes are rendered on an MR's Discussion page, they are # displayed in one discussion instead of individually. diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb index fd70e920c7e..e95655e19f8 100644 --- a/app/models/notification_recipient.rb +++ b/app/models/notification_recipient.rb @@ -35,7 +35,8 @@ class NotificationRecipient # check this last because it's expensive # nobody should receive notifications if they've specifically unsubscribed - return false if unsubscribed? + # except if they were mentioned. + return false if @type != :mention && unsubscribed? true end diff --git a/app/models/project.rb b/app/models/project.rb index ba278a49688..e5ede967668 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -150,6 +150,7 @@ class Project < ActiveRecord::Base # Merge Requests for target project should be removed with it has_many :merge_requests, foreign_key: 'target_project_id' + has_many :source_of_merge_requests, foreign_key: 'source_project_id', class_name: 'MergeRequest' has_many :issues has_many :labels, class_name: 'ProjectLabel' has_many :services @@ -187,6 +188,8 @@ class Project < ActiveRecord::Base has_many :todos has_many :notification_settings, as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent + has_many :internal_ids + has_one :import_data, class_name: 'ProjectImportData', inverse_of: :project, autosave: true has_one :project_feature, inverse_of: :project has_one :statistics, class_name: 'ProjectStatistics' @@ -221,6 +224,8 @@ class Project < ActiveRecord::Base has_one :auto_devops, class_name: 'ProjectAutoDevops' has_many :custom_attributes, class_name: 'ProjectCustomAttribute' + has_many :project_badges, class_name: 'ProjectBadge' + accepts_nested_attributes_for :variables, allow_destroy: true accepts_nested_attributes_for :project_feature, update_only: true accepts_nested_attributes_for :import_data @@ -274,7 +279,8 @@ class Project < ActiveRecord::Base scope :without_storage_feature, ->(feature) { where('storage_version < :version OR storage_version IS NULL', version: HASHED_STORAGE_FEATURES[feature]) } scope :with_unmigrated_storage, -> { where('storage_version < :version OR storage_version IS NULL', version: LATEST_STORAGE_VERSION) } - scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) } + # last_activity_at is throttled every minute, but last_repository_updated_at is updated with every push + scope :sorted_by_activity, -> { reorder("GREATEST(COALESCE(last_activity_at, '1970-01-01'), COALESCE(last_repository_updated_at, '1970-01-01')) DESC") } scope :sorted_by_stars, -> { reorder('projects.star_count DESC') } scope :in_namespace, ->(namespace_ids) { where(namespace_id: namespace_ids) } @@ -317,42 +323,13 @@ class Project < ActiveRecord::Base # Returns a collection of projects that is either public or visible to the # logged in user. - # - # A caller may pass in a block to modify individual parts of - # the query, e.g. to apply .with_feature_available_for_user on top of it. - # This is useful for performance as we can stick those additional filters - # at the bottom of e.g. the UNION. - # - # Optionally, turning `use_where_in` off leads to returning a - # relation using #from instead of #where. This can perform much better - # but leads to trouble when used in conjunction with AR's #merge method. - def self.public_or_visible_to_user(user = nil, use_where_in: true, &block) - # If we don't get a block passed, use identity to avoid if/else repetitions - block = ->(part) { part } unless block_given? - - return block.call(public_to_user) unless user - - # If the user is allowed to see all projects, - # we can shortcut and just return. - return block.call(all) if user.full_private_access? - - authorized = user - .project_authorizations - .select(1) - .where('project_authorizations.project_id = projects.id') - authorized_projects = block.call(where('EXISTS (?)', authorized)) - - levels = Gitlab::VisibilityLevel.levels_for_user(user) - visible_projects = block.call(where(visibility_level: levels)) - - # We use a UNION here instead of OR clauses since this results in better - # performance. - union = Gitlab::SQL::Union.new([authorized_projects.select('projects.id'), visible_projects.select('projects.id')]) - - if use_where_in - where("projects.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection + def self.public_or_visible_to_user(user = nil) + if user + where('EXISTS (?) OR projects.visibility_level IN (?)', + user.authorizations_for_projects, + Gitlab::VisibilityLevel.levels_for_user(user)) else - from("(#{union.to_sql}) AS #{table_name}") + public_to_user end end @@ -371,14 +348,11 @@ class Project < ActiveRecord::Base elsif user column = ProjectFeature.quoted_access_level_column(feature) - authorized = user.project_authorizations.select(1) - .where('project_authorizations.project_id = projects.id') - with_project_feature .where("#{column} IN (?) OR (#{column} = ? AND EXISTS (?))", visible, ProjectFeature::PRIVATE, - authorized) + user.authorizations_for_projects) else with_feature_access_level(feature, visible) end @@ -570,7 +544,7 @@ class Project < ActiveRecord::Base latest_pipeline = pipelines.latest_successful_for(ref) if latest_pipeline - latest_pipeline.builds.latest.with_artifacts + latest_pipeline.builds.latest.with_artifacts_archive else builds.none end @@ -808,7 +782,7 @@ class Project < ActiveRecord::Base end def last_activity_date - last_repository_updated_at || last_activity_at || updated_at + [last_activity_at, last_repository_updated_at, updated_at].compact.max end def project_id @@ -1111,7 +1085,7 @@ class Project < ActiveRecord::Base # Forked import is handled asynchronously return if forked? && !force - if gitlab_shell.add_repository(repository_storage, disk_path) + if gitlab_shell.create_repository(repository_storage, disk_path) repository.after_create true else @@ -1547,8 +1521,8 @@ class Project < ActiveRecord::Base @errors = original_errors end - def add_export_job(current_user:) - job_id = ProjectExportWorker.perform_async(current_user.id, self.id) + def add_export_job(current_user:, params: {}) + job_id = ProjectExportWorker.perform_async(current_user.id, self.id, params) if job_id Rails.logger.info "Export job started for project ID #{self.id} with job ID #{job_id}" @@ -1557,16 +1531,34 @@ class Project < ActiveRecord::Base end end + def import_export_shared + @import_export_shared ||= Gitlab::ImportExport::Shared.new(self) + end + def export_path return nil unless namespace.present? || hashed_storage?(:repository) - File.join(Gitlab::ImportExport.storage_path, disk_path) + import_export_shared.archive_path end def export_project_path Dir.glob("#{export_path}/*export.tar.gz").max_by { |f| File.ctime(f) } end + def export_status + if export_in_progress? + :started + elsif export_project_path + :finished + else + :none + end + end + + def export_in_progress? + import_export_shared.active_export_count > 0 + end + def remove_exports return nil unless export_path.present? @@ -1582,29 +1574,30 @@ class Project < ActiveRecord::Base end def predefined_variables - [ - { key: 'CI_PROJECT_ID', value: id.to_s, public: true }, - { key: 'CI_PROJECT_NAME', value: path, public: true }, - { key: 'CI_PROJECT_PATH', value: full_path, public: true }, - { key: 'CI_PROJECT_PATH_SLUG', value: full_path_slug, public: true }, - { key: 'CI_PROJECT_NAMESPACE', value: namespace.full_path, public: true }, - { key: 'CI_PROJECT_URL', value: web_url, public: true }, - { key: 'CI_PROJECT_VISIBILITY', value: Gitlab::VisibilityLevel.string_level(visibility_level), public: true } - ] + visibility = Gitlab::VisibilityLevel.string_level(visibility_level) + + Gitlab::Ci::Variables::Collection.new + .append(key: 'CI_PROJECT_ID', value: id.to_s) + .append(key: 'CI_PROJECT_NAME', value: path) + .append(key: 'CI_PROJECT_PATH', value: full_path) + .append(key: 'CI_PROJECT_PATH_SLUG', value: full_path_slug) + .append(key: 'CI_PROJECT_NAMESPACE', value: namespace.full_path) + .append(key: 'CI_PROJECT_URL', value: web_url) + .append(key: 'CI_PROJECT_VISIBILITY', value: visibility) + .concat(container_registry_variables) + .concat(auto_devops_variables) end def container_registry_variables - return [] unless Gitlab.config.registry.enabled + Gitlab::Ci::Variables::Collection.new.tap do |variables| + return variables unless Gitlab.config.registry.enabled - variables = [ - { key: 'CI_REGISTRY', value: Gitlab.config.registry.host_port, public: true } - ] + variables.append(key: 'CI_REGISTRY', value: Gitlab.config.registry.host_port) - if container_registry_enabled? - variables << { key: 'CI_REGISTRY_IMAGE', value: container_registry_url, public: true } + if container_registry_enabled? + variables.append(key: 'CI_REGISTRY_IMAGE', value: container_registry_url) + end end - - variables end def secret_variables_for(ref:, environment: nil) @@ -1624,16 +1617,14 @@ class Project < ActiveRecord::Base end end - def deployment_variables - return [] unless deployment_platform - - deployment_platform.predefined_variables + def deployment_variables(environment: nil) + deployment_platform(environment: environment)&.predefined_variables || [] end def auto_devops_variables return [] unless auto_devops_enabled? - (auto_devops || build_auto_devops)&.variables + (auto_devops || build_auto_devops)&.predefined_variables end def append_or_update_attribute(name, value) @@ -1695,8 +1686,9 @@ class Project < ActiveRecord::Base end end - def multiple_issue_boards_available?(user) - feature_available?(:multiple_issue_boards, user) + # Overridden on EE module + def multiple_issue_boards_available? + false end def issue_board_milestone_available?(user = nil) @@ -1798,6 +1790,44 @@ class Project < ActiveRecord::Base .set(import_jid, StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION) end + def badges + return project_badges unless group + + group_badges_rel = GroupBadge.where(group: group.self_and_ancestors) + + union = Gitlab::SQL::Union.new([project_badges.select(:id), + group_badges_rel.select(:id)]) + + Badge.where("id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection + end + + def merge_requests_allowing_push_to_user(user) + return MergeRequest.none unless user + + developer_access_exists = user.project_authorizations + .where('access_level >= ? ', Gitlab::Access::DEVELOPER) + .where('project_authorizations.project_id = merge_requests.target_project_id') + .limit(1) + .select(1) + source_of_merge_requests.opened + .where(allow_maintainer_to_push: true) + .where('EXISTS (?)', developer_access_exists) + end + + def branch_allows_maintainer_push?(user, branch_name) + return false unless user + + cache_key = "user:#{user.id}:#{branch_name}:branch_allows_push" + + memoized_results = strong_memoize(:branch_allows_maintainer_push) do + Hash.new do |result, cache_key| + result[cache_key] = fetch_branch_allows_maintainer_push?(user, branch_name) + end + end + + memoized_results[cache_key] + end + private def storage @@ -1920,4 +1950,22 @@ class Project < ActiveRecord::Base raise ex end + + def fetch_branch_allows_maintainer_push?(user, branch_name) + check_access = -> do + merge_request = source_of_merge_requests.opened + .where(allow_maintainer_to_push: true) + .find_by(source_branch: branch_name) + + merge_request&.can_be_merged_by?(user) + end + + if RequestStore.active? + RequestStore.fetch("project-#{id}:branch-#{branch_name}:user-#{user.id}:branch_allows_maintainer_push") do + check_access.call + end + else + check_access.call + end + end end diff --git a/app/models/project_auto_devops.rb b/app/models/project_auto_devops.rb index 112ed7ed434..ed6c1eddbc1 100644 --- a/app/models/project_auto_devops.rb +++ b/app/models/project_auto_devops.rb @@ -14,9 +14,12 @@ class ProjectAutoDevops < ActiveRecord::Base domain.present? || instance_domain.present? end - def variables - variables = [] - variables << { key: 'AUTO_DEVOPS_DOMAIN', value: domain.presence || instance_domain, public: true } if has_domain? - variables + def predefined_variables + Gitlab::Ci::Variables::Collection.new.tap do |variables| + if has_domain? + variables.append(key: 'AUTO_DEVOPS_DOMAIN', + value: domain.presence || instance_domain) + end + end end end diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb index 109258d1eb7..4f289e6e215 100644 --- a/app/models/project_services/asana_service.rb +++ b/app/models/project_services/asana_service.rb @@ -68,7 +68,7 @@ http://app.asana.com/-/account_api' end user = data[:user_name] - project_name = project.name_with_namespace + project_name = project.full_name data[:commits].each do |commit| push_msg = "#{user} pushed to branch #{branch} of #{project_name} ( #{commit[:url]} ):" diff --git a/app/models/project_services/campfire_service.rb b/app/models/project_services/campfire_service.rb index c3f5b310619..8d7a4fceb08 100644 --- a/app/models/project_services/campfire_service.rb +++ b/app/models/project_services/campfire_service.rb @@ -86,7 +86,7 @@ class CampfireService < Service after = push[:after] message = "" - message << "[#{project.name_with_namespace}] " + message << "[#{project.full_name}] " message << "#{push[:user_name]} " if Gitlab::Git.blank_ref?(before) diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb index 818cfb01b14..dab0ea1a681 100644 --- a/app/models/project_services/chat_notification_service.rb +++ b/app/models/project_services/chat_notification_service.rb @@ -99,7 +99,7 @@ class ChatNotificationService < Service def get_message(object_kind, data) case object_kind when "push", "tag_push" - ChatMessage::PushMessage.new(data) + ChatMessage::PushMessage.new(data) if notify_for_ref?(data) when "issue" ChatMessage::IssueMessage.new(data) unless update?(data) when "merge_request" @@ -129,7 +129,7 @@ class ChatNotificationService < Service end def project_name - project.name_with_namespace.gsub(/\s/, '') + project.full_name.gsub(/\s/, '') end def project_url @@ -145,10 +145,16 @@ class ChatNotificationService < Service end def notify_for_ref?(data) - return true if data[:object_attributes][:tag] + return true if data.dig(:object_attributes, :tag) return true unless notify_only_default_branch? - data[:object_attributes][:ref] == project.default_branch + ref = if data[:ref] + Gitlab::Git.ref_name(data[:ref]) + else + data.dig(:object_attributes, :ref) + end + + ref == project.default_branch end def notify_for_pipeline?(data) diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index bfe7ac29c18..f31c3f02af2 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -120,7 +120,7 @@ class HipchatService < Service else message << "pushed to #{ref_type} <a href=\""\ "#{project.web_url}/commits/#{CGI.escape(ref)}\">#{ref}</a> " - message << "of <a href=\"#{project.web_url}\">#{project.name_with_namespace.gsub!(/\s/, '')}</a> " + message << "of <a href=\"#{project.web_url}\">#{project.full_name.gsub!(/\s/, '')}</a> " message << "(<a href=\"#{project.web_url}/compare/#{before}...#{after}\">Compare changes</a>)" push[:commits].take(MAX_COMMITS).each do |commit| @@ -274,7 +274,7 @@ class HipchatService < Service end def project_name - project.name_with_namespace.gsub(/\s/, '') + project.full_name.gsub(/\s/, '') end def project_url diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 436a870b0c4..ed4bbfb6cfc 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -1,5 +1,7 @@ class JiraService < IssueTrackerService include Gitlab::Routing + include ApplicationHelper + include ActionView::Helpers::AssetUrlHelper validates :url, url: true, presence: true, if: :activated? validates :api_url, url: true, allow_blank: true @@ -12,9 +14,8 @@ class JiraService < IssueTrackerService alias_method :project_url, :url - # This is confusing, but JiraService does not really support these events. - # The values here are required to display correct options in the service - # configuration screen. + # When these are false GitLab does not create cross reference + # comments on JIRA except when an issue gets transitioned. def self.supported_events %w(commit merge_request) end @@ -159,11 +160,6 @@ class JiraService < IssueTrackerService add_comment(data, jira_issue) end - # reason why service cannot be tested - def disabled_title - "Please fill in Password and Username." - end - def test(_) result = test_settings success = result.present? @@ -268,7 +264,9 @@ class JiraService < IssueTrackerService url: url, title: title, status: status, - icon: { title: 'GitLab', url16x16: 'https://gitlab.com/favicon.ico' } + icon: { + title: 'GitLab', url16x16: asset_url('favicon.ico', host: gitlab_config.url) + } } } end @@ -319,4 +317,13 @@ class JiraService < IssueTrackerService url_changed? end + + def self.event_description(event) + case event + when "merge_request", "merge_request_events" + "JIRA comments will be created when an issue gets referenced in a merge request." + when "commit", "commit_events" + "JIRA comments will be created when an issue gets referenced in a commit." + end + end end diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb index ad4ad7903ad..20fed432e55 100644 --- a/app/models/project_services/kubernetes_service.rb +++ b/app/models/project_services/kubernetes_service.rb @@ -105,19 +105,19 @@ class KubernetesService < DeploymentService def predefined_variables config = YAML.dump(kubeconfig) - variables = [ - { key: 'KUBE_URL', value: api_url, public: true }, - { key: 'KUBE_TOKEN', value: token, public: false }, - { key: 'KUBE_NAMESPACE', value: actual_namespace, public: true }, - { key: 'KUBECONFIG', value: config, public: false, file: true } - ] - - if ca_pem.present? - variables << { key: 'KUBE_CA_PEM', value: ca_pem, public: true } - variables << { key: 'KUBE_CA_PEM_FILE', value: ca_pem, public: true, file: true } + Gitlab::Ci::Variables::Collection.new.tap do |variables| + variables + .append(key: 'KUBE_URL', value: api_url) + .append(key: 'KUBE_TOKEN', value: token, public: false) + .append(key: 'KUBE_NAMESPACE', value: actual_namespace) + .append(key: 'KUBECONFIG', value: config, public: false, file: true) + + if ca_pem.present? + variables + .append(key: 'KUBE_CA_PEM', value: ca_pem) + .append(key: 'KUBE_CA_PEM_FILE', value: ca_pem, file: true) + end end - - variables end # Constructs a list of terminals from the reactive cache @@ -197,7 +197,7 @@ class KubernetesService < DeploymentService kubeclient = build_kubeclient! kubeclient.get_pods(namespace: actual_namespace).as_json - rescue KubeException => err + rescue Kubeclient::HttpError => err raise err unless err.error_code == 404 [] diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb index 4d2037286a2..227d430083d 100644 --- a/app/models/project_services/mattermost_slash_commands_service.rb +++ b/app/models/project_services/mattermost_slash_commands_service.rb @@ -37,7 +37,7 @@ class MattermostSlashCommandsService < SlashCommandsService private def command(params) - pretty_project_name = project.name_with_namespace + pretty_project_name = project.full_name params.merge( auto_complete: true, diff --git a/app/models/project_services/monitoring_service.rb b/app/models/project_services/monitoring_service.rb index ee9cd78327a..9af68b4e821 100644 --- a/app/models/project_services/monitoring_service.rb +++ b/app/models/project_services/monitoring_service.rb @@ -9,11 +9,11 @@ class MonitoringService < Service %w() end - def environment_metrics(environment) + def can_query? raise NotImplementedError end - def deployment_metrics(deployment) + def query(_, *_) raise NotImplementedError end end diff --git a/app/models/project_services/pipelines_email_service.rb b/app/models/project_services/pipelines_email_service.rb index 9c7b58dead5..4cf149ac044 100644 --- a/app/models/project_services/pipelines_email_service.rb +++ b/app/models/project_services/pipelines_email_service.rb @@ -39,10 +39,6 @@ class PipelinesEmailService < Service project.pipelines.any? end - def disabled_title - 'Please setup a pipeline on your repository.' - end - def test_data(project, user) data = Gitlab::DataBuilder::Pipeline.build(project.pipelines.last) data[:user] = user.hook_attrs diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb index 58731451429..dcaeb65dc32 100644 --- a/app/models/project_services/prometheus_service.rb +++ b/app/models/project_services/prometheus_service.rb @@ -1,9 +1,5 @@ class PrometheusService < MonitoringService - include ReactiveService - - self.reactive_cache_lease_timeout = 30.seconds - self.reactive_cache_refresh_interval = 30.seconds - self.reactive_cache_lifetime = 1.minute + include PrometheusAdapter # Access to prometheus is directly through the API prop_accessor :api_url @@ -13,7 +9,7 @@ class PrometheusService < MonitoringService validates :api_url, url: true end - before_save :synchronize_service_state! + before_save :synchronize_service_state after_save :clear_reactive_cache! @@ -66,63 +62,15 @@ class PrometheusService < MonitoringService # Check we can connect to the Prometheus API def test(*args) - client.ping + Gitlab::PrometheusClient.new(prometheus_client).ping { success: true, result: 'Checked API endpoint' } rescue Gitlab::PrometheusClient::Error => err { success: false, result: err } end - def environment_metrics(environment) - with_reactive_cache(Gitlab::Prometheus::Queries::EnvironmentQuery.name, environment.id, &rename_field(:data, :metrics)) - end - - def deployment_metrics(deployment) - metrics = with_reactive_cache(Gitlab::Prometheus::Queries::DeploymentQuery.name, deployment.environment.id, deployment.id, &rename_field(:data, :metrics)) - metrics&.merge(deployment_time: deployment.created_at.to_i) || {} - end - - def additional_environment_metrics(environment) - with_reactive_cache(Gitlab::Prometheus::Queries::AdditionalMetricsEnvironmentQuery.name, environment.id, &:itself) - end - - def additional_deployment_metrics(deployment) - with_reactive_cache(Gitlab::Prometheus::Queries::AdditionalMetricsDeploymentQuery.name, deployment.environment.id, deployment.id, &:itself) - end - - def matched_metrics - with_reactive_cache(Gitlab::Prometheus::Queries::MatchedMetricsQuery.name, &:itself) - end - - # Cache metrics for specific environment - def calculate_reactive_cache(query_class_name, *args) - return unless active? && project && !project.pending_delete? - - environment_id = args.first - client = client(environment_id) - - data = Kernel.const_get(query_class_name).new(client).query(*args) - { - success: true, - data: data, - last_update: Time.now.utc - } - rescue Gitlab::PrometheusClient::Error => err - { success: false, result: err.message } - end - - def client(environment_id = nil) - if manual_configuration? - Gitlab::PrometheusClient.new(RestClient::Resource.new(api_url)) - else - cluster = cluster_with_prometheus(environment_id) - raise Gitlab::PrometheusClient::Error, "couldn't find cluster with Prometheus installed" unless cluster - - rest_client = client_from_cluster(cluster) - raise Gitlab::PrometheusClient::Error, "couldn't create proxy Prometheus client" unless rest_client - - Gitlab::PrometheusClient.new(rest_client) - end + def prometheus_client + RestClient::Resource.new(api_url) if api_url && manual_configuration? && active? end def prometheus_installed? @@ -134,32 +82,7 @@ class PrometheusService < MonitoringService private - def cluster_with_prometheus(environment_id = nil) - clusters = if environment_id - ::Environment.find_by(id: environment_id).try do |env| - # sort results by descending order based on environment_scope being longer - # thus more closely matching environment slug - project.clusters.enabled.for_environment(env).sort_by { |c| c.environment_scope&.length }.reverse! - end - else - project.clusters.enabled.for_all_environments - end - - clusters&.detect { |cluster| cluster.application_prometheus&.installed? } - end - - def client_from_cluster(cluster) - cluster.application_prometheus.proxy_client - end - - def rename_field(old_field, new_field) - -> (metrics) do - metrics[new_field] = metrics.delete(old_field) - metrics - end - end - - def synchronize_service_state! + def synchronize_service_state self.active = prometheus_installed? || manual_configuration? true diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb index aa7bd4c3c84..e3a1ca2d45f 100644 --- a/app/models/project_services/pushover_service.rb +++ b/app/models/project_services/pushover_service.rb @@ -88,10 +88,10 @@ class PushoverService < Service user: user_key, device: device, priority: priority, - title: "#{project.name_with_namespace}", + title: "#{project.full_name}", message: message, url: data[:project][:web_url], - url_title: "See project #{project.name_with_namespace}" + url_title: "See project #{project.full_name}" } # Sound parameter MUST NOT be sent to API if not selected diff --git a/app/models/project_team.rb b/app/models/project_team.rb index a9e5cfb8240..33280eda0b9 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -85,6 +85,15 @@ class ProjectTeam @masters ||= fetch_members(Gitlab::Access::MASTER) end + def owners + @owners ||= + if group + group.owners + else + [project.owner] + end + end + def import(source_project, current_user = nil) target_project = project diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index f6041da986c..52e067cb44c 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -169,7 +169,7 @@ class ProjectWiki private def create_repo!(raw_repository) - gitlab_shell.add_repository(project.repository_storage, disk_path) + gitlab_shell.create_repository(project.repository_storage, disk_path) raise CouldNotCreateWikiError unless raw_repository.exists? diff --git a/app/models/repository.rb b/app/models/repository.rb index 242d9d5f125..42f1ac43e29 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -16,6 +16,7 @@ class Repository ].freeze include Gitlab::ShellAdapter + include Gitlab::RepositoryCacheAdapter attr_accessor :full_path, :disk_path, :project, :is_wiki @@ -35,7 +36,7 @@ class Repository CACHED_METHODS = %i(size commit_count rendered_readme contribution_guide changelog license_blob license_key gitignore koding_yml gitlab_ci_yml branch_names tag_names branch_count - tag_count avatar exists? empty? root_ref has_visible_content? + tag_count avatar exists? root_ref has_visible_content? issue_template_names merge_request_template_names).freeze # Methods that use cache_method but only memoize the value @@ -57,22 +58,6 @@ class Repository merge_request_template: :merge_request_template_names }.freeze - # Wraps around the given method and caches its output in Redis and an instance - # variable. - # - # This only works for methods that do not take any arguments. - def self.cache_method(name, fallback: nil, memoize_only: false) - original = :"_uncached_#{name}" - - alias_method(original, name) - - define_method(name) do - cache_method_output(name, fallback: fallback, memoize_only: memoize_only) do - __send__(original) # rubocop:disable GitlabSecurity/PublicSend - end - end - end - def initialize(full_path, project, disk_path: nil, is_wiki: false) @full_path = full_path @disk_path = disk_path || full_path @@ -253,7 +238,7 @@ class Repository # branches or tags, but we want to keep some of these commits around, for # example if they have comments or CI builds. def keep_around(sha) - return unless sha && commit_by(oid: sha) + return unless sha.present? && commit_by(oid: sha) return if kept_around?(sha) @@ -302,17 +287,6 @@ class Repository expire_method_caches(CACHED_METHODS) end - # Expires the caches of a specific set of methods - def expire_method_caches(methods) - methods.each do |key| - cache.expire(key) - - ivar = cache_instance_variable_name(key) - - remove_instance_variable(ivar) if instance_variable_defined?(ivar) - end - end - def expire_avatar_cache expire_method_caches(%i(avatar)) end @@ -360,7 +334,7 @@ class Repository def expire_emptiness_caches return unless empty? - expire_method_caches(%i(empty? has_visible_content?)) + expire_method_caches(%i(has_visible_content?)) end def lookup_cache @@ -506,12 +480,14 @@ class Repository end cache_method :exists? + # We don't need to cache the output of this method because both exists? and + # has_visible_content? are already memoized and cached. There's no guarantee + # that the values are expired and loaded atomically. def empty? return true unless exists? !has_visible_content? end - cache_method :empty? # The size of this repository in megabytes. def size @@ -651,14 +627,15 @@ class Repository end def last_commit_for_path(sha, path) - commit_by(oid: last_commit_id_for_path(sha, path)) + commit = raw_repository.last_commit_for_path(sha, path) + ::Commit.new(commit, @project) if commit end def last_commit_id_for_path(sha, path) key = path.blank? ? "last_commit_id_for_path:#{sha}" : "last_commit_id_for_path:#{sha}:#{Digest::SHA1.hexdigest(path)}" cache.fetch(key) do - raw_repository.last_commit_id_for_path(sha, path) + last_commit_for_path(sha, path)&.id end end @@ -866,20 +843,20 @@ class Repository raw_repository.ancestor?(ancestor_id, descendant_id) end - def fetch_as_mirror(url, forced: false, refmap: :all_refs, remote_name: nil) + def fetch_as_mirror(url, forced: false, refmap: :all_refs, remote_name: nil, prune: true) unless remote_name remote_name = "tmp-#{SecureRandom.hex}" tmp_remote_name = true end add_remote(remote_name, url, mirror_refmap: refmap) - fetch_remote(remote_name, forced: forced) + fetch_remote(remote_name, forced: forced, prune: prune) ensure remove_remote(remote_name) if tmp_remote_name end - def fetch_remote(remote, forced: false, ssh_auth: nil, no_tags: false) - gitlab_shell.fetch_remote(raw_repository, remote, ssh_auth: ssh_auth, forced: forced, no_tags: no_tags) + def fetch_remote(remote, forced: false, ssh_auth: nil, no_tags: false, prune: true) + gitlab_shell.fetch_remote(raw_repository, remote, ssh_auth: ssh_auth, forced: forced, no_tags: no_tags, prune: prune) end def fetch_source_branch!(source_repository, source_branch, local_ref) @@ -921,49 +898,6 @@ class Repository end end - # Caches the supplied block both in a cache and in an instance variable. - # - # The cache key and instance variable are named the same way as the value of - # the `key` argument. - # - # This method will return `nil` if the corresponding instance variable is also - # set to `nil`. This ensures we don't keep yielding the block when it returns - # `nil`. - # - # key - The name of the key to cache the data in. - # fallback - A value to fall back to in the event of a Git error. - def cache_method_output(key, fallback: nil, memoize_only: false, &block) - ivar = cache_instance_variable_name(key) - - if instance_variable_defined?(ivar) - instance_variable_get(ivar) - else - # If the repository doesn't exist and a fallback was specified we return - # that value inmediately. This saves us Rugged/gRPC invocations. - return fallback unless fallback.nil? || exists? - - begin - value = - if memoize_only - yield - else - cache.fetch(key, &block) - end - - instance_variable_set(ivar, value) - rescue Gitlab::Git::Repository::NoRepository - # Even if the above `#exists?` check passes these errors might still - # occur (for example because of a non-existing HEAD). We want to - # gracefully handle this and not cache anything - fallback - end - end - end - - def cache_instance_variable_name(key) - :"@#{key.to_s.tr('?!', '')}" - end - def file_on_head(type) if head = tree(:head) head.blobs.find do |blob| @@ -1018,8 +952,7 @@ class Repository end def cache - # TODO: should we use UUIDs here? We could move repositories without clearing this cache - @cache ||= RepositoryCache.new(full_path, @project.id) + @cache ||= Gitlab::RepositoryCache.new(self) end def tags_sorted_by_committed_date diff --git a/app/models/service.rb b/app/models/service.rb index 369cae2e85f..1dcb79157a2 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -129,6 +129,17 @@ class Service < ActiveRecord::Base fields end + def configurable_events + events = self.class.supported_events + + # No need to disable individual triggers when there is only one + if events.count == 1 + [] + else + events + end + end + def supported_events self.class.supported_events end @@ -151,11 +162,6 @@ class Service < ActiveRecord::Base true end - # reason why service cannot be tested - def disabled_title - "Please setup a project repository." - end - # Provide convenient accessor methods # for each serialized property. # Also keep track of updated properties in a similar way as ActiveModel::Dirty @@ -298,6 +304,29 @@ class Service < ActiveRecord::Base end end + def self.event_description(event) + case event + when "push", "push_events" + "Event will be triggered by a push to the repository" + when "tag_push", "tag_push_events" + "Event will be triggered when a new tag is pushed to the repository" + when "note", "note_events" + "Event will be triggered when someone adds a comment" + when "issue", "issue_events" + "Event will be triggered when an issue is created/updated/closed" + when "confidential_issue", "confidential_issue_events" + "Event will be triggered when a confidential issue is created/updated/closed" + when "merge_request", "merge_request_events" + "Event will be triggered when a merge request is created/updated/merged" + when "pipeline", "pipeline_events" + "Event will be triggered when a pipeline status changes" + when "wiki_page", "wiki_page_events" + "Event will be triggered when a wiki page is created/updated" + when "commit", "commit_events" + "Event will be triggered when a commit is created/updated" + end + end + def valid_recipients? activated? && !importing? end diff --git a/app/models/snippet.rb b/app/models/snippet.rb index a58c208279e..644120453cf 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -168,5 +168,9 @@ class Snippet < ActiveRecord::Base def search_code(query) fuzzy_search(query, [:content]) end + + def parent_class + ::Project + end end end diff --git a/app/models/todo.rb b/app/models/todo.rb index bb5965e20eb..8afacd188e0 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -32,8 +32,6 @@ class Todo < ActiveRecord::Base validates :target_id, presence: true, unless: :for_commit? validates :commit_id, presence: true, if: :for_commit? - default_scope { reorder(id: :desc) } - scope :pending, -> { with_state(:pending) } scope :done, -> { with_state(:done) } @@ -53,10 +51,14 @@ class Todo < ActiveRecord::Base # milestones, but still show something if the user has a URL with that # selected. def sort(method) - case method.to_s - when 'priority', 'label_priority' then order_by_labels_priority - else order_by(method) - end + sorted = + case method.to_s + when 'priority', 'label_priority' then order_by_labels_priority + else order_by(method) + end + + # Break ties with the ID column for pagination + sorted.order(id: :desc) end # Order by priority depending on which issue/merge request the Todo belongs to diff --git a/app/models/user.rb b/app/models/user.rb index 9547506d33d..b8c55205ab8 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -114,13 +114,15 @@ class User < ActiveRecord::Base has_many :project_authorizations has_many :authorized_projects, through: :project_authorizations, source: :project + has_many :user_interacted_projects + has_many :project_interactions, through: :user_interacted_projects, source: :project, class_name: 'Project' + has_many :snippets, dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent has_many :notes, dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent has_many :issues, dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent has_many :merge_requests, dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent has_many :events, dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent has_many :subscriptions, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent - has_many :recent_events, -> { order "id DESC" }, foreign_key: :author_id, class_name: "Event" has_many :oauth_applications, class_name: 'Doorkeeper::Application', as: :owner, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_one :abuse_report, dependent: :destroy, foreign_key: :user_id # rubocop:disable Cop/ActiveRecordDependent has_many :reported_abuse_reports, dependent: :destroy, foreign_key: :reporter_id, class_name: "AbuseReport" # rubocop:disable Cop/ActiveRecordDependent @@ -601,6 +603,15 @@ class User < ActiveRecord::Base authorized_projects(min_access_level).exists?({ id: project.id }) end + # Typically used in conjunction with projects table to get projects + # a user has been given access to. + # + # Example use: + # `Project.where('EXISTS(?)', user.authorizations_for_projects)` + def authorizations_for_projects + project_authorizations.select(1).where('project_authorizations.project_id = projects.id') + end + # Returns the projects this user has reporter (or greater) access to, limited # to at most the given projects. # @@ -1026,14 +1037,33 @@ class User < ActiveRecord::Base end end + def todos_done_count(force: false) + Rails.cache.fetch(['users', id, 'todos_done_count'], force: force, expires_in: 20.minutes) do + TodosFinder.new(self, state: :done).execute.count + end + end + + def todos_pending_count(force: false) + Rails.cache.fetch(['users', id, 'todos_pending_count'], force: force, expires_in: 20.minutes) do + TodosFinder.new(self, state: :pending).execute.count + end + end + def update_cache_counts assigned_open_merge_requests_count(force: true) assigned_open_issues_count(force: true) end + def update_todos_count_cache + todos_done_count(force: true) + todos_pending_count(force: true) + end + def invalidate_cache_counts invalidate_issue_cache_counts invalidate_merge_request_cache_counts + invalidate_todos_done_count + invalidate_todos_pending_count end def invalidate_issue_cache_counts @@ -1044,21 +1074,12 @@ class User < ActiveRecord::Base Rails.cache.delete(['users', id, 'assigned_open_merge_requests_count']) end - def todos_done_count(force: false) - Rails.cache.fetch(['users', id, 'todos_done_count'], force: force, expires_in: 20.minutes) do - TodosFinder.new(self, state: :done).execute.count - end + def invalidate_todos_done_count + Rails.cache.delete(['users', id, 'todos_done_count']) end - def todos_pending_count(force: false) - Rails.cache.fetch(['users', id, 'todos_pending_count'], force: force, expires_in: 20.minutes) do - TodosFinder.new(self, state: :pending).execute.count - end - end - - def update_todos_count_cache - todos_done_count(force: true) - todos_pending_count(force: true) + def invalidate_todos_pending_count + Rails.cache.delete(['users', id, 'todos_pending_count']) end # This is copied from Devise::Models::Lockable#valid_for_authentication?, as our auth diff --git a/app/models/user_interacted_project.rb b/app/models/user_interacted_project.rb new file mode 100644 index 00000000000..dd55a6acb79 --- /dev/null +++ b/app/models/user_interacted_project.rb @@ -0,0 +1,59 @@ +class UserInteractedProject < ActiveRecord::Base + belongs_to :user + belongs_to :project + + validates :project_id, presence: true + validates :user_id, presence: true + + CACHE_EXPIRY_TIME = 1.day + + # Schema version required for this model + REQUIRED_SCHEMA_VERSION = 20180223120443 + + class << self + def track(event) + # For events without a project, we simply don't care. + # An example of this is the creation of a snippet (which + # is not related to any project). + return unless event.project_id + + attributes = { + project_id: event.project_id, + user_id: event.author_id + } + + cached_exists?(attributes) do + transaction(requires_new: true) do + begin + where(attributes).select(1).first || create!(attributes) + true # not caching the whole record here for now + rescue ActiveRecord::RecordNotUnique + # Note, above queries are not atomic and prone + # to race conditions (similar like #find_or_create!). + # In the case where we hit this, the record we want + # already exists - shortcut and return. + true + end + end + end + end + + # Check if we can safely call .track (table exists) + def available? + @available_flag ||= ActiveRecord::Migrator.current_version >= REQUIRED_SCHEMA_VERSION # rubocop:disable Gitlab/PredicateMemoization + end + + # Flushes cached information about schema + def reset_column_information + @available_flag = nil + super + end + + private + + def cached_exists?(project_id:, user_id:, &block) + cache_key = "user_interacted_projects:#{project_id}:#{user_id}" + Rails.cache.fetch(cache_key, expires_in: CACHE_EXPIRY_TIME, &block) + end + end +end diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index f0bcba588a2..c9cb730c4e9 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -48,7 +48,12 @@ class GroupPolicy < BasePolicy rule { has_access }.enable :read_namespace rule { developer }.enable :admin_milestones - rule { reporter }.enable :admin_label + + rule { reporter }.policy do + enable :admin_label + enable :admin_list + enable :admin_issue + end rule { master }.policy do enable :create_projects diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 3b0550b4dd6..57ab0c23dcd 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -61,6 +61,11 @@ class ProjectPolicy < BasePolicy desc "Project has request access enabled" condition(:request_access_enabled, scope: :subject) { project.request_access_enabled } + desc "Has merge requests allowing pushes to user" + condition(:has_merge_requests_allowing_pushes, scope: :subject) do + project.merge_requests_allowing_push_to_user(user).any? + end + features = %w[ merge_requests issues @@ -291,6 +296,15 @@ class ProjectPolicy < BasePolicy prevent :read_issue end + # These rules are included to allow maintainers of projects to push to certain + # to run pipelines for the branches they have access to. + rule { can?(:public_access) & has_merge_requests_allowing_pushes }.policy do + enable :create_build + enable :update_build + enable :create_pipeline + enable :update_pipeline + end + private def team_member? diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb index 08ae49562c7..9f3f2637183 100644 --- a/app/presenters/merge_request_presenter.rb +++ b/app/presenters/merge_request_presenter.rb @@ -78,7 +78,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated end def rebase_path - if !rebase_in_progress? && should_be_rebased? && user_can_push_to_source_branch? + if !rebase_in_progress? && should_be_rebased? && can_push_to_source_branch? rebase_project_merge_request_path(project, merge_request) end end @@ -160,7 +160,11 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated end def can_push_to_source_branch? - source_branch_exists? && user_can_push_to_source_branch? + return false unless source_branch_exists? + + !!::Gitlab::UserAccess + .new(current_user, project: source_project) + .can_push_to_branch?(source_branch) end private @@ -191,17 +195,10 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated end.sort.to_sentence end - def user_can_push_to_source_branch? - return false unless source_branch_exists? - - ::Gitlab::UserAccess - .new(current_user, project: source_project) - .can_push_to_branch?(source_branch) - end - def user_can_collaborate_with_project? can?(current_user, :push_code, project) || - (current_user && current_user.already_forked?(project)) + (current_user && current_user.already_forked?(project)) || + can_push_to_source_branch? end def user_can_fork_project? diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb index 4e8ef320af2..4a812e39ee1 100644 --- a/app/serializers/merge_request_widget_entity.rb +++ b/app/serializers/merge_request_widget_entity.rb @@ -11,6 +11,7 @@ class MergeRequestWidgetEntity < IssuableEntity expose :source_project_id expose :target_branch expose :target_project_id + expose :allow_maintainer_to_push expose :should_be_rebased?, as: :should_be_rebased expose :ff_only_enabled do |merge_request| @@ -29,6 +30,7 @@ class MergeRequestWidgetEntity < IssuableEntity expose :can_push_to_source_branch do |merge_request| presenter(merge_request).can_push_to_source_branch? end + expose :rebase_path do |merge_request| presenter(merge_request).rebase_path end @@ -38,7 +40,7 @@ class MergeRequestWidgetEntity < IssuableEntity # Diff sha's expose :diff_head_sha do |merge_request| - merge_request.diff_head_sha if merge_request.diff_head_commit + merge_request.diff_head_sha.presence end expose :merge_commit_message @@ -136,8 +138,8 @@ class MergeRequestWidgetEntity < IssuableEntity end expose :new_blob_path do |merge_request| - if can?(current_user, :push_code, merge_request.project) - project_new_blob_path(merge_request.project, merge_request.source_branch) + if presenter(merge_request).can_push_to_source_branch? + project_new_blob_path(merge_request.source_project, merge_request.source_branch) end end diff --git a/app/services/badges/base_service.rb b/app/services/badges/base_service.rb new file mode 100644 index 00000000000..4f87426bd38 --- /dev/null +++ b/app/services/badges/base_service.rb @@ -0,0 +1,11 @@ +module Badges + class BaseService + protected + + attr_accessor :params + + def initialize(params = {}) + @params = params.dup + end + end +end diff --git a/app/services/badges/build_service.rb b/app/services/badges/build_service.rb new file mode 100644 index 00000000000..6267e571838 --- /dev/null +++ b/app/services/badges/build_service.rb @@ -0,0 +1,12 @@ +module Badges + class BuildService < Badges::BaseService + # returns the created badge + def execute(source) + if source.is_a?(Group) + GroupBadge.new(params.merge(group: source)) + else + ProjectBadge.new(params.merge(project: source)) + end + end + end +end diff --git a/app/services/badges/create_service.rb b/app/services/badges/create_service.rb new file mode 100644 index 00000000000..aafb87f7dcd --- /dev/null +++ b/app/services/badges/create_service.rb @@ -0,0 +1,10 @@ +module Badges + class CreateService < Badges::BaseService + # returns the created badge + def execute(source) + badge = Badges::BuildService.new(params).execute(source) + + badge.tap { |b| b.save } + end + end +end diff --git a/app/services/badges/update_service.rb b/app/services/badges/update_service.rb new file mode 100644 index 00000000000..7ca84b5df31 --- /dev/null +++ b/app/services/badges/update_service.rb @@ -0,0 +1,12 @@ +module Badges + class UpdateService < Badges::BaseService + # returns the updated badge + def execute(badge) + if params.present? + badge.update_attributes(params) + end + + badge + end + end +end diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb index 6078fe38064..ecd74b74f8a 100644 --- a/app/services/boards/issues/list_service.rb +++ b/app/services/boards/issues/list_service.rb @@ -40,7 +40,11 @@ module Boards end def set_parent - params[:project_id] = parent.id + if parent.is_a?(Group) + params[:group_id] = parent.id + else + params[:project_id] = parent.id + end end def set_state diff --git a/app/services/boards/issues/move_service.rb b/app/services/boards/issues/move_service.rb index 797d6df7c1a..15fed7d17c1 100644 --- a/app/services/boards/issues/move_service.rb +++ b/app/services/boards/issues/move_service.rb @@ -60,8 +60,10 @@ module Boards label_ids = if moving_to_list.movable? moving_from_list.label_id + elsif board.group_board? + ::Label.on_group_boards(parent.id).pluck(:label_id) else - Label.on_project_boards(parent.id).pluck(:label_id) + ::Label.on_project_boards(parent.id).pluck(:label_id) end Array(label_ids).compact diff --git a/app/services/boards/lists/create_service.rb b/app/services/boards/lists/create_service.rb index 183556a1d6b..bebc90c7a8d 100644 --- a/app/services/boards/lists/create_service.rb +++ b/app/services/boards/lists/create_service.rb @@ -12,7 +12,11 @@ module Boards private def available_labels_for(board) - LabelsFinder.new(current_user, project_id: parent.id).execute + if board.group_board? + parent.labels + else + LabelsFinder.new(current_user, project_id: parent.id).execute + end end def next_position(board) diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index c8b112132b3..3b3d9239086 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -81,7 +81,7 @@ module Ci end def related_merge_requests - MergeRequest.opened.where(source_project: pipeline.project, source_branch: pipeline.ref) + pipeline.project.source_of_merge_requests.opened.where(source_branch: pipeline.ref) end end end diff --git a/app/services/ci/create_trace_artifact_service.rb b/app/services/ci/create_trace_artifact_service.rb deleted file mode 100644 index ffde824972c..00000000000 --- a/app/services/ci/create_trace_artifact_service.rb +++ /dev/null @@ -1,36 +0,0 @@ -module Ci - class CreateTraceArtifactService < BaseService - def execute(job) - return if job.job_artifacts_trace - - job.trace.read do |stream| - break unless stream.file? - - clone_file!(stream.path, JobArtifactUploader.workhorse_upload_path) do |clone_path| - create_job_trace!(job, clone_path) - FileUtils.rm(stream.path) - end - end - end - - private - - def create_job_trace!(job, path) - File.open(path) do |stream| - job.create_job_artifacts_trace!( - project: job.project, - file_type: :trace, - file: stream) - end - end - - def clone_file!(src_path, temp_dir) - FileUtils.mkdir_p(temp_dir) - Dir.mktmpdir('tmp-trace', temp_dir) do |dir_path| - temp_path = File.join(dir_path, "job.log") - FileUtils.copy(src_path, temp_path) - yield(temp_path) - end - end - end -end diff --git a/app/services/ci/fetch_kubernetes_token_service.rb b/app/services/ci/fetch_kubernetes_token_service.rb index e73c6ad6780..bca883ec0a0 100644 --- a/app/services/ci/fetch_kubernetes_token_service.rb +++ b/app/services/ci/fetch_kubernetes_token_service.rb @@ -32,7 +32,7 @@ module Ci kubeclient = build_kubeclient! kubeclient.get_secrets.as_json - rescue KubeException => err + rescue Kubeclient::HttpError => err raise err unless err.error_code == 404 [] diff --git a/app/services/clusters/applications/check_installation_progress_service.rb b/app/services/clusters/applications/check_installation_progress_service.rb index bde090eaeec..90393e951a4 100644 --- a/app/services/clusters/applications/check_installation_progress_service.rb +++ b/app/services/clusters/applications/check_installation_progress_service.rb @@ -12,7 +12,7 @@ module Clusters else check_timeout end - rescue KubeException => ke + rescue Kubeclient::HttpError => ke app.make_errored!("Kubernetes error: #{ke.message}") unless app.errored? end diff --git a/app/services/clusters/applications/install_service.rb b/app/services/clusters/applications/install_service.rb index 8ceeec687cd..4c25a09814b 100644 --- a/app/services/clusters/applications/install_service.rb +++ b/app/services/clusters/applications/install_service.rb @@ -10,7 +10,7 @@ module Clusters ClusterWaitForAppInstallationWorker.perform_in( ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id) - rescue KubeException => ke + rescue Kubeclient::HttpError => ke app.make_errored!("Kubernetes error: #{ke.message}") rescue StandardError app.make_errored!("Can't start installation process") diff --git a/app/services/compare_service.rb b/app/services/compare_service.rb index 1db91c3c90c..2a69a205629 100644 --- a/app/services/compare_service.rb +++ b/app/services/compare_service.rb @@ -10,9 +10,14 @@ class CompareService @start_ref_name = new_start_ref_name end - def execute(target_project, target_ref, straight: false) + def execute(target_project, target_ref, base_sha: nil, straight: false) raw_compare = target_project.repository.compare_source_branch(target_ref, start_project.repository, start_ref_name, straight: straight) - Compare.new(raw_compare, target_project, straight: straight) if raw_compare + return unless raw_compare + + Compare.new(raw_compare, + target_project, + base_sha: base_sha, + straight: straight) end end diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb index 46acdc5406c..a954564946b 100644 --- a/app/services/files/create_service.rb +++ b/app/services/files/create_service.rb @@ -1,11 +1,11 @@ module Files class CreateService < Files::BaseService def create_commit! - handler = Lfs::FileModificationHandler.new(project, @branch_name) + transformer = Lfs::FileTransformer.new(project, @branch_name) - handler.new_file(@file_path, @file_content) do |content_or_lfs_pointer| - create_transformed_commit(content_or_lfs_pointer) - end + result = transformer.new_file(@file_path, @file_content) + + create_transformed_commit(result.content) end private diff --git a/app/services/files/multi_service.rb b/app/services/files/multi_service.rb index a03c59f569d..13a1dee4173 100644 --- a/app/services/files/multi_service.rb +++ b/app/services/files/multi_service.rb @@ -3,11 +3,33 @@ module Files UPDATE_FILE_ACTIONS = %w(update move delete).freeze def create_commit! + transformer = Lfs::FileTransformer.new(project, @branch_name) + + actions = actions_after_lfs_transformation(transformer, params[:actions]) + + commit_actions!(actions) + end + + private + + def actions_after_lfs_transformation(transformer, actions) + actions.map do |action| + if action[:action] == 'create' + result = transformer.new_file(action[:file_path], action[:content], encoding: action[:encoding]) + action[:content] = result.content + action[:encoding] = result.encoding + end + + action + end + end + + def commit_actions!(actions) repository.multi_action( current_user, message: @commit_message, branch_name: @branch_name, - actions: params[:actions], + actions: actions, author_email: @author_email, author_name: @author_name, start_project: @start_project, @@ -17,8 +39,6 @@ module Files raise_error(e) end - private - def validate! super diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index e87fd49d193..02fb48108fb 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -109,6 +109,10 @@ class IssuableBaseService < BaseService @available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute end + def handle_quick_actions_on_create(issuable) + merge_quick_actions_into_params!(issuable) + end + def merge_quick_actions_into_params!(issuable) original_description = params.fetch(:description, issuable.description) @@ -131,7 +135,7 @@ class IssuableBaseService < BaseService end def create(issuable) - merge_quick_actions_into_params!(issuable) + handle_quick_actions_on_create(issuable) filter_params(issuable) params.delete(:state_event) diff --git a/app/services/lfs/file_modification_handler.rb b/app/services/lfs/file_modification_handler.rb deleted file mode 100644 index fe9091a6e5d..00000000000 --- a/app/services/lfs/file_modification_handler.rb +++ /dev/null @@ -1,42 +0,0 @@ -module Lfs - class FileModificationHandler - attr_reader :project, :branch_name - - delegate :repository, to: :project - - def initialize(project, branch_name) - @project = project - @branch_name = branch_name - end - - def new_file(file_path, file_content) - if project.lfs_enabled? && lfs_file?(file_path) - lfs_pointer_file = Gitlab::Git::LfsPointerFile.new(file_content) - lfs_object = create_lfs_object!(lfs_pointer_file, file_content) - content = lfs_pointer_file.pointer - - success = yield(content) - - link_lfs_object!(lfs_object) if success - else - yield(file_content) - end - end - - private - - def lfs_file?(file_path) - repository.attributes_at(branch_name, file_path)['filter'] == 'lfs' - end - - def create_lfs_object!(lfs_pointer_file, file_content) - LfsObject.find_or_create_by(oid: lfs_pointer_file.sha256, size: lfs_pointer_file.size) do |lfs_object| - lfs_object.file = CarrierWaveStringFile.new(file_content) - end - end - - def link_lfs_object!(lfs_object) - project.lfs_objects << lfs_object - end - end -end diff --git a/app/services/lfs/file_transformer.rb b/app/services/lfs/file_transformer.rb new file mode 100644 index 00000000000..69281ee3137 --- /dev/null +++ b/app/services/lfs/file_transformer.rb @@ -0,0 +1,66 @@ +module Lfs + # Usage: Calling `new_file` check to see if a file should be in LFS and + # return a transformed result with `content` and `encoding` to commit. + # + # For LFS an LfsObject linked to the project is stored and an LFS + # pointer returned. If the file isn't in LFS the untransformed content + # is returned to save in the commit. + # + # transformer = Lfs::FileTransformer.new(project, @branch_name) + # content_or_lfs_pointer = transformer.new_file(file_path, content).content + # create_transformed_commit(content_or_lfs_pointer) + # + class FileTransformer + attr_reader :project, :branch_name + + delegate :repository, to: :project + + def initialize(project, branch_name) + @project = project + @branch_name = branch_name + end + + def new_file(file_path, file_content, encoding: nil) + if project.lfs_enabled? && lfs_file?(file_path) + file_content = Base64.decode64(file_content) if encoding == 'base64' + lfs_pointer_file = Gitlab::Git::LfsPointerFile.new(file_content) + lfs_object = create_lfs_object!(lfs_pointer_file, file_content) + + link_lfs_object!(lfs_object) + + Result.new(content: lfs_pointer_file.pointer, encoding: 'text') + else + Result.new(content: file_content, encoding: encoding) + end + end + + class Result + attr_reader :content, :encoding + + def initialize(content:, encoding:) + @content = content + @encoding = encoding + end + end + + private + + def lfs_file?(file_path) + cached_attributes.attributes(file_path)['filter'] == 'lfs' + end + + def cached_attributes + @cached_attributes ||= Gitlab::Git::AttributesAtRefParser.new(repository, branch_name) + end + + def create_lfs_object!(lfs_pointer_file, file_content) + LfsObject.find_or_create_by(oid: lfs_pointer_file.sha256, size: lfs_pointer_file.size) do |lfs_object| + lfs_object.file = CarrierWaveStringFile.new(file_content) + end + end + + def link_lfs_object!(lfs_object) + project.lfs_objects << lfs_object + end + end +end diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb index b141bfd5fbc..5b51e1982f1 100644 --- a/app/services/members/destroy_service.rb +++ b/app/services/members/destroy_service.rb @@ -5,12 +5,9 @@ module Members return member if member.is_a?(GroupMember) && member.source.last_owner?(member.user) - Member.transaction do - unassign_issues_and_merge_requests(member) unless member.invite? - member.notification_setting&.destroy + member.destroy - member.destroy - end + member.user&.invalidate_cache_counts if member.request? && member.user != current_user notification_service.decline_access_request(member) @@ -37,38 +34,5 @@ module Members raise "Unknown member type: #{member}!" end end - - def unassign_issues_and_merge_requests(member) - if member.is_a?(GroupMember) - issues = Issue.unscoped.select(1) - .joins(:project) - .where('issues.id = issue_assignees.issue_id AND projects.namespace_id = ?', member.source_id) - - # DELETE FROM issue_assignees WHERE user_id = X AND EXISTS (...) - IssueAssignee.unscoped - .where('user_id = :user_id AND EXISTS (:sub)', user_id: member.user_id, sub: issues) - .delete_all - - MergeRequestsFinder.new(current_user, group_id: member.source_id, assignee_id: member.user_id) - .execute - .update_all(assignee_id: nil) - else - project = member.source - - # SELECT 1 FROM issues WHERE issues.id = issue_assignees.issue_id AND issues.project_id = X - issues = Issue.unscoped.select(1) - .where('issues.id = issue_assignees.issue_id') - .where(project_id: project.id) - - # DELETE FROM issue_assignees WHERE user_id = X AND EXISTS (...) - IssueAssignee.unscoped - .where('user_id = :user_id AND EXISTS (:sub)', user_id: member.user_id, sub: issues) - .delete_all - - project.merge_requests.opened.assigned_to(member.user).update_all(assignee_id: nil) - end - - member.user.invalidate_cache_counts - end end end diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index 20a2b50d3de..231ab76fde4 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -24,6 +24,25 @@ module MergeRequests private + def handle_wip_event(merge_request) + if wip_event = params.delete(:wip_event) + # We update the title that is provided in the params or we use the mr title + title = params[:title] || merge_request.title + params[:title] = case wip_event + when 'wip' then MergeRequest.wip_title(title) + when 'unwip' then MergeRequest.wipless_title(title) + end + end + end + + def filter_params(merge_request) + super + + unless merge_request.can_allow_maintainer_to_push?(current_user) + params.delete(:allow_maintainer_to_push) + end + end + def merge_request_metrics_service(merge_request) MergeRequestMetricsService.new(merge_request.metrics) end diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index 4b186d93772..a98bbdf74dd 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -6,6 +6,7 @@ module MergeRequests @params_issue_iid = params.delete(:issue_iid) self.merge_request = MergeRequest.new(params) + merge_request.author = current_user merge_request.compare_commits = [] merge_request.source_project = find_source_project merge_request.target_project = find_target_project diff --git a/app/services/merge_requests/conflicts/list_service.rb b/app/services/merge_requests/conflicts/list_service.rb index ca9a33678e4..72cbc49adb2 100644 --- a/app/services/merge_requests/conflicts/list_service.rb +++ b/app/services/merge_requests/conflicts/list_service.rb @@ -17,15 +17,7 @@ module MergeRequests return @conflicts_can_be_resolved_in_ui = false unless merge_request.has_complete_diff_refs? return @conflicts_can_be_resolved_in_ui = false if merge_request.branch_missing? - begin - # Try to parse each conflict. If the MR's mergeable status hasn't been - # updated, ensure that we don't say there are conflicts to resolve - # when there are no conflict files. - conflicts.files.each(&:lines) - @conflicts_can_be_resolved_in_ui = conflicts.files.length > 0 - rescue Gitlab::Git::CommandError, Gitlab::Git::Conflict::Parser::UnresolvableError, Gitlab::Git::Conflict::Resolver::ConflictSideMissing - @conflicts_can_be_resolved_in_ui = false - end + @conflicts_can_be_resolved_in_ui = conflicts.can_be_resolved_in_ui? end def conflicts diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb index a18b1c90765..c57a2445341 100644 --- a/app/services/merge_requests/create_service.rb +++ b/app/services/merge_requests/create_service.rb @@ -34,6 +34,12 @@ module MergeRequests super end + # Override from IssuableBaseService + def handle_quick_actions_on_create(merge_request) + super + handle_wip_event(merge_request) + end + private def update_merge_requests_head_pipeline(merge_request) diff --git a/app/services/merge_requests/merge_request_diff_cache_service.rb b/app/services/merge_requests/merge_request_diff_cache_service.rb index 2945a7fd4e4..10aa9ae609c 100644 --- a/app/services/merge_requests/merge_request_diff_cache_service.rb +++ b/app/services/merge_requests/merge_request_diff_cache_service.rb @@ -1,8 +1,17 @@ module MergeRequests class MergeRequestDiffCacheService - def execute(merge_request) + def execute(merge_request, new_diff) # Executing the iteration we cache all the highlighted diff information merge_request.diffs.diff_files.to_a + + # Remove cache for all diffs on this MR. Do not use the association on the + # model, as that will interfere with other actions happening when + # reloading the diff. + MergeRequestDiff.where(merge_request: merge_request).each do |merge_request_diff| + next if merge_request_diff == new_diff + + merge_request_diff.diffs.clear_cache! + end end end end diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index c153872c874..8a40ad88182 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -98,17 +98,6 @@ module MergeRequests private - def handle_wip_event(merge_request) - if wip_event = params.delete(:wip_event) - # We update the title that is provided in the params or we use the mr title - title = params[:title] || merge_request.title - params[:title] = case wip_event - when 'wip' then MergeRequest.wip_title(title) - when 'unwip' then MergeRequest.wipless_title(title) - end - end - end - def create_branch_change_note(issuable, branch_type, old_branch, new_branch) SystemNoteService.change_branch( issuable, issuable.project, current_user, branch_type, diff --git a/app/services/notes/build_service.rb b/app/services/notes/build_service.rb index abf25bb778b..77e7b8a5ea7 100644 --- a/app/services/notes/build_service.rb +++ b/app/services/notes/build_service.rb @@ -26,14 +26,19 @@ module Notes if project project.notes.find_discussion(discussion_id) else - # only PersonalSnippets can have discussions without project association discussion = Note.find_discussion(discussion_id) noteable = discussion.noteable - return nil unless noteable.is_a?(PersonalSnippet) && can?(current_user, :comment_personal_snippet, noteable) + return nil unless noteable_without_project?(noteable) discussion end end + + def noteable_without_project?(noteable) + return true if noteable.is_a?(PersonalSnippet) && can?(current_user, :comment_personal_snippet, noteable) + + false + end end end diff --git a/app/services/notes/post_process_service.rb b/app/services/notes/post_process_service.rb index 6a10e172483..ad3dcc5010b 100644 --- a/app/services/notes/post_process_service.rb +++ b/app/services/notes/post_process_service.rb @@ -11,7 +11,7 @@ module Notes unless @note.system? EventCreateService.new.leave_note(@note, @note.author) - return if @note.for_personal_snippet? + return unless @note.for_project_noteable? @note.create_cross_references! execute_note_hooks diff --git a/app/services/notes/quick_actions_service.rb b/app/services/notes/quick_actions_service.rb index a8d0cc15527..0a33d5f3f3d 100644 --- a/app/services/notes/quick_actions_service.rb +++ b/app/services/notes/quick_actions_service.rb @@ -9,14 +9,12 @@ module Notes UPDATE_SERVICES[note.noteable_type] end - def self.supported?(note, current_user) - noteable_update_service(note) && - current_user && - current_user.can?(:"update_#{note.to_ability_name}", note.noteable) + def self.supported?(note) + !!noteable_update_service(note) end def supported?(note) - self.class.supported?(note, current_user) + self.class.supported?(note) end def extract_commands(note, options = {}) diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index 6835b14648b..e4be953e810 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -280,7 +280,7 @@ module NotificationRecipientService add_participants(note.author) add_mentions(note.author, target: note) - unless note.for_personal_snippet? + if note.for_project_noteable? # Merge project watchers add_project_watchers diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index e07ecda27b5..d7d2cde1004 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -208,9 +208,9 @@ class NotificationService def new_access_request(member) return true unless member.notifiable?(:subscription) - recipients = member.source.members.owners_and_masters + recipients = member.source.members.active_without_invites_and_requests.owners_and_masters if fallback_to_group_owners_masters?(recipients, member) - recipients = member.source.group.members.owners_and_masters + recipients = member.source.group.members.active_without_invites_and_requests.owners_and_masters end recipients.each { |recipient| deliver_access_request_email(recipient, member) } diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 01838ec6b5d..7fa1387084c 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -85,7 +85,7 @@ module Projects end def after_create_actions - log_info("#{@project.owner.name} created a new project \"#{@project.name_with_namespace}\"") + log_info("#{@project.owner.name} created a new project \"#{@project.full_name}\"") unless @project.gitlab_project_import? @project.write_repository_config diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index 81972df9b3c..4b8f955ae69 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -88,7 +88,11 @@ module Projects def attempt_rollback(project, message) return unless project - project.update_attributes(delete_error: message, pending_delete: false) + # It's possible that the project was destroyed, but some after_commit + # hook failed and caused us to end up here. A destroyed model will be a frozen hash, + # which cannot be altered. + project.update_attributes(delete_error: message, pending_delete: false) unless project.destroyed? + log_error("Deletion failed on #{project.full_path} with the following message: #{message}") end diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb index fe4e8ea10bf..d16aa3de639 100644 --- a/app/services/projects/import_export/export_service.rb +++ b/app/services/projects/import_export/export_service.rb @@ -2,7 +2,7 @@ module Projects module ImportExport class ExportService < BaseService def execute(_options = {}) - @shared = Gitlab::ImportExport::Shared.new(relative_path: File.join(project.disk_path, 'work')) + @shared = project.import_export_shared save_all end @@ -26,7 +26,7 @@ module Projects end def project_tree_saver - Gitlab::ImportExport::ProjectTreeSaver.new(project: project, current_user: @current_user, shared: @shared) + Gitlab::ImportExport::ProjectTreeSaver.new(project: project, current_user: @current_user, shared: @shared, params: @params) end def uploads_saver diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb index c760bd3b626..00fdd047208 100644 --- a/app/services/projects/update_pages_service.rb +++ b/app/services/projects/update_pages_service.rb @@ -1,5 +1,8 @@ module Projects class UpdatePagesService < BaseService + InvaildStateError = Class.new(StandardError) + FailedToExtractError = Class.new(StandardError) + BLOCK_SIZE = 32.kilobytes MAX_SIZE = 1.terabyte SITE_PATH = 'public/'.freeze @@ -11,13 +14,15 @@ module Projects end def execute + register_attempt + # Create status notifying the deployment of pages @status = create_status @status.enqueue! @status.run! - raise 'missing pages artifacts' unless build.artifacts? - raise 'pages are outdated' unless latest? + raise InvaildStateError, 'missing pages artifacts' unless build.artifacts? + raise InvaildStateError, 'pages are outdated' unless latest? # Create temporary directory in which we will extract the artifacts FileUtils.mkdir_p(tmp_path) @@ -26,24 +31,22 @@ module Projects # Check if we did extract public directory archive_public_path = File.join(archive_path, 'public') - raise 'pages miss the public folder' unless Dir.exist?(archive_public_path) - raise 'pages are outdated' unless latest? + raise FailedToExtractError, 'pages miss the public folder' unless Dir.exist?(archive_public_path) + raise InvaildStateError, 'pages are outdated' unless latest? deploy_page!(archive_public_path) success end - rescue => e + rescue InvaildStateError, FailedToExtractError => e register_failure error(e.message) - ensure - register_attempt - build.erase_artifacts! unless build.has_expiring_artifacts? end private def success @status.success + delete_artifact! super end @@ -52,6 +55,7 @@ module Projects @status.allow_failure = !latest? @status.description = message @status.drop(:script_failure) + delete_artifact! super end @@ -72,7 +76,7 @@ module Projects elsif artifacts.ends_with?('.zip') extract_zip_archive!(temp_path) else - raise 'unsupported artifacts format' + raise FailedToExtractError, 'unsupported artifacts format' end end @@ -81,17 +85,17 @@ module Projects %W(dd bs=#{BLOCK_SIZE} count=#{blocks}), %W(tar -x -C #{temp_path} #{SITE_PATH}), err: '/dev/null') - raise 'pages failed to extract' unless results.compact.all?(&:success?) + raise FailedToExtractError, 'pages failed to extract' unless results.compact.all?(&:success?) end def extract_zip_archive!(temp_path) - raise 'missing artifacts metadata' unless build.artifacts_metadata? + raise FailedToExtractError, 'missing artifacts metadata' unless build.artifacts_metadata? # Calculate page size after extract public_entry = build.artifacts_metadata_entry(SITE_PATH, recursive: true) if public_entry.total_size > max_size - raise "artifacts for pages are too large: #{public_entry.total_size}" + raise FailedToExtractError, "artifacts for pages are too large: #{public_entry.total_size}" end # Requires UnZip at least 6.00 Info-ZIP. @@ -100,7 +104,7 @@ module Projects # We add * to end of SITE_PATH, because we want to extract SITE_PATH and all subdirectories site_path = File.join(SITE_PATH, '*') unless system(*%W(unzip -qq -n #{artifacts} #{site_path} -d #{temp_path})) - raise 'pages failed to extract' + raise FailedToExtractError, 'pages failed to extract' end end @@ -163,6 +167,11 @@ module Projects build.artifacts_file.path end + def delete_artifact! + build.reload # Reload stable object to prevent erase artifacts with old state + build.erase_artifacts! unless build.has_expiring_artifacts? + end + def latest_sha project.commit(build.ref).try(:sha).to_s end diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb index 379a8068023..5f2615a2c01 100644 --- a/app/services/projects/update_service.rb +++ b/app/services/projects/update_service.rb @@ -58,7 +58,7 @@ module Projects def enabling_wiki? return false if @project.wiki_enabled? - params[:project_feature_attributes][:wiki_access_level].to_i > ProjectFeature::DISABLED + params.dig(:project_feature_attributes, :wiki_access_level).to_i > ProjectFeature::DISABLED end def ensure_wiki_exists diff --git a/app/services/prometheus/adapter_service.rb b/app/services/prometheus/adapter_service.rb new file mode 100644 index 00000000000..4504d2ccfe6 --- /dev/null +++ b/app/services/prometheus/adapter_service.rb @@ -0,0 +1,36 @@ +module Prometheus + class AdapterService + def initialize(project, deployment_platform = nil) + @project = project + + @deployment_platform = if deployment_platform + deployment_platform + else + project.deployment_platform + end + end + + attr_reader :deployment_platform, :project + + def prometheus_adapter + @prometheus_adapter ||= if service_prometheus_adapter.can_query? + service_prometheus_adapter + else + cluster_prometheus_adapter + end + end + + def service_prometheus_adapter + project.find_or_initialize_service('prometheus') + end + + def cluster_prometheus_adapter + return unless deployment_platform.respond_to?(:cluster) + + cluster = deployment_platform.cluster + return unless cluster.application_prometheus&.installed? + + cluster.application_prometheus + end + end +end diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb index 1e9bd84e749..cba49faac31 100644 --- a/app/services/quick_actions/interpret_service.rb +++ b/app/services/quick_actions/interpret_service.rb @@ -347,9 +347,9 @@ module QuickActions "#{verb} this #{noun} as Work In Progress." end condition do - issuable.persisted? && - issuable.respond_to?(:work_in_progress?) && - current_user.can?(:"update_#{issuable.to_ability_name}", issuable) + issuable.respond_to?(:work_in_progress?) && + # Allow it to mark as WIP on MR creation page _or_ through MR notes. + (issuable.new_record? || current_user.can?(:"update_#{issuable.to_ability_name}", issuable)) end command :wip do @updates[:wip_event] = issuable.work_in_progress? ? 'unwip' : 'wip' diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb index af8c02a10b7..ba7946fd23c 100644 --- a/app/services/system_hooks_service.rb +++ b/app/services/system_hooks_service.rb @@ -20,8 +20,8 @@ class SystemHooksService def build_event_data(model, event) data = { event_name: build_event_name(model, event), - created_at: model.created_at.xmlschema, - updated_at: model.updated_at.xmlschema + created_at: model.created_at&.xmlschema, + updated_at: model.updated_at&.xmlschema } case model diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index c2ca404b179..ffd48e842c2 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -241,8 +241,7 @@ class TodoService end def handle_note(note, author, skip_users = []) - # Skip system notes, and notes on project snippet - return if note.system? || note.for_snippet? + return unless note.can_create_todo? project = note.project target = note.noteable diff --git a/app/services/users/destroy_service.rb b/app/services/users/destroy_service.rb index b71002433d6..06b604dad4d 100644 --- a/app/services/users/destroy_service.rb +++ b/app/services/users/destroy_service.rb @@ -49,6 +49,8 @@ module Users ::Projects::DestroyService.new(project, current_user, skip_repo: project.legacy_storage?).execute end + yield(user) if block_given? + MigrateToGhostUserService.new(user).execute unless options[:hard_delete] # Destroy the namespace after destroying the user since certain methods may depend on the namespace existing diff --git a/app/validators/url_placeholder_validator.rb b/app/validators/url_placeholder_validator.rb new file mode 100644 index 00000000000..dd681218b6b --- /dev/null +++ b/app/validators/url_placeholder_validator.rb @@ -0,0 +1,32 @@ +# UrlValidator +# +# Custom validator for URLs. +# +# By default, only URLs for the HTTP(S) protocols will be considered valid. +# Provide a `:protocols` option to configure accepted protocols. +# +# Also, this validator can help you validate urls with placeholders inside. +# Usually, if you have a url like 'http://www.example.com/%{project_path}' the +# URI parser will reject that URL format. Provide a `:placeholder_regex` option +# to configure accepted placeholders. +# +# Example: +# +# class User < ActiveRecord::Base +# validates :personal_url, url: true +# +# validates :ftp_url, url: { protocols: %w(ftp) } +# +# validates :git_url, url: { protocols: %w(http https ssh git) } +# +# validates :placeholder_url, url: { placeholder_regex: /(project_path|project_id|default_branch)/ } +# end +# +class UrlPlaceholderValidator < UrlValidator + def validate_each(record, attribute, value) + placeholder_regex = self.options[:placeholder_regex] + value = value.gsub(/%{#{placeholder_regex}}/, 'foo') if placeholder_regex && value + + super(record, attribute, value) + end +end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index b89b7a9ff85..81d7db04a3c 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -657,24 +657,26 @@ .checkbox = f.label :version_check_enabled do = f.check_box :version_check_enabled - Version check enabled + Enable version check .help-block - Let GitLab inform you when an update is available. + GitLab will inform you if a new version is available. + = link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "version-check") + about what information is shared with GitLab Inc. .form-group .col-sm-offset-2.col-sm-10 - can_be_configured = @application_setting.usage_ping_can_be_configured? .checkbox = f.label :usage_ping_enabled do = f.check_box :usage_ping_enabled, disabled: !can_be_configured - Usage ping enabled - = link_to icon('question-circle'), help_page_path("user/admin_area/settings/usage_statistics", anchor: "usage-ping") + Enable usage ping .help-block - if can_be_configured - Every week GitLab will report license usage back to GitLab, Inc. - Disable this option if you do not want this to occur. To see the - JSON payload that will be sent, visit the - = succeed '.' do - = link_to "Cohorts page", admin_cohorts_path(anchor: 'usage-ping') + To help improve GitLab and its user experience, GitLab will + periodically collect usage information. + = link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "usage-ping") + about what information is shared with GitLab Inc. Visit + = link_to 'Cohorts', admin_cohorts_path(anchor: 'usage-ping') + to see the JSON payload sent. - else The usage ping is disabled, and cannot be configured through this form. For more information, see the documentation on diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index e3711421b61..05c41082882 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -164,7 +164,7 @@ %h4 Latest projects - @projects.each do |project| %p - = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project], class: 'str-truncated-60' + = link_to project.full_name, [:admin, project.namespace.becomes(Namespace), project], class: 'str-truncated-60' %span.light.pull-right #{time_ago_with_tooltip(project.created_at)} .col-md-4 diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index 2545cecc721..324f3c0a22f 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -68,7 +68,7 @@ - @projects.each do |project| %li %strong - = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project] + = link_to project.full_name, [:admin, project.namespace.becomes(Namespace), project] %span.badge = storage_counter(project.statistics.storage_size) %span.pull-right.light @@ -86,7 +86,7 @@ - @group.shared_projects.sort_by(&:name).each do |project| %li %strong - = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project] + = link_to project.full_name, [:admin, project.namespace.becomes(Namespace), project] %span.badge = storage_counter(project.statistics.storage_size) %span.pull-right.light diff --git a/app/views/admin/hooks/_form.html.haml b/app/views/admin/hooks/_form.html.haml index d8f96ed5b0d..a6324a97fd5 100644 --- a/app/views/admin/hooks/_form.html.haml +++ b/app/views/admin/hooks/_form.html.haml @@ -1,21 +1,20 @@ = form_errors(hook) .form-group - = form.label :url, 'URL', class: 'control-label' - .col-sm-10 - = form.text_field :url, class: 'form-control' + = form.label :url, 'URL', class: 'label-light' + = form.text_field :url, class: 'form-control' .form-group - = form.label :token, 'Secret Token', class: 'control-label' - .col-sm-10 - = form.text_field :token, class: 'form-control' - %p.help-block - Use this token to validate received payloads + = form.label :token, 'Secret Token', class: 'label-light' + = form.text_field :token, class: 'form-control' + %p.help-block + Use this token to validate received payloads .form-group - = form.label :url, 'Trigger', class: 'control-label' - .col-sm-10.prepend-top-10 - %div - System hook will be triggered on set of events like creating project - or adding ssh key. But you can also enable extra triggers like Push events. + = form.label :url, 'Trigger', class: 'label-light' + %ul.list-unstyled + %li + .help-block + System hook will be triggered on set of events like creating project + or adding ssh key. But you can also enable extra triggers like Push events. .prepend-top-default = form.check_box :repository_update_events, class: 'pull-left' @@ -24,21 +23,21 @@ %strong Repository update events %p.light This URL will be triggered when repository is updated - %div + %li = form.check_box :push_events, class: 'pull-left' .prepend-left-20 = form.label :push_events, class: 'list-label' do %strong Push events %p.light This URL will be triggered for each branch updated to the repository - %div + %li = form.check_box :tag_push_events, class: 'pull-left' .prepend-left-20 = form.label :tag_push_events, class: 'list-label' do %strong Tag push events %p.light This URL will be triggered when a new tag is pushed to the repository - %div + %li = form.check_box :merge_requests_events, class: 'pull-left' .prepend-left-20 = form.label :merge_requests_events, class: 'list-label' do @@ -46,9 +45,8 @@ %p.light This URL will be triggered when a merge request is created/updated/merged .form-group - = form.label :enable_ssl_verification, 'SSL verification', class: 'control-label checkbox' - .col-sm-10 - .checkbox - = form.label :enable_ssl_verification do - = form.check_box :enable_ssl_verification - %strong Enable SSL verification + = form.label :enable_ssl_verification, 'SSL verification', class: 'label-light checkbox' + .checkbox + = form.label :enable_ssl_verification do + = form.check_box :enable_ssl_verification + %strong Enable SSL verification diff --git a/app/views/admin/hooks/index.html.haml b/app/views/admin/hooks/index.html.haml index bc02d9969d6..d9e2ce5e74c 100644 --- a/app/views/admin/hooks/index.html.haml +++ b/app/views/admin/hooks/index.html.haml @@ -1,33 +1,35 @@ - page_title 'System Hooks' -%h3.page-title - System hooks +.row.prepend-top-default + .col-lg-4 + %h4.prepend-top-0 + = page_title + %p + #{link_to 'System hooks ', help_page_path('system_hooks/system_hooks'), class: 'vlink'} can be + used for binding events when GitLab creates a User or Project. -%p.light - #{link_to 'System hooks ', help_page_path('system_hooks/system_hooks'), class: 'vlink'} can be - used for binding events when GitLab creates a User or Project. + .col-lg-8.append-bottom-default + = form_for @hook, as: :hook, url: admin_hooks_path do |f| + = render partial: 'form', locals: { form: f, hook: @hook } + = f.submit 'Add system hook', class: 'btn btn-create' -%hr + %hr -= form_for @hook, as: :hook, url: admin_hooks_path, html: { class: 'form-horizontal' } do |f| - = render partial: 'form', locals: { form: f, hook: @hook } - .form-actions - = f.submit 'Add system hook', class: 'btn btn-create' -%hr + - if @hooks.any? + .panel.panel-default + .panel-heading + System hooks (#{@hooks.count}) + %ul.content-list + - @hooks.each do |hook| + %li + .controls + = render 'shared/web_hooks/test_button', triggers: SystemHook.triggers, hook: hook, button_class: 'btn-sm' + = link_to 'Edit', edit_admin_hook_path(hook), class: 'btn btn-sm' + = link_to 'Remove', admin_hook_path(hook), data: { confirm: 'Are you sure?' }, method: :delete, class: 'btn btn-remove btn-sm' + .monospace= hook.url + %div + - SystemHook.triggers.each_value do |event| + - if hook.public_send(event) + %span.label.label-gray= event.to_s.titleize + %span.label.label-gray SSL Verification: #{hook.enable_ssl_verification ? 'enabled' : 'disabled'} -- if @hooks.any? - .panel.panel-default - .panel-heading - System hooks (#{@hooks.count}) - %ul.content-list - - @hooks.each do |hook| - %li - .controls - = render 'shared/web_hooks/test_button', triggers: SystemHook.triggers, hook: hook, button_class: 'btn-sm' - = link_to 'Edit', edit_admin_hook_path(hook), class: 'btn btn-sm' - = link_to 'Remove', admin_hook_path(hook), data: { confirm: 'Are you sure?' }, method: :delete, class: 'btn btn-remove btn-sm' - .monospace= hook.url - %div - - SystemHook.triggers.each_value do |event| - - if hook.public_send(event) - %span.label.label-gray= event.to_s.titleize - %span.label.label-gray SSL Verification: #{hook.enable_ssl_verification ? 'enabled' : 'disabled'} += render 'shared/plugins/index' diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index 42f92079d85..c02ddafe108 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -1,8 +1,8 @@ - add_to_breadcrumbs "Projects", admin_projects_path -- breadcrumb_title @project.name_with_namespace -- page_title @project.name_with_namespace, "Projects" +- breadcrumb_title @project.full_name +- page_title @project.full_name, "Projects" %h3.page-title - Project: #{@project.name_with_namespace} + Project: #{@project.full_name} = link_to edit_project_path(@project), class: "btn btn-nr pull-right" do %i.fa.fa-pencil-square-o Edit diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml index 6d8fad0eb8d..37269862de6 100644 --- a/app/views/admin/runners/show.html.haml +++ b/app/views/admin/runners/show.html.haml @@ -9,6 +9,10 @@ %span.runner-state.runner-state-specific Specific +- add_to_breadcrumbs _("Runners"), admin_runners_path +- breadcrumb_title "##{@runner.id}" +- @no_container = true + - if @runner.shared? .bs-callout.bs-callout-success %h4 This Runner will process jobs from ALL UNASSIGNED projects @@ -39,7 +43,7 @@ %tr.alert-info %td %strong - = project.name_with_namespace + = project.full_name %td .pull-right = link_to 'Disable', [:admin, project.namespace.becomes(Namespace), project, runner_project], method: :delete, class: 'btn btn-danger btn-xs' @@ -61,7 +65,7 @@ - @projects.each do |project| %tr %td - = project.name_with_namespace + = project.full_name %td .pull-right = form_for [:admin, project.namespace.becomes(Namespace), project, project.runner_projects.new] do |f| @@ -95,7 +99,7 @@ %td.status - if project - = project.name_with_namespace + = project.full_name %td.build-link - if project diff --git a/app/views/admin/users/projects.html.haml b/app/views/admin/users/projects.html.haml index 4a440f3f6d4..96835ee9af5 100644 --- a/app/views/admin/users/projects.html.haml +++ b/app/views/admin/users/projects.html.haml @@ -29,12 +29,12 @@ .panel.panel-default .panel-heading Joined projects (#{@joined_projects.count}) %ul.well-list - - @joined_projects.sort_by(&:name_with_namespace).each do |project| + - @joined_projects.sort_by(&:full_name).each do |project| - member = project.team.find_member(@user.id) %li.project_member .list-item-name = link_to admin_project_path(project), class: dom_class(project) do - = project.name_with_namespace + = project.full_name - if member .pull-right diff --git a/app/views/discussions/_diff_with_notes.html.haml b/app/views/discussions/_diff_with_notes.html.haml index f9bfc01f213..8680ec2e298 100644 --- a/app/views/discussions/_diff_with_notes.html.haml +++ b/app/views/discussions/_diff_with_notes.html.haml @@ -2,8 +2,12 @@ - blob = discussion.blob - discussions = { discussion.original_line_code => [discussion] } - diff_file_class = diff_file.text? ? 'text-file' : 'js-image-file' +- diff_data = {} +- expanded = discussion.expanded? || local_assigns.fetch(:expanded, nil) +- unless expanded + - diff_data = { lines_path: project_merge_request_discussion_path(discussion.project, discussion.noteable, discussion) } -.diff-file.file-holder{ class: diff_file_class } +.diff-file.file-holder{ class: diff_file_class, data: diff_data } .js-file-title.file-title.file-title-flex-parent .file-header-content = render "projects/diffs/file_header", diff_file: diff_file, url: discussion_path(discussion), show_toggle: false @@ -11,17 +15,24 @@ - if diff_file.text? .diff-content.code.js-syntax-highlight %table - = render partial: "projects/diffs/line", - collection: discussion.truncated_diff_lines, - as: :line, - locals: { diff_file: diff_file, - discussions: discussions, - discussion_expanded: true, - plain: true } + - if expanded + - discussions = { discussion.original_line_code => [discussion] } + = render partial: "projects/diffs/line", + collection: discussion.truncated_diff_lines, + as: :line, + locals: { diff_file: diff_file, + discussions: discussions, + discussion_expanded: true, + plain: true } + - else + %tr.line_holder.line-holder-placeholder + %td.old_line.diff-line-num + %td.new_line.diff-line-num + %td.line_content + .js-code-placeholder + = render "discussions/diff_discussion", discussions: [discussion], expanded: true - else - partial = (diff_file.new_file? || diff_file.deleted_file?) ? 'single_image_diff' : 'replaced_image_diff' - = render partial: "projects/diffs/#{partial}", locals: { diff_file: diff_file, position: discussion.position.to_json, click_to_comment: false } - .note-container = render partial: "discussions/notes", locals: { discussion: discussion, show_toggle: false, show_image_comment_badge: true, disable_collapse_class: true } diff --git a/app/views/discussions/_discussion.html.haml b/app/views/discussions/_discussion.html.haml index 8b9fa3d6b05..e9589213f80 100644 --- a/app/views/discussions/_discussion.html.haml +++ b/app/views/discussions/_discussion.html.haml @@ -8,7 +8,7 @@ .discussion.js-toggle-container{ data: { discussion_id: discussion.id } } .discussion-header .discussion-actions - %button.note-action-button.discussion-toggle-button.js-toggle-button{ type: "button" } + %button.note-action-button.discussion-toggle-button.js-toggle-button{ type: "button", class: ("js-toggle-lazy-diff" unless expanded) } - if expanded = icon("chevron-up") - else diff --git a/app/views/groups/boards/index.html.haml b/app/views/groups/boards/index.html.haml new file mode 100644 index 00000000000..bb56769bd3f --- /dev/null +++ b/app/views/groups/boards/index.html.haml @@ -0,0 +1 @@ += render "shared/boards/show", board: @boards.first diff --git a/app/views/groups/boards/show.html.haml b/app/views/groups/boards/show.html.haml new file mode 100644 index 00000000000..92838fa4b11 --- /dev/null +++ b/app/views/groups/boards/show.html.haml @@ -0,0 +1 @@ += render "shared/boards/show", board: @board, group: true diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml index ca3f018c5e6..36df03302e8 100644 --- a/app/views/groups/issues.html.haml +++ b/app/views/groups/issues.html.haml @@ -2,9 +2,6 @@ = content_for :meta_tags do = auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@group.name} issues") -- content_for :page_specific_javascripts do - = webpack_bundle_tag 'common_vue' - - if group_issues_count(state: 'all').zero? = render 'shared/empty_states/issues', project_select_button: true - else diff --git a/app/views/groups/labels/index.html.haml b/app/views/groups/labels/index.html.haml index d10efdad53b..ac7e12fcd0b 100644 --- a/app/views/groups/labels/index.html.haml +++ b/app/views/groups/labels/index.html.haml @@ -1,8 +1,10 @@ - page_title 'Labels' +- issuables = ['issues', 'merge requests'] + .top-area.adjust .nav-text - Labels can be applied to issues and merge requests. Group labels are available for any project within the group. + = _("Labels can be applied to %{features}. Group labels are available for any project within the group.") % { features: issuables.to_sentence } .nav-controls - if can?(current_user, :admin_label, @group) @@ -16,4 +18,4 @@ = paginate @labels, theme: 'gitlab' - else .nothing-here-block - No labels created yet. + = _("No labels created yet.") diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml index 8d2bc810a7d..ef181b425bc 100644 --- a/app/views/groups/projects.html.haml +++ b/app/views/groups/projects.html.haml @@ -14,7 +14,7 @@ .list-item-name %span{ class: visibility_level_color(project.visibility_level) } = visibility_level_icon(project.visibility_level) - %strong= link_to project.name_with_namespace, project + %strong= link_to project.full_name, project .pull-right - if project.archived %span.label.label-warning archived diff --git a/app/views/ide/index.html.haml b/app/views/ide/index.html.haml index 3dbdfc97654..e0e8fe548d0 100644 --- a/app/views/ide/index.html.haml +++ b/app/views/ide/index.html.haml @@ -2,10 +2,11 @@ - page_title 'IDE' - content_for :page_specific_javascripts do - = webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'ide', force_same_domain: true -#ide.ide-loading{ data: {"empty-state-svg-path" => image_path('illustrations/multi_file_editor_empty.svg')} } +#ide.ide-loading{ data: {"empty-state-svg-path" => image_path('illustrations/multi_file_editor_empty.svg'), + "no-changes-state-svg-path" => image_path('illustrations/multi-editor_no_changes_empty.svg'), + "committed-state-svg-path" => image_path('illustrations/multi-editor_all_changes_committed_empty.svg') } } .text-center = icon('spinner spin 2x') %h2.clgray= _('Loading the GitLab IDE...') diff --git a/app/views/import/_githubish_status.html.haml b/app/views/import/_githubish_status.html.haml index e9a04e6c122..638c8b5a672 100644 --- a/app/views/import/_githubish_status.html.haml +++ b/app/views/import/_githubish_status.html.haml @@ -2,11 +2,11 @@ - provider_title = Gitlab::ImportSources.title(provider) %p.light - Select projects you want to import. + = import_githubish_choose_repository_message %hr %p = button_tag class: "btn btn-import btn-success js-import-all" do - Import all projects + = import_all_githubish_repositories_button_label = icon("spinner spin", class: "loading-icon") .table-responsive @@ -16,9 +16,9 @@ %colgroup.import-jobs-status-col %thead %tr - %th From #{provider_title} - %th To GitLab - %th Status + %th= _('From %{provider_title}') % { provider_title: provider_title } + %th= _('To GitLab') + %th= _('Status') %tbody - @already_added_projects.each do |project| %tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" } @@ -30,10 +30,12 @@ - if project.import_status == 'finished' %span %i.fa.fa-check - done + = _('Done') - elsif project.import_status == 'started' %i.fa.fa-spinner.fa-spin - started + = _('Started') + - elsif project.import_status == 'failed' + = _('Failed') - else = project.human_import_status_name @@ -55,7 +57,9 @@ = text_field_tag :path, repo.name, class: "input-mini form-control", tabindex: 2, autofocus: true, required: true %td.import-actions.job-status = button_tag class: "btn btn-import js-add-to-import" do - Import + = has_ci_cd_only_params? ? _('Connect') : _('Import') = icon("spinner spin", class: "loading-icon") -.js-importer-status{ data: { jobs_import_path: "#{url_for([:jobs, :import, provider])}", import_path: "#{url_for([:import, provider])}" } } +.js-importer-status{ data: { jobs_import_path: "#{url_for([:jobs, :import, provider])}", + import_path: "#{url_for([:import, provider])}", + ci_cd_only: "#{has_ci_cd_only_params?}" } } diff --git a/app/views/import/github/new.html.haml b/app/views/import/github/new.html.haml index 9c2da3a3eec..54ef51b30e3 100644 --- a/app/views/import/github/new.html.haml +++ b/app/views/import/github/new.html.haml @@ -1,43 +1,31 @@ -- page_title "GitHub Import" +- title = has_ci_cd_only_params? ? _('Connect repositories from GitHub') : _('GitHub import') +- page_title title +- breadcrumb_title title - header_title "Projects", root_path %h3.page-title - = icon 'github', text: 'Import Projects from GitHub' + = icon 'github', text: import_github_title - if github_import_configured? %p - To import a GitHub project, you first need to authorize GitLab to access - the list of your GitHub repositories: + = import_github_authorize_message - = link_to 'List your GitHub repositories', status_import_github_path, class: 'btn btn-success' + = link_to _('List your GitHub repositories'), status_import_github_path(ci_cd_only: params[:ci_cd_only]), class: 'btn btn-success' %hr %p - - if github_import_configured? - Alternatively, - - else - To import a GitHub project, - you can use a - = succeed '.' do - = link_to 'Personal Access Token', 'https://github.com/settings/tokens' - When you create your Personal Access Token, - you will need to select the <code>repo</code> scope, so we can display a - list of your public and private repositories which are available for import. + = import_github_personal_access_token_message = form_tag personal_access_token_import_github_path, method: :post, class: 'form-inline' do .form-group - = text_field_tag :personal_access_token, '', class: 'form-control', placeholder: "Personal Access Token", size: 40 - = submit_tag 'List your GitHub repositories', class: 'btn btn-success' + = text_field_tag :personal_access_token, '', class: 'form-control', placeholder: _('Personal Access Token'), size: 40 + = submit_tag _('List your GitHub repositories'), class: 'btn btn-success' + + -# EE-specific start + -# EE-specific end - unless github_import_configured? %hr %p - Note: - - if current_user.admin? - As an administrator you may like to configure - - else - Consider asking your GitLab administrator to configure - = link_to 'GitHub integration', help_page_path("integration/github") - which will allow login via GitHub and allow importing projects without - generating a Personal Access Token. + = import_configure_github_admin_message diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml index 0fe578a0036..b00b972d9c9 100644 --- a/app/views/import/github/status.html.haml +++ b/app/views/import/github/status.html.haml @@ -1,6 +1,8 @@ -- page_title "GitHub Import" +- title = has_ci_cd_only_params? ? _('Connect repositories from GitHub') : _('GitHub import') +- page_title title +- breadcrumb_title title - header_title "Projects", root_path %h3.page-title - = icon 'github', text: 'Import Projects from GitHub' + = icon 'github', text: import_github_title = render 'import/githubish_status', provider: 'github' diff --git a/app/views/invites/show.html.haml b/app/views/invites/show.html.haml index ad6213b4efd..c2bb1216c5f 100644 --- a/app/views/invites/show.html.haml +++ b/app/views/invites/show.html.haml @@ -12,7 +12,7 @@ - project = @member.source project %strong - = link_to project.name_with_namespace, project_url(project) + = link_to project.full_name, project_url(project) - when Group - group = @member.source group diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index 0c979109b3f..b981b5fdafa 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -42,7 +42,6 @@ = webpack_bundle_tag "common" = webpack_bundle_tag "main" = webpack_bundle_tag "raven" if Gitlab::CurrentSettings.clientside_sentry_enabled - = webpack_bundle_tag "test" if Rails.env.test? - if content_for?(:page_specific_javascripts) = yield :page_specific_javascripts diff --git a/app/views/layouts/_mailer.html.haml b/app/views/layouts/_mailer.html.haml index b50537438a9..ddc1cdb24b5 100644 --- a/app/views/layouts/_mailer.html.haml +++ b/app/views/layouts/_mailer.html.haml @@ -67,12 +67,8 @@ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" } %img{ alt: "GitLab", height: "33", src: image_url('mailers/gitlab_footer_logo.gif'), style: "display:block;margin:0 auto 1em;", width: "90" }/ %div - %a{ href: profile_notifications_url, style: "color:#3777b0;text-decoration:none;" } Manage all notifications - · - %a{ href: help_url, style: "color:#3777b0;text-decoration:none;" } Help - %div - You're receiving this email because of your account on - = succeed "." do - %a{ href: root_url, style: "color:#3777b0;text-decoration:none;" }= Gitlab.config.gitlab.host + - manage_notifications_link = link_to(_("Manage all notifications"), profile_notifications_url, style: "color:#3777b0;text-decoration:none;") + - help_link = link_to(_("Help"), help_url, style: "color:#3777b0;text-decoration:none;") + = _("You're receiving this email because of your account on %{host}. %{manage_notifications_link} · %{help_link}").html_safe % { host: Gitlab.config.gitlab.host, manage_notifications_link: manage_notifications_link, help_link: help_link } = yield :additional_footer diff --git a/app/views/layouts/nav/projects_dropdown/_show.html.haml b/app/views/layouts/nav/projects_dropdown/_show.html.haml index 59becb043d3..5809d6f7fea 100644 --- a/app/views/layouts/nav/projects_dropdown/_show.html.haml +++ b/app/views/layouts/nav/projects_dropdown/_show.html.haml @@ -1,4 +1,4 @@ -- project_meta = { id: @project.id, name: @project.name, namespace: @project.name_with_namespace, web_url: project_path(@project), avatar_url: @project.avatar_url } if @project&.persisted? +- project_meta = { id: @project.id, name: @project.name, namespace: @project.full_name, web_url: project_path(@project), avatar_url: @project.avatar_url } if @project&.persisted? .projects-dropdown-container .project-dropdown-sidebar.qa-projects-dropdown-sidebar %ul diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml index b520f28123f..5ea19c9882d 100644 --- a/app/views/layouts/nav/sidebar/_group.html.haml +++ b/app/views/layouts/nav/sidebar/_group.html.haml @@ -1,6 +1,6 @@ - issues_count = group_issues_count(state: 'opened') - merge_requests_count = group_merge_requests_count(state: 'opened') -- issues_sub_menu_items = ['groups#issues', 'labels#index', 'milestones#index'] +- issues_sub_menu_items = ['groups#issues', 'labels#index', 'milestones#index', 'boards#index', 'boards#show'] .nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?) } .nav-sidebar-inner-scroll @@ -51,12 +51,19 @@ %strong.fly-out-top-item-name #{ _('Issues') } %span.badge.count.issue_counter.fly-out-badge= number_with_delimiter(issues_count) + %li.divider.fly-out-top-item = nav_link(path: 'groups#issues', html_options: { class: 'home' }) do = link_to issues_group_path(@group), title: 'List' do %span List + - if group_sidebar_link?(:boards) + = nav_link(path: ['boards#index', 'boards#show']) do + = link_to group_boards_path(@group), title: boards_link_text do + %span + = boards_link_text + - if group_sidebar_link?(:labels) = nav_link(path: 'labels#index') do = link_to group_labels_path(@group), title: 'Labels' do diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml index 6b847fb4b7c..6b51483810e 100644 --- a/app/views/layouts/project.html.haml +++ b/app/views/layouts/project.html.haml @@ -1,4 +1,4 @@ -- page_title @project.name_with_namespace +- page_title @project.full_name - page_description @project.description unless page_description - header_title project_title(@project) unless header_title - nav "project" diff --git a/app/views/notify/project_was_exported_email.html.haml b/app/views/notify/project_was_exported_email.html.haml index f0ba7827cef..71c62f6be4e 100644 --- a/app/views/notify/project_was_exported_email.html.haml +++ b/app/views/notify/project_was_exported_email.html.haml @@ -3,6 +3,6 @@ %p The project export can be downloaded from: = link_to download_export_project_url(@project), rel: 'nofollow', download: '' do - = @project.name_with_namespace + " export" + = @project.full_name + " export" %p The download link will expire in 24 hours. diff --git a/app/views/notify/project_was_moved_email.html.haml b/app/views/notify/project_was_moved_email.html.haml index c476a39b661..1b6b1a81665 100644 --- a/app/views/notify/project_was_moved_email.html.haml +++ b/app/views/notify/project_was_moved_email.html.haml @@ -3,7 +3,7 @@ %p The project is now located under = link_to project_url(@project) do - = @project.name_with_namespace + = @project.full_name %p To update the remote url in your local repository run (for ssh): %p{ style: "background: #f5f5f5; padding:10px; border:1px solid #ddd" } diff --git a/app/views/peek/_bar.html.haml b/app/views/peek/_bar.html.haml new file mode 100644 index 00000000000..b4d86e1601c --- /dev/null +++ b/app/views/peek/_bar.html.haml @@ -0,0 +1,12 @@ +- return unless peek_enabled? + +#js-peek{ data: { env: Peek.env, + request_id: Peek.request_id, + peek_url: peek_routes.results_url, + profile_url: url_for(params.merge(lineprofiler: 'true')) }, + class: Peek.env } + +#peek-view-performance-bar.hidden + = render_server_response_time + %span#serverstats + %ul.performance-bar diff --git a/app/views/peek/views/_gc.html.haml b/app/views/peek/views/_gc.html.haml new file mode 100644 index 00000000000..9fc83e56ee7 --- /dev/null +++ b/app/views/peek/views/_gc.html.haml @@ -0,0 +1,7 @@ +- local_assigns.fetch(:view) + +%span.bold + %span{ title: 'Invoke Time', data: { defer_to: "#{view.defer_key}-gc_time" } }... + \/ + %span{ title: 'Invoke Count', data: { defer_to: "#{view.defer_key}-invokes" } }... +gc diff --git a/app/views/peek/views/_gitaly.html.haml b/app/views/peek/views/_gitaly.html.haml deleted file mode 100644 index a7d040d6821..00000000000 --- a/app/views/peek/views/_gitaly.html.haml +++ /dev/null @@ -1,7 +0,0 @@ -- local_assigns.fetch(:view) - -%strong - %span{ data: { defer_to: "#{view.defer_key}-duration" } } ... - \/ - %span{ data: { defer_to: "#{view.defer_key}-calls" } } ... - Gitaly diff --git a/app/views/peek/views/_host.html.haml b/app/views/peek/views/_host.html.haml deleted file mode 100644 index 40769b5c6f6..00000000000 --- a/app/views/peek/views/_host.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -%span.current-host - = truncate(view.hostname) diff --git a/app/views/peek/views/_mysql2.html.haml b/app/views/peek/views/_mysql2.html.haml deleted file mode 100644 index ac811a10ef5..00000000000 --- a/app/views/peek/views/_mysql2.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -- local_assigns.fetch(:view) - -= render 'peek/views/sql', view: view -mysql diff --git a/app/views/peek/views/_pg.html.haml b/app/views/peek/views/_pg.html.haml deleted file mode 100644 index ee94c2f3274..00000000000 --- a/app/views/peek/views/_pg.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -- local_assigns.fetch(:view) - -= render 'peek/views/sql', view: view -pg diff --git a/app/views/peek/views/_rblineprof.html.haml b/app/views/peek/views/_rblineprof.html.haml deleted file mode 100644 index 6c037930ca9..00000000000 --- a/app/views/peek/views/_rblineprof.html.haml +++ /dev/null @@ -1,7 +0,0 @@ -Profile: - -= link_to 'all', url_for(lineprofiler: 'true'), class: 'js-toggle-modal-peek-line-profile' -\/ -= link_to 'app & lib', url_for(lineprofiler: 'app'), class: 'js-toggle-modal-peek-line-profile' -\/ -= link_to 'views', url_for(lineprofiler: 'views'), class: 'js-toggle-modal-peek-line-profile' diff --git a/app/views/peek/views/_redis.html.haml b/app/views/peek/views/_redis.html.haml new file mode 100644 index 00000000000..f7fba6c95fc --- /dev/null +++ b/app/views/peek/views/_redis.html.haml @@ -0,0 +1,7 @@ +- local_assigns.fetch(:view) + +%span.bold + %span{ data: { defer_to: "#{view.defer_key}-duration" } }... + \/ + %span{ data: { defer_to: "#{view.defer_key}-calls" } }... +redis diff --git a/app/views/peek/views/_sidekiq.html.haml b/app/views/peek/views/_sidekiq.html.haml new file mode 100644 index 00000000000..7efbc05890d --- /dev/null +++ b/app/views/peek/views/_sidekiq.html.haml @@ -0,0 +1,7 @@ +- local_assigns.fetch(:view) + +%span.bold + %span{ data: { defer_to: "#{view.defer_key}-duration" } }... + \/ + %span{ data: { defer_to: "#{view.defer_key}-calls" } }... +sidekiq diff --git a/app/views/peek/views/_sql.html.haml b/app/views/peek/views/_sql.html.haml deleted file mode 100644 index dd8b524064f..00000000000 --- a/app/views/peek/views/_sql.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -%strong - %a.js-toggle-modal-peek-sql - %span{ data: { defer_to: "#{view.defer_key}-duration" } }... - \/ - %span{ data: { defer_to: "#{view.defer_key}-calls" } }... -#modal-peek-pg-queries.modal{ tabindex: -1 } - .modal-dialog.modal-full - .modal-content - .modal-header - %button.close.btn.btn-link.btn-sm{ type: 'button', data: { dismiss: 'modal' } } X - %h4 - SQL queries - .modal-body{ data: { defer_to: "#{view.defer_key}-queries" } }... diff --git a/app/views/profiles/chat_names/_chat_name.html.haml b/app/views/profiles/chat_names/_chat_name.html.haml index fe1cf802971..c7094800fb2 100644 --- a/app/views/profiles/chat_names/_chat_name.html.haml +++ b/app/views/profiles/chat_names/_chat_name.html.haml @@ -4,7 +4,7 @@ %td %strong - if can?(current_user, :read_project, project) - = link_to project.name_with_namespace, project_path(project) + = link_to project.full_name, project_path(project) - else .light N/A %td diff --git a/app/views/profiles/keys/index.html.haml b/app/views/profiles/keys/index.html.haml index 457583cfd35..1e206def7ee 100644 --- a/app/views/profiles/keys/index.html.haml +++ b/app/views/profiles/keys/index.html.haml @@ -12,7 +12,9 @@ Add an SSH key %p.profile-settings-content Before you can add an SSH key you need to - = link_to "generate it.", help_page_path("ssh/README") + = link_to "generate one", help_page_path("ssh/README", anchor: 'generating-a-new-ssh-key-pair') + or use an + = link_to "existing key.", help_page_path("ssh/README", anchor: 'locating-an-existing-ssh-key-pair') = render 'form' %hr %h5 diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml index 329bf16895f..1bd10018b40 100644 --- a/app/views/profiles/two_factor_auths/show.html.haml +++ b/app/views/profiles/two_factor_auths/show.html.haml @@ -2,10 +2,6 @@ - add_to_breadcrumbs("Two-Factor Authentication", profile_account_path) - @content_class = "limit-container-width" unless fluid_layout - -- content_for :page_specific_javascripts do - = webpack_bundle_tag('two_factor_auth') - .js-two-factor-auth{ 'data-two-factor-skippable' => "#{two_factor_skippable?}", 'data-two_factor_skip_url' => skip_profile_two_factor_auth_path } .row.prepend-top-default .col-lg-4 diff --git a/app/views/projects/_commit_button.html.haml b/app/views/projects/_commit_button.html.haml index b55dc3dce5c..b387e38c1a6 100644 --- a/app/views/projects/_commit_button.html.haml +++ b/app/views/projects/_commit_button.html.haml @@ -3,6 +3,4 @@ = link_to 'Cancel', cancel_path, class: 'btn btn-cancel', data: {confirm: leave_edit_message} - - unless can?(current_user, :push_code, @project) - .inline.prepend-left-10 - = commit_in_fork_help + = render 'shared/projects/edit_information' diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index b565f14747a..a2ecfddb163 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -23,6 +23,12 @@ - deleted_message = s_('ForkedFromProjectPath|Forked from %{project_name} (deleted)') = deleted_message % { project_name: fork_source_name(@project) } + .project-badges + - @project.badges.each do |badge| + - badge_link_url = badge.rendered_link_url(@project) + %a{ href: badge_link_url, target: '_blank', rel: 'noopener noreferrer' } + %img{ src: badge.rendered_image_url(@project), alt: badge_link_url } + .project-repo-buttons .count-buttons = render 'projects/buttons/star' diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml index d367bd6be7b..f4b5ef1555e 100644 --- a/app/views/projects/_new_project_fields.html.haml +++ b/app/views/projects/_new_project_fields.html.haml @@ -1,6 +1,8 @@ - visibility_level = params.dig(:project, :visibility_level) || default_project_visibility +- ci_cd_only = local_assigns.fetch(:ci_cd_only, false) .row{ id: project_name_id } + = f.hidden_field :ci_cd_only, value: ci_cd_only .form-group.project-path.col-sm-6 = f.label :namespace_id, class: 'label-light' do %span diff --git a/app/views/projects/blob/_new_dir.html.haml b/app/views/projects/blob/_new_dir.html.haml index 5d48a35dc4c..48ff66900be 100644 --- a/app/views/projects/blob/_new_dir.html.haml +++ b/app/views/projects/blob/_new_dir.html.haml @@ -17,6 +17,4 @@ = submit_tag _("Create directory"), class: 'btn btn-create' = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" - - unless can?(current_user, :push_code, @project) - .inline.prepend-left-10 - = commit_in_fork_help + = render 'shared/projects/edit_information' diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml index f1324c61500..182d02376bf 100644 --- a/app/views/projects/blob/_upload.html.haml +++ b/app/views/projects/blob/_upload.html.haml @@ -24,6 +24,4 @@ = button_title = link_to _("Cancel"), '#', class: "btn btn-cancel", "data-dismiss" => "modal" - - unless can?(current_user, :push_code, @project) - .inline.prepend-left-10 - = commit_in_fork_help + = render 'shared/projects/edit_information' diff --git a/app/views/projects/branches/_panel.html.haml b/app/views/projects/branches/_panel.html.haml new file mode 100644 index 00000000000..12e5a8e8d69 --- /dev/null +++ b/app/views/projects/branches/_panel.html.haml @@ -0,0 +1,19 @@ +- branches = local_assigns.fetch(:branches) +- state = local_assigns.fetch(:state) +- panel_title = local_assigns.fetch(:panel_title) +- show_more_text = local_assigns.fetch(:show_more_text) +- project = local_assigns.fetch(:project) +- overview_max_branches = local_assigns.fetch(:overview_max_branches) + +- return unless branches.any? + +.panel.panel-default.prepend-top-10 + .panel-heading + %h4.panel-title + = panel_title + %ul.content-list.all-branches + - branches.first(overview_max_branches).each do |branch| + = render "projects/branches/branch", branch: branch, merged: project.repository.merged_to_root_ref?(branch) + - if branches.size > overview_max_branches + .panel-footer.text-center + = link_to show_more_text, project_branches_filtered_path(project, state: state), id: "state-#{state}", data: { state: state } diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml index fb770764364..5dcc72d8263 100644 --- a/app/views/projects/branches/index.html.haml +++ b/app/views/projects/branches/index.html.haml @@ -3,26 +3,35 @@ %div{ class: container_class } .top-area.adjust - - if can?(current_user, :admin_project, @project) - .nav-text - - project_settings_link = link_to s_('Branches|project settings'), project_protected_branches_path(@project) - = s_('Branches|Protected branches can be managed in %{project_settings_link}').html_safe % { project_settings_link: project_settings_link } + %ul.nav-links.issues-state-filters + %li{ class: active_when(@mode == 'overview') }> + = link_to s_('Branches|Overview'), project_branches_path(@project), title: s_('Branches|Show overview of the branches') + + %li{ class: active_when(@mode == 'active') }> + = link_to s_('Branches|Active'), project_branches_filtered_path(@project, state: 'active'), title: s_('Branches|Show active branches') + + %li{ class: active_when(@mode == 'stale') }> + = link_to s_('Branches|Stale'), project_branches_filtered_path(@project, state: 'stale'), title: s_('Branches|Show stale branches') + + %li{ class: active_when(!%w[overview active stale].include?(@mode)) }> + = link_to s_('Branches|All'), project_branches_filtered_path(@project, state: 'all'), title: s_('Branches|Show all branches') .nav-controls - = form_tag(filter_branches_path, method: :get) do + = form_tag(project_branches_filtered_path(@project, state: 'all'), method: :get) do = search_field_tag :search, params[:search], { placeholder: s_('Branches|Filter by branch name'), id: 'branch-search', class: 'form-control search-text-input input-short', spellcheck: false } - .dropdown.inline> - %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' } - %span.light - = branches_sort_options_hash[@sort] - = icon('chevron-down') - %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable - %li.dropdown-header - = s_('Branches|Sort by') - - branches_sort_options_hash.each do |value, title| - %li - = link_to title, filter_branches_path(sort: value), class: ("is-active" if @sort == value) + - unless @mode == 'overview' + .dropdown.inline> + %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' } + %span.light + = branches_sort_options_hash[@sort] + = icon('chevron-down') + %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable + %li.dropdown-header + = s_('Branches|Sort by') + - branches_sort_options_hash.each do |value, title| + %li + = link_to title, project_branches_filtered_path(@project, state: 'all', search: params[:search], sort: value), class: ("is-active" if @sort == value) - if can? current_user, :push_code, @project = link_to project_merged_branches_path(@project), @@ -35,7 +44,17 @@ = link_to new_project_branch_path(@project), class: 'btn btn-create' do = s_('Branches|New branch') - - if @branches.any? + - if can?(current_user, :admin_project, @project) + - project_settings_link = link_to s_('Branches|project settings'), project_protected_branches_path(@project) + .row-content-block + %h5 + = s_('Branches|Protected branches can be managed in %{project_settings_link}.').html_safe % { project_settings_link: project_settings_link } + + - if @mode == 'overview' && (@active_branches.any? || @stale_branches.any?) + = render "projects/branches/panel", branches: @active_branches, state: 'active', panel_title: s_('Branches|Active branches'), show_more_text: s_('Branches|Show more active branches'), project: @project, overview_max_branches: @overview_max_branches + = render "projects/branches/panel", branches: @stale_branches, state: 'stale', panel_title: s_('Branches|Stale branches'), show_more_text: s_('Branches|Show more stale branches'), project: @project, overview_max_branches: @overview_max_branches + + - elsif @branches.any? %ul.content-list.all-branches - @branches.each do |branch| = render "projects/branches/branch", branch: branch, merged: @merged_branch_names.include?(branch.name) diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index 0cd2d45c74b..9126476e79e 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -63,7 +63,7 @@ - if admin %td - if job.project - = link_to job.project.name_with_namespace, admin_project_path(job.project) + = link_to job.project.full_name, admin_project_path(job.project) %td - if job.try(:runner) = runner_link(job.runner) diff --git a/app/views/projects/clusters/_integration_form.html.haml b/app/views/projects/clusters/_integration_form.html.haml index d4c0cd82ce3..db97203a2aa 100644 --- a/app/views/projects/clusters/_integration_form.html.haml +++ b/app/views/projects/clusters/_integration_form.html.haml @@ -21,6 +21,12 @@ = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked') .form-group + %h5= s_('ClusterIntegration|Security') + %p + = s_("ClusterIntegration|The default cluster configuration grants access to a wide set of functionalities needed to successfully build and deploy a containerised application.") + = link_to s_("ClusterIntegration|Learn more about security configuration"), help_page_path('user/project/clusters/index.md', anchor: 'security-implications') + + .form-group %h5= s_('ClusterIntegration|Environment scope') %p = s_("ClusterIntegration|Choose which of your project's environments will use this Kubernetes cluster.") diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml index 93407956f56..21e4664d4e4 100644 --- a/app/views/projects/commit/_change.html.haml +++ b/app/views/projects/commit/_change.html.haml @@ -35,6 +35,4 @@ = submit_tag label, class: 'btn btn-create' = link_to _("Cancel"), '#', class: "btn btn-cancel", "data-dismiss" => "modal" - - unless can?(current_user, :push_code, @project) - .inline.prepend-left-10 - = commit_in_fork_help + = render 'shared/projects/edit_information' diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml index 02395b6eb9b..5041f322612 100644 --- a/app/views/projects/cycle_analytics/show.html.haml +++ b/app/views/projects/cycle_analytics/show.html.haml @@ -1,7 +1,5 @@ - @no_container = true - page_title "Cycle Analytics" -- content_for :page_specific_javascripts do - = webpack_bundle_tag('common_vue') #cycle-analytics{ class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project) } } - if @cycle_analytics_no_data diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml index b082ad0ef0e..6fd6018dea3 100644 --- a/app/views/projects/diffs/_stats.html.haml +++ b/app/views/projects/diffs/_stats.html.haml @@ -7,9 +7,9 @@ = icon("caret-down", class: "prepend-left-5") %span.diff-stats-additions-deletions-expanded#diff-stats with - %strong.cgreen #{sum_added_lines} additions + %strong.cgreen= pluralize(sum_added_lines, 'addition') and - %strong.cred #{sum_removed_lines} deletions + %strong.cred= pluralize(sum_removed_lines, 'deletion') .diff-stats-additions-deletions-collapsed.pull-right.hidden-xs.hidden-sm{ "aria-hidden": "true", "aria-describedby": "diff-stats" } %strong.cgreen< +#{sum_added_lines} diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index 0d656b25bc8..7ebe617766f 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -2,9 +2,6 @@ - page_title "Environments" - add_to_breadcrumbs("Pipelines", project_pipelines_path(@project)) -- content_for :page_specific_javascripts do - = webpack_bundle_tag("common_vue") - #environments-list-view{ data: { environments_data: environments_list_data, "can-create-deployment" => can?(current_user, :create_deployment, @project).to_s, "can-read-environment" => can?(current_user, :read_environment, @project).to_s, diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml index 9d9759ebc5f..d6f0b230b58 100644 --- a/app/views/projects/environments/metrics.html.haml +++ b/app/views/projects/environments/metrics.html.haml @@ -14,8 +14,10 @@ "documentation-path": help_page_path('administration/monitoring/prometheus/index.md'), "empty-getting-started-svg-path": image_path('illustrations/monitoring/getting_started.svg'), "empty-loading-svg-path": image_path('illustrations/monitoring/loading.svg'), + "empty-no-data-svg-path": image_path('illustrations/monitoring/no_data.svg'), "empty-unable-to-connect-svg-path": image_path('illustrations/monitoring/unable_to_connect.svg'), - "additional-metrics": additional_metrics_project_environment_path(@project, @environment, format: :json), + "metrics-endpoint": additional_metrics_project_environment_path(@project, @environment, format: :json), + "deployment-endpoint": project_environment_deployments_path(@project, @environment, format: :json), "project-path": project_path(@project), "tags-path": project_tags_path(@project), - "has-metrics": "#{@environment.has_metrics?}", deployment_endpoint: project_environment_deployments_path(@project, @environment, format: :json) } } + "has-metrics": "#{@environment.has_metrics?}" } } diff --git a/app/views/projects/environments/terminal.html.haml b/app/views/projects/environments/terminal.html.haml index 7be4ef39117..6ec4ff56552 100644 --- a/app/views/projects/environments/terminal.html.haml +++ b/app/views/projects/environments/terminal.html.haml @@ -3,7 +3,6 @@ - content_for :page_specific_javascripts do = stylesheet_link_tag "xterm/xterm" - = webpack_bundle_tag("terminal") %div{ class: container_class } .top-area diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml index 2599ce5c4b8..620fd1906ba 100644 --- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml +++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml @@ -53,7 +53,7 @@ - if admin %td - if generic_commit_status.project - = link_to generic_commit_status.project.name_with_namespace, admin_project_path(generic_commit_status.project) + = link_to generic_commit_status.project.full_name, admin_project_path(generic_commit_status.project) %td - if generic_commit_status.try(:runner) = runner_link(generic_commit_status.runner) diff --git a/app/views/projects/imports/show.html.haml b/app/views/projects/imports/show.html.haml index 8c490773a56..3b0c828ccd1 100644 --- a/app/views/projects/imports/show.html.haml +++ b/app/views/projects/imports/show.html.haml @@ -1,12 +1,11 @@ -- page_title @project.forked? ? "Forking in progress" : "Import in progress" +- page_title import_in_progress_title + .save-project-loader .center %h2 %i.fa.fa-spinner.fa-spin - - if @project.forked? - Forking in progress. - - else - Import in progress. - - if @project.external_import? + = import_in_progress_title + - if !has_ci_cd_only_params? && @project.external_import? %p.monospace git clone --bare #{@project.safe_import_url} - %p Please wait while we import the repository for you. Refresh at will. + %p + = import_wait_and_refresh_message diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index 64c648f201b..0c58dd60e2c 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -7,7 +7,9 @@ .issue-main-info .issue-title.title %span.issue-title-text - = confidential_icon(issue) + - if issue.confidential? + %span.has-tooltip{ title: _('Confidential') } + = confidential_icon(issue) = link_to issue.title, issue_path(issue) - if issue.tasks? %span.task-status.hidden-xs @@ -24,11 +26,11 @@ - if issue.milestone %span.issuable-milestone.hidden-xs - = link_to project_issues_path(issue.project, milestone_title: issue.milestone.title), data: { html: 1, toggle: 'tooltip', title: milestone_tooltip_title(issue.milestone) } do + = link_to project_issues_path(issue.project, milestone_title: issue.milestone.title), data: { html: 1, toggle: 'tooltip', title: issuable_milestone_tooltip_title(issue) } do = icon('clock-o') = issue.milestone.title - if issue.due_date - %span.issuable-due-date.hidden-xs{ class: "#{'cred' if issue.overdue?}" } + %span.issuable-due-date.hidden-xs.has-tooltip{ class: "#{'cred' if issue.overdue?}", title: _('Due date') } = icon('calendar') = issue.due_date.to_s(:medium) diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml index 5f97d31f610..5c36d2202a6 100644 --- a/app/views/projects/issues/_merge_requests.html.haml +++ b/app/views/projects/issues/_merge_requests.html.haml @@ -18,7 +18,7 @@ - unless @issue.project.id == merge_request.target_project.id in - project = merge_request.target_project - = link_to project.name_with_namespace, project_path(project) + = link_to project.full_name, project_path(project) - if merge_request.merged? %span.merge-request-status.prepend-left-10.merged diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index fb06ba58c27..c427a9eedc2 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -4,9 +4,6 @@ - page_title "Issues" - new_issue_email = @project.new_issuable_address(current_user, 'issue') -- content_for :page_specific_javascripts do - = webpack_bundle_tag 'common_vue' - = content_for :meta_tags do = auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@project.name} issues") diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index 80e4dce1a80..9c78bade254 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -4,6 +4,7 @@ - can_admin_label = can?(current_user, :admin_label, @project) - if @labels.exists? || @prioritized_labels.exists? + #promote-label-modal %div{ class: container_class } .top-area.adjust .nav-text diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index f45a000833b..a94267deeb2 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -23,11 +23,11 @@ - if merge_request.milestone %span.issuable-milestone.hidden-xs - = link_to project_merge_requests_path(merge_request.project, milestone_title: merge_request.milestone.title), data: { html: 1, toggle: 'tooltip', title: milestone_tooltip_title(merge_request.milestone) } do + = link_to project_merge_requests_path(merge_request.project, milestone_title: merge_request.milestone.title), data: { html: 1, toggle: 'tooltip', title: issuable_milestone_tooltip_title(merge_request) } do = icon('clock-o') = merge_request.milestone.title - if merge_request.target_project.default_branch != merge_request.target_branch - %span.project-ref-path + %span.project-ref-path.has-tooltip{ title: _('Target branch') } = link_to project_ref_path(merge_request.project, merge_request.target_branch), class: 'ref-name' do = sprite_icon('fork', size: 12, css_class: 'fork-sprite') @@ -51,11 +51,11 @@ = render_pipeline_status(merge_request.head_pipeline) - if merge_request.open? && merge_request.broken? %li.issuable-pipeline-broken.hidden-xs - = link_to merge_request_path(merge_request), class: "has-tooltip", title: "Cannot be merged automatically", data: { container: 'body' } do + = link_to merge_request_path(merge_request), class: "has-tooltip", title: _('Cannot be merged automatically') do = icon('exclamation-triangle') - if merge_request.assignee %li - = link_to_member(merge_request.source_project, merge_request.assignee, name: false, title: "Assigned to :name") + = link_to_member(merge_request.source_project, merge_request.assignee, name: false, title: _('Assigned to :name')) = render 'shared/issuable_meta_data', issuable: merge_request diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml index 720ba236434..b2c0d9e1cfa 100644 --- a/app/views/projects/merge_requests/index.html.haml +++ b/app/views/projects/merge_requests/index.html.haml @@ -6,9 +6,6 @@ - page_title "Merge Requests" - new_merge_request_email = @project.new_issuable_address(current_user, 'merge_request') -- content_for :page_specific_javascripts do - = webpack_bundle_tag 'common_vue' - %div{ class: container_class } = render 'projects/last_push' diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml index f2e35ef6e0c..9866cc716ee 100644 --- a/app/views/projects/merge_requests/show.html.haml +++ b/app/views/projects/merge_requests/show.html.haml @@ -5,11 +5,6 @@ - page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests" - page_description @merge_request.description - page_card_attributes @merge_request.card_attributes -- content_for :page_specific_javascripts do - = webpack_bundle_tag('common_vue') - - - if has_vue_discussions_cookie? - = webpack_bundle_tag('mr_notes') .merge-request{ data: { mr_action: j(params[:tab].presence || 'show'), url: merge_request_path(@merge_request, format: :json), project_path: project_path(@merge_request.project) } } = render "projects/merge_requests/mr_title" diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml index 6a7bc4b1888..5b0197ed58c 100644 --- a/app/views/projects/milestones/index.html.haml +++ b/app/views/projects/milestones/index.html.haml @@ -13,6 +13,7 @@ .milestones #delete-milestone-modal + #promote-milestone-modal %ul.content-list = render @milestones diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index de381d489c6..b423888c875 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -27,8 +27,15 @@ Edit - if @project.group - = link_to promote_project_milestone_path(@milestone.project, @milestone), title: "Promote to Group Milestone", class: 'btn btn-grouped', data: { confirm: "Promoting #{@milestone.title} will make it available for all projects inside #{@project.group.name}. Existing project milestones with the same name will be merged. This action cannot be reversed.", toggle: "tooltip" }, method: :post do - Promote + %button.js-promote-project-milestone-button.btn.btn-grouped{ data: { toggle: 'modal', + target: '#promote-milestone-modal', + milestone_title: @milestone.title, + url: promote_project_milestone_path(@milestone.project, @milestone), + container: 'body' }, + disabled: true, + type: 'button' } + = _('Promote') + #promote-milestone-modal - if @milestone.active? = link_to 'Close milestone', project_milestone_path(@project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-nr btn-grouped" diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 679ba23a4db..8cdb0a6aff4 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -12,11 +12,14 @@ .row.prepend-top-default .col-lg-3.profile-settings-sidebar %h4.prepend-top-0 - New project + = _('New project') %p - A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), #{link_to 'among other things', help_page_path("user/project/index.md", anchor: "projects-features"), target: '_blank'}. + - among_other_things_link = link_to _('among other things'), help_page_path("user/project/index.md", anchor: "projects-features"), target: '_blank' + = _('A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}.').html_safe % { among_other_things_link: among_other_things_link } %p - All features are enabled when you create a project, but you can disable the ones you don’t need in the project settings. + = _('All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings.') + -# EE-specific start + -# EE-specific end .md = brand_new_project_guidelines %p @@ -28,36 +31,38 @@ .col-lg-9.js-toggle-container %ul.nav-links.gitlab-tabs{ role: 'tablist' } - %li{ class: ('active' if active_tab == 'blank'), role: 'presentation' } + %li{ class: active_when(active_tab == 'blank'), role: 'presentation' } %a{ href: '#blank-project-pane', id: 'blank-project-tab', data: { toggle: 'tab' }, role: 'tab' } %span.hidden-xs Blank project %span.visible-xs Blank - %li{ class: ('active' if active_tab == 'template'), role: 'presentation' } + %li{ class: active_when(active_tab == 'template'), role: 'presentation' } %a{ href: '#create-from-template-pane', id: 'create-from-template-tab', data: { toggle: 'tab' }, role: 'tab' } %span.hidden-xs Create from template %span.visible-xs Template - %li{ class: ('active' if active_tab == 'import'), role: 'presentation' } + %li{ class: active_when(active_tab == 'import'), role: 'presentation' } %a{ href: '#import-project-pane', id: 'import-project-tab', data: { toggle: 'tab' }, role: 'tab' } %span.hidden-xs Import project %span.visible-xs Import + -# EE-specific start + -# EE-specific end .tab-content.gitlab-tab-content - .tab-pane{ id: 'blank-project-pane', class: ('active' if active_tab == 'blank'), role: 'tabpanel' } + .tab-pane{ id: 'blank-project-pane', class: active_when(active_tab == 'blank'), role: 'tabpanel' } = form_for @project, html: { class: 'new_project' } do |f| = render 'new_project_fields', f: f, project_name_id: "blank-project-name" - .tab-pane.no-padding{ id: 'create-from-template-pane', class: ('active' if active_tab == 'template'), role: 'tabpanel' } + .tab-pane.no-padding{ id: 'create-from-template-pane', class: active_when(active_tab == 'template'), role: 'tabpanel' } = form_for @project, html: { class: 'new_project' } do |f| .project-template .form-group %div = render 'project_templates', f: f - .tab-pane.import-project-pane{ id: 'import-project-pane', class: ('active' if active_tab == 'import'), role: 'tabpanel' } + .tab-pane.import-project-pane.js-toggle-container{ id: 'import-project-pane', class: active_when(active_tab == 'import'), role: 'tabpanel' } = form_for @project, html: { class: 'new_project' } do |f| - if import_sources_enabled? .project-import.row - .col-sm-12 + .col-lg-12 .form-group.import-btn-container.clearfix = f.label :visibility_level, class: 'label-light' do #the label here seems wrong Import project from @@ -68,7 +73,7 @@ = icon('gitlab', text: 'GitLab export') %div - if github_import_enabled? - = link_to new_import_github_path, class: 'btn import_github' do + = link_to new_import_github_path, class: 'btn js-import-github' do = icon('github', text: 'GitHub') %div - if bitbucket_import_enabled? @@ -97,7 +102,7 @@ Gitea %div - if git_import_enabled? - %button.btn.js-toggle-button.import_git{ type: "button" } + %button.btn.js-toggle-button.js-import-git-toggle-button{ type: "button", data: { toggle_open_class: 'active' } } = icon('git', text: 'Repo by URL') .col-lg-12 .js-toggle-content.toggle-import-form{ class: ('hide' if active_tab != 'import') } @@ -105,6 +110,10 @@ = render "shared/import_form", f: f = render 'new_project_fields', f: f, project_name_id: "import-url-name" + + -# EE-specific start + -# EE-specific end + .save-project-loader.hide .center %h2 diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index cf95cdbfec2..3e6b3346787 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -7,8 +7,9 @@ "help-auto-devops-path" => help_page_path('topics/autodevops/index.md'), "empty-state-svg-path" => image_path('illustrations/pipelines_empty.svg'), "error-state-svg-path" => image_path('illustrations/pipelines_failed.svg'), - "new-pipeline-path" => new_project_pipeline_path(@project), + "no-pipelines-svg-path" => image_path('illustrations/pipelines_pending.svg'), "can-create-pipeline" => can?(current_user, :create_pipeline, @project).to_s, - "has-ci" => @repository.gitlab_ci_yml, - "ci-lint-path" => ci_lint_path, - "reset-cache-path" => reset_cache_project_settings_ci_cd_path(@project) } } + "new-pipeline-path" => can?(current_user, :create_pipeline, @project) && new_project_pipeline_path(@project), + "ci-lint-path" => can?(current_user, :create_pipeline, @project) && ci_lint_path, + "reset-cache-path" => can?(current_user, :admin_pipeline, @project) && reset_cache_project_settings_ci_cd_path(@project) , + "has-gitlab-ci" => (@project.has_ci? && @project.builds_enabled?).to_s } } diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml index ffb0ae95f9b..a7d7c923957 100644 --- a/app/views/projects/pipelines/show.html.haml +++ b/app/views/projects/pipelines/show.html.haml @@ -10,6 +10,3 @@ = render "projects/pipelines/with_tabs", pipeline: @pipeline .js-pipeline-details-vue{ data: { endpoint: project_pipeline_path(@project, @pipeline, format: :json) } } - -- content_for :page_specific_javascripts do - = webpack_bundle_tag('common_vue') diff --git a/app/views/projects/protected_branches/_index.html.haml b/app/views/projects/protected_branches/_index.html.haml index 127a338e413..2b0a502fe4d 100644 --- a/app/views/projects/protected_branches/_index.html.haml +++ b/app/views/projects/protected_branches/_index.html.haml @@ -1,6 +1,3 @@ -- content_for :page_specific_javascripts do - = webpack_bundle_tag('protected_branches') - - content_for :create_protected_branch do = render 'projects/protected_branches/create_protected_branch' diff --git a/app/views/projects/registry/repositories/index.html.haml b/app/views/projects/registry/repositories/index.html.haml index 27e1f9fba3e..12d56e244ce 100644 --- a/app/views/projects/registry/repositories/index.html.haml +++ b/app/views/projects/registry/repositories/index.html.haml @@ -14,8 +14,6 @@ .col-lg-12 #js-vue-registry-images{ data: { endpoint: project_container_registry_index_path(@project, format: :json) } } - = webpack_bundle_tag('common_vue') - .row.prepend-top-10 .col-lg-12 .panel.panel-default diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml index 17e804d682b..684b082efbb 100644 --- a/app/views/projects/services/_form.html.haml +++ b/app/views/projects/services/_form.html.haml @@ -5,6 +5,9 @@ = boolean_to_icon @service.activated? %p= @service.description + + - if @service.respond_to?(:detailed_description) + %p= @service.detailed_description .col-lg-9 = form_for(@service, as: :service, url: project_service_path(@project, @service.to_param), method: :put, html: { class: 'gl-show-field-errors form-horizontal integration-settings-form js-integration-settings-form', data: { 'can-test' => @service.can_test?, 'test-url' => test_project_service_path(@project, @service) } }) do |form| = render 'shared/service_settings', form: form, subject: @service @@ -12,11 +15,6 @@ .footer-block.row-content-block = service_save_button(@service) - - if @service.valid? && @service.activated? - - unless @service.can_test? - - disabled_class = 'disabled' - - disabled_title = @service.disabled_title - = link_to 'Cancel', project_settings_integrations_path(@project), class: 'btn btn-cancel' - if lookup_context.template_exists?('show', "projects/services/#{@service.to_param}", true) diff --git a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml index 5dbcbf7eba6..2ab0227126a 100644 --- a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml +++ b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml @@ -1,4 +1,4 @@ -- run_actions_text = "Perform common operations on GitLab project: #{@project.name_with_namespace}" +- run_actions_text = "Perform common operations on GitLab project: #{@project.full_name}" %p To setup this service: %ul.list-unstyled.indent-list @@ -20,7 +20,7 @@ .form-group = label_tag :display_name, 'Display name', class: 'col-sm-2 col-xs-12 control-label' .col-sm-10.col-xs-12.input-group - = text_field_tag :display_name, "GitLab / #{@project.name_with_namespace}", class: 'form-control input-sm', readonly: 'readonly' + = text_field_tag :display_name, "GitLab / #{@project.full_name}", class: 'form-control input-sm', readonly: 'readonly' .input-group-btn = clipboard_button(target: '#display_name') diff --git a/app/views/projects/services/prometheus/_show.html.haml b/app/views/projects/services/prometheus/_show.html.haml index 6dc2b85fd32..43e6a173108 100644 --- a/app/views/projects/services/prometheus/_show.html.haml +++ b/app/views/projects/services/prometheus/_show.html.haml @@ -7,21 +7,19 @@ = link_to s_('PrometheusService|More information'), help_page_path('user/project/integrations/prometheus') .col-lg-9 - .panel.panel-default.js-panel-monitored-metrics{ data: { active_metrics: active_common_project_prometheus_metrics_path(@project, :json) } } + .panel.panel-default.js-panel-monitored-metrics{ data: { active_metrics: active_common_project_prometheus_metrics_path(@project, :json), metrics_help_path: help_page_path('user/project/integrations/prometheus_library/metrics') } } .panel-heading %h3.panel-title - = s_('PrometheusService|Monitored') + = s_('PrometheusService|Common metrics') %span.badge.js-monitored-count 0 .panel-body - .loading-metrics.text-center.js-loading-metrics - = icon('spinner spin 3x', class: 'metrics-load-spinner') - %p + .loading-metrics.js-loading-metrics + %p.prepend-top-10.prepend-left-10 + = icon('spinner spin', class: 'metrics-load-spinner') = s_('PrometheusService|Finding and configuring metrics...') - .empty-metrics.text-center.hidden.js-empty-metrics - = custom_icon('icon_empty_metrics') - %p - = s_('PrometheusService|No metrics are being monitored. To start monitoring, deploy to an environment.') - = link_to s_('PrometheusService|View environments'), project_environments_path(@project), class: 'btn btn-success' + .empty-metrics.hidden.js-empty-metrics + %p.text-tertiary.prepend-top-10.prepend-left-10 + = s_('PrometheusService|Waiting for your first deployment to an environment to find common metrics') %ul.list-unstyled.metrics-list.hidden.js-metrics-list .panel.panel-default.hidden.js-panel-missing-env-vars diff --git a/app/views/projects/services/slack_slash_commands/_help.html.haml b/app/views/projects/services/slack_slash_commands/_help.html.haml index c31c95608c6..d592a5e4663 100644 --- a/app/views/projects/services/slack_slash_commands/_help.html.haml +++ b/app/views/projects/services/slack_slash_commands/_help.html.haml @@ -1,4 +1,4 @@ -- pretty_name = defined?(@project) ? @project.name_with_namespace : 'namespace / path' +- pretty_name = defined?(@project) ? @project.full_name : 'namespace / path' - run_actions_text = "Perform common operations on GitLab project: #{pretty_name}" .well diff --git a/app/views/projects/settings/repository/show.html.haml b/app/views/projects/settings/repository/show.html.haml index 235d532bf98..6bef4d19434 100644 --- a/app/views/projects/settings/repository/show.html.haml +++ b/app/views/projects/settings/repository/show.html.haml @@ -2,9 +2,6 @@ - page_title "Repository" - @content_class = "limit-container-width" unless fluid_layout -- content_for :page_specific_javascripts do - = webpack_bundle_tag('common_vue') - -# Protected branches & tags use a lot of nested partials. -# The shared parts of the views can be found in the `shared` directory. -# Those are used throughout the actual views. These `shared` views are then diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml index da364b58e36..10415d011d6 100644 --- a/app/views/projects/tags/index.html.haml +++ b/app/views/projects/tags/index.html.haml @@ -1,7 +1,6 @@ - @no_container = true - @sort ||= sort_value_recently_updated - page_title s_('TagsPage|Tags') -- add_to_breadcrumbs("Repository", project_tree_path(@project)) .flex-list{ class: container_class } .top-area.adjust diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml index 39511435508..5ef5e9c09a2 100644 --- a/app/views/projects/tree/_tree_header.html.haml +++ b/app/views/projects/tree/_tree_header.html.haml @@ -72,13 +72,12 @@ #{ _('New tag') } .tree-controls - - if show_new_ide? - = succeed " " do - = link_to ide_edit_path(@project, @id), class: 'btn btn-default' do - = _('Web IDE') - = link_to s_('Commits|History'), project_commits_path(@project, @id), class: 'btn' = render 'projects/find_file_link' + = succeed " " do + = link_to ide_edit_path(@project, @id, ""), class: 'btn btn-default' do + = _('Web IDE') + = render 'projects/buttons/download', project: @project, ref: @ref diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml index 915e648a5d3..7d43fd61081 100644 --- a/app/views/search/_category.html.haml +++ b/app/views/search/_category.html.haml @@ -14,25 +14,25 @@ = link_to search_filter_path(scope: 'issues') do Issues %span.badge - = @search_results.issues_count + = limited_count(@search_results.limited_issues_count) - if project_search_tabs?(:merge_requests) %li{ class: active_when(@scope == 'merge_requests') } = link_to search_filter_path(scope: 'merge_requests') do Merge requests %span.badge - = @search_results.merge_requests_count + = limited_count(@search_results.limited_merge_requests_count) - if project_search_tabs?(:milestones) %li{ class: active_when(@scope == 'milestones') } = link_to search_filter_path(scope: 'milestones') do Milestones %span.badge - = @search_results.milestones_count + = limited_count(@search_results.limited_milestones_count) - if project_search_tabs?(:notes) %li{ class: active_when(@scope == 'notes') } = link_to search_filter_path(scope: 'notes') do Comments %span.badge - = @search_results.notes_count + = limited_count(@search_results.limited_notes_count) - if project_search_tabs?(:wiki) %li{ class: active_when(@scope == 'wiki_blobs') } = link_to search_filter_path(scope: 'wiki_blobs') do diff --git a/app/views/search/_filter.html.haml b/app/views/search/_filter.html.haml index e43796e9654..e4902d368e7 100644 --- a/app/views/search/_filter.html.haml +++ b/app/views/search/_filter.html.haml @@ -22,7 +22,7 @@ %span.dropdown-toggle-text Project: - if @project.present? - = @project.name_with_namespace + = @project.full_name - else Any = icon("chevron-down") diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml index 60ef44482f0..ab56f48ba4d 100644 --- a/app/views/search/_results.html.haml +++ b/app/views/search/_results.html.haml @@ -6,7 +6,7 @@ = search_entries_info(@search_objects, @scope, @search_term) - unless @show_snippets - if @project - in project #{link_to @project.name_with_namespace, [@project.namespace.becomes(Namespace), @project]} + in project #{link_to @project.full_name, [@project.namespace.becomes(Namespace), @project]} - elsif @group in group #{link_to @group.name, @group} diff --git a/app/views/search/results/_issue.html.haml b/app/views/search/results/_issue.html.haml index b4bc8982c05..b7a27ef6be2 100644 --- a/app/views/search/results/_issue.html.haml +++ b/app/views/search/results/_issue.html.haml @@ -10,4 +10,4 @@ .description.term = search_md_sanitize(issue, :description) %span.light - #{issue.project.name_with_namespace} + #{issue.project.full_name} diff --git a/app/views/search/results/_merge_request.html.haml b/app/views/search/results/_merge_request.html.haml index 1a5499e4d58..8b0fd74f680 100644 --- a/app/views/search/results/_merge_request.html.haml +++ b/app/views/search/results/_merge_request.html.haml @@ -11,4 +11,4 @@ .description.term = search_md_sanitize(merge_request, :description) %span.light - #{merge_request.project.name_with_namespace} + #{merge_request.project.full_name} diff --git a/app/views/search/results/_note.html.haml b/app/views/search/results/_note.html.haml index a7e178dfa71..e4ab7b0541f 100644 --- a/app/views/search/results/_note.html.haml +++ b/app/views/search/results/_note.html.haml @@ -7,7 +7,7 @@ %i.fa.fa-comment = link_to_member(project, note.author, avatar: false) commented on - = link_to project.name_with_namespace, project + = link_to project.full_name, project · - if note.for_commit? diff --git a/app/views/search/results/_snippet_title.html.haml b/app/views/search/results/_snippet_title.html.haml index 65710c09a89..d46c4d11e51 100644 --- a/app/views/search/results/_snippet_title.html.haml +++ b/app/views/search/results/_snippet_title.html.haml @@ -11,7 +11,7 @@ %small.pull-right.cgray - if snippet_title.project_id? - = link_to snippet_title.project.name_with_namespace, project_path(snippet_title.project) + = link_to snippet_title.project.full_name, project_path(snippet_title.project) .snippet-info = snippet_title.to_reference diff --git a/app/views/sent_notifications/unsubscribe.html.haml b/app/views/sent_notifications/unsubscribe.html.haml index de52fd00157..7d3e243495f 100644 --- a/app/views/sent_notifications/unsubscribe.html.haml +++ b/app/views/sent_notifications/unsubscribe.html.haml @@ -1,7 +1,7 @@ - noteable = @sent_notification.noteable - noteable_type = @sent_notification.noteable_type.titleize.downcase - noteable_text = %(#{noteable.title} (#{noteable.to_reference})) -- page_title "Unsubscribe", noteable_text, noteable_type.pluralize, @sent_notification.project.name_with_namespace +- page_title "Unsubscribe", noteable_text, noteable_type.pluralize, @sent_notification.project.full_name %h3.page-title Unsubscribe from #{noteable_type} diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml index 736afa085e8..5eaaa1448d5 100644 --- a/app/views/shared/_import_form.html.haml +++ b/app/views/shared/_import_form.html.haml @@ -1,17 +1,22 @@ +- ci_cd_only = local_assigns.fetch(:ci_cd_only, false) + .form-group.import-url-data = f.label :import_url, class: 'label-light' do - %span Git repository URL + %span + = _('Git repository URL') - = f.text_field :import_url, autocomplete: 'off', class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git' + = f.text_field :import_url, autocomplete: 'off', class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git', required: true .well.prepend-top-20 %ul %li - The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>. + = _('The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>.').html_safe %li - If your HTTP repository is not publicly accessible, add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>. + = _('If your HTTP repository is not publicly accessible, add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>.').html_safe %li - The import will time out after #{time_interval_in_words(Gitlab.config.gitlab_shell.git_timeout)}. - For repositories that take longer, use a clone/push combination. + = import_will_timeout_message(ci_cd_only) %li - To migrate an SVN repository, check out #{link_to "this document", help_page_path('user/project/import/svn')}. + = import_svn_message(ci_cd_only) + +-# EE-specific start +-# EE-specific end diff --git a/app/views/shared/_issuable_meta_data.html.haml b/app/views/shared/_issuable_meta_data.html.haml index 435acbc634c..430d9a9dd76 100644 --- a/app/views/shared/_issuable_meta_data.html.haml +++ b/app/views/shared/_issuable_meta_data.html.haml @@ -5,21 +5,21 @@ - issuable_mr = @issuable_meta_data[issuable.id].merge_requests_count - if issuable_mr > 0 - %li.issuable-mr.hidden-xs + %li.issuable-mr.hidden-xs.has-tooltip{ title: _('Related merge requests') } = image_tag('icon-merge-request-unmerged.svg', class: 'icon-merge-request-unmerged') = issuable_mr - if upvotes > 0 - %li.issuable-upvotes.hidden-xs + %li.issuable-upvotes.hidden-xs.has-tooltip{ title: _('Upvotes') } = icon('thumbs-up') = upvotes - if downvotes > 0 - %li.issuable-downvotes.hidden-xs + %li.issuable-downvotes.hidden-xs.has-tooltip{ title: _('Downvotes') } = icon('thumbs-down') = downvotes %li.issuable-comments.hidden-xs - = link_to issuable_url, class: ('no-comments' if note_count.zero?) do + = link_to issuable_url, class: ['has-tooltip', ('no-comments' if note_count.zero?)], title: _('Comments') do = icon('comments') = note_count diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml index 8847d11f623..5afbc78df53 100644 --- a/app/views/shared/_label.html.haml +++ b/app/views/shared/_label.html.haml @@ -48,8 +48,16 @@ .pull-right.hidden-xs.hidden-sm - if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_label, label.project.group) - = link_to promote_project_label_path(label.project, label), title: "Promote to Group Label", class: 'btn btn-transparent btn-action', data: {confirm: "Promoting #{label.title} will make it available for all projects inside #{label.project.group.name}. Existing project labels with the same name will be merged. This action cannot be reversed.", toggle: "tooltip"}, method: :post do - %span.sr-only Promote to Group + %button.js-promote-project-label-button.btn.btn-transparent.btn-action.has-tooltip{ title: _('Promote to Group Label'), + disabled: true, + type: 'button', + data: { url: promote_project_label_path(label.project, label), + label_title: label.title, + label_color: label.color, + label_text_color: label.text_color, + target: '#promote-label-modal', + container: 'body', + toggle: 'modal' } } = sprite_icon('level-up') - if can?(current_user, :admin_label, label) = link_to edit_label_path(label), title: "Edit", class: 'btn btn-transparent btn-action', data: {toggle: "tooltip"} do diff --git a/app/views/shared/_new_commit_form.html.haml b/app/views/shared/_new_commit_form.html.haml index 0a4a24ae807..9221fd1e025 100644 --- a/app/views/shared/_new_commit_form.html.haml +++ b/app/views/shared/_new_commit_form.html.haml @@ -1,3 +1,6 @@ +- project = @project.present(current_user: current_user) +- branch_name = selected_branch + = render 'shared/commit_message_container', placeholder: placeholder - if @project.empty_repo? @@ -7,12 +10,14 @@ .form-group.branch = label_tag 'branch_name', _('Target Branch'), class: 'control-label' .col-sm-10 - = text_field_tag 'branch_name', @branch_name || tree_edit_branch, required: true, class: "form-control js-branch-name ref-name" + = text_field_tag 'branch_name', branch_name, required: true, class: "form-control js-branch-name ref-name" .js-create-merge-request-container = render 'shared/new_merge_request_checkbox' + - elsif project.can_current_user_push_to_branch?(branch_name) + = hidden_field_tag 'branch_name', branch_name - else - = hidden_field_tag 'branch_name', @branch_name || tree_edit_branch + = hidden_field_tag 'branch_name', branch_name = hidden_field_tag 'create_merge_request', 1 = hidden_field_tag 'original_branch', @ref, class: 'js-original-branch' diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml index 479bd2cdb38..4c8c92d722a 100644 --- a/app/views/shared/_ref_switcher.html.haml +++ b/app/views/shared/_ref_switcher.html.haml @@ -1,6 +1,5 @@ - show_create = local_assigns.fetch(:show_create, false) -- show_new_branch_form = show_new_ide? && show_create && can?(current_user, :push_code, @project) - dropdown_toggle_text = @ref || @project.default_branch = form_tag switch_project_refs_path(@project), method: :get, class: "project-refs-form" do = hidden_field_tag :destination, destination @@ -16,14 +15,3 @@ = dropdown_filter _("Search branches and tags") = dropdown_content = dropdown_loading - - if show_new_branch_form - = dropdown_footer do - %ul.dropdown-footer-list - %li - %a.dropdown-toggle-page{ href: "#" } - Create new branch - - if show_new_branch_form - .dropdown-page-two - = dropdown_title("Create new branch", options: { back: true }) - = dropdown_content do - .js-new-branch-dropdown diff --git a/app/views/shared/_service_settings.html.haml b/app/views/shared/_service_settings.html.haml index 61b39afb5d4..a41aaed66a3 100644 --- a/app/views/shared/_service_settings.html.haml +++ b/app/views/shared/_service_settings.html.haml @@ -13,12 +13,12 @@ .col-sm-10 = form.check_box :active, disabled: disable_fields_service?(@service) - - if @service.supported_events.present? + - if @service.configurable_events.present? .form-group = form.label :url, "Trigger", class: 'control-label' .col-sm-10 - - @service.supported_events.each do |event| + - @service.configurable_events.each do |event| %div = form.check_box service_event_field_name(event), class: 'pull-left' .prepend-left-20 @@ -33,7 +33,7 @@ = form.text_field field[:name], class: "form-control", placeholder: field[:placeholder] %p.light - = service_event_description(event) + = @service.class.event_description(event) - @service.global_fields.each do |field| - type = field[:type] diff --git a/app/views/shared/boards/_show.html.haml b/app/views/shared/boards/_show.html.haml index 014b8de1dc9..dac60094686 100644 --- a/app/views/shared/boards/_show.html.haml +++ b/app/views/shared/boards/_show.html.haml @@ -1,3 +1,5 @@ +- board = local_assigns.fetch(:board, nil) +- group = local_assigns.fetch(:group, false) - @no_breadcrumb_container = true - @no_container = true - @content_class = "issue-boards-content" @@ -5,7 +7,6 @@ - page_title "Boards" - content_for :page_specific_javascripts do - = webpack_bundle_tag 'common_vue' -# haml-lint:disable InlineJavaScript %script#js-board-template{ type: "text/x-template" }= render "shared/boards/components/board" @@ -26,8 +27,8 @@ ":issue-link-base" => "issueLinkBase", ":root-path" => "rootPath", ":board-id" => "boardId", - ":key" => "_uid" } - = render "shared/boards/components/sidebar" + ":key" => "list.id" } + = render "shared/boards/components/sidebar", group: group - if @project %board-add-issues-modal{ "new-issue-path" => new_project_issue_path(@project), "milestone-path" => milestones_filter_dropdown_path, diff --git a/app/views/shared/boards/components/_board.html.haml b/app/views/shared/boards/components/_board.html.haml index c687e66fd43..149bf8da4b9 100644 --- a/app/views/shared/boards/components/_board.html.haml +++ b/app/views/shared/boards/components/_board.html.haml @@ -4,7 +4,7 @@ %header.board-header{ ":class" => '{ "has-border": list.label && list.label.color }', ":style" => "{ borderTopColor: (list.label && list.label.color ? list.label.color : null) }", "@click" => "toggleExpanded($event)" } %h3.board-title.js-board-handle{ ":class" => '{ "user-can-drag": (!disabled && !list.preset) }' } %i.fa.fa-fw.board-title-expandable-toggle{ "v-if": "list.isExpandable", - ":class": "{ \"fa-caret-down\": list.isExpanded, \"fa-caret-right\": !list.isExpanded && list.position === -1, \"fa-caret-left\": !list.isExpanded && list.position !== -1 }", + ":class": "{ \"fa-caret-down\": list.isExpanded, \"fa-caret-right\": !list.isExpanded }", "aria-hidden": "true" } %span.board-title-text.has-tooltip{ "v-if": "list.type !== \"label\"", @@ -42,6 +42,7 @@ ":disabled" => "disabled", ":issue-link-base" => "issueLinkBase", ":root-path" => "rootPath", + ":groupId" => ((current_board_parent.id if @group) || 'null'), "ref" => "board-list" } - if can?(current_user, :admin_list, current_board_parent) %board-blank-state{ "v-if" => 'list.id == "blank"' } diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 6dfabd7ba4c..4c8f03f1498 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -33,6 +33,8 @@ = render 'shared/issuable/form/merge_params', issuable: issuable += render 'shared/issuable/form/contribution', issuable: issuable, form: form + - if @merge_request_to_resolve_discussions_of .form-group .col-sm-10.col-sm-offset-2 diff --git a/app/views/shared/issuable/_label_page_create.html.haml b/app/views/shared/issuable/_label_page_create.html.haml index d5e7d3b87b7..91aa329eb93 100644 --- a/app/views/shared/issuable/_label_page_create.html.haml +++ b/app/views/shared/issuable/_label_page_create.html.haml @@ -1,5 +1,6 @@ +- subject = @project || @group .dropdown-page-two.dropdown-new-label - = dropdown_title("Create new label", options: { back: true }) + = dropdown_title(create_label_title(subject), options: { back: true }) = dropdown_content do .dropdown-labels-error.js-label-error %input#new_label_name.default-dropdown-input{ type: "text", placeholder: _('Name new label') } diff --git a/app/views/shared/issuable/_label_page_default.html.haml b/app/views/shared/issuable/_label_page_default.html.haml index 6a83321abcb..2bd922bca2b 100644 --- a/app/views/shared/issuable/_label_page_default.html.haml +++ b/app/views/shared/issuable/_label_page_default.html.haml @@ -3,6 +3,7 @@ - show_footer = local_assigns.fetch(:show_footer, true) - filter_placeholder = local_assigns.fetch(:filter_placeholder, 'Search') - show_boards_content = local_assigns.fetch(:show_boards_content, false) +- subject = @project || @group .dropdown-page-one = dropdown_title(title) - if show_boards_content @@ -17,11 +18,11 @@ - if can?(current_user, :admin_label, current_board_parent) %li %a.dropdown-toggle-page{ href: "#" } - = _('Create new label') + = create_label_title(subject) %li = link_to labels_path, :"data-is-link" => true do - if show_create && can?(current_user, :admin_label, current_board_parent) - = _('Manage labels') + = manage_labels_title(subject) - else - = _('View labels') + = view_labels_title(subject) = dropdown_loading diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index fabb17c7340..fc6f71ef60f 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -112,6 +112,7 @@ - if can?(current_user, :admin_label, board.parent) = render partial: "shared/issuable/label_page_create" = dropdown_loading - #js-add-issues-btn.prepend-left-10 + - if @project + #js-add-issues-btn.prepend-left-10{ data: { can_admin_list: can?(current_user, :admin_list, @project) } } - elsif type != :boards_modal = render 'shared/sort_dropdown' diff --git a/app/views/shared/issuable/form/_contribution.html.haml b/app/views/shared/issuable/form/_contribution.html.haml new file mode 100644 index 00000000000..de508278d7c --- /dev/null +++ b/app/views/shared/issuable/form/_contribution.html.haml @@ -0,0 +1,20 @@ +- issuable = local_assigns.fetch(:issuable) +- form = local_assigns.fetch(:form) + +- return unless issuable.is_a?(MergeRequest) +- return unless issuable.for_fork? +- return unless can?(current_user, :push_code, issuable.source_project) + +%hr + +.form-group + .control-label + = _('Contribution') + .col-sm-10 + .checkbox + = form.label :allow_maintainer_to_push do + = form.check_box :allow_maintainer_to_push, disabled: !issuable.can_allow_maintainer_to_push?(current_user) + = _('Allow edits from maintainers.') + = link_to 'About this feature', help_page_path('user/project/merge_requests/maintainer_access') + .help-block + = allow_maintainer_push_unavailable_reason(issuable) diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml index 129f6ab604e..eba64daaadc 100644 --- a/app/views/shared/milestones/_issuable.html.haml +++ b/app/views/shared/milestones/_issuable.html.haml @@ -12,7 +12,7 @@ - if show_project_name %strong #{project.name} · - elsif show_full_project_name - %strong #{project.name_with_namespace} · + %strong #{project.full_name} · - if issuable.is_a?(Issue) = confidential_icon(issuable) = link_to issuable.title, issuable_url_args, title: issuable.title diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml index e3b2b53833e..5926867e2d7 100644 --- a/app/views/shared/milestones/_milestone.html.haml +++ b/app/views/shared/milestones/_milestone.html.haml @@ -27,7 +27,7 @@ - milestone.milestones.each do |milestone| = link_to milestone_path(milestone) do %span.label.label-gray - = dashboard ? milestone.project.name_with_namespace : milestone.project.name + = dashboard ? milestone.project.full_name : milestone.project.name - if @group .col-sm-6.milestone-actions - if can?(current_user, :admin_milestones, @group) @@ -51,18 +51,25 @@ \ - if @project.group - = link_to promote_project_milestone_path(milestone.project, milestone), title: "Promote to Group Milestone", class: 'btn btn-xs btn-grouped', data: { confirm: "Promoting #{milestone.title} will make it available for all projects inside #{@project.group.name}. Existing project milestones with the same name will be merged. This action cannot be reversed.", toggle: "tooltip" }, method: :post do - Promote + %button.js-promote-project-milestone-button.btn.btn-xs.btn-grouped.has-tooltip{ title: _('Promote to Group Milestone'), + disabled: true, + type: 'button', + data: { url: promote_project_milestone_path(milestone.project, milestone), + milestone_title: milestone.title, + target: '#promote-milestone-modal', + container: 'body', + toggle: 'modal' } } + = _('Promote') = link_to 'Close Milestone', project_milestone_path(@project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-xs btn-close btn-grouped" - %button.js-delete-milestone-button.btn.btn-xs.btn-grouped.btn-danger{ data: { toggle: 'modal', - target: '#delete-milestone-modal', - milestone_id: milestone.id, - milestone_title: markdown_field(milestone, :title), - milestone_url: project_milestone_path(milestone.project, milestone), - milestone_issue_count: milestone.issues.count, - milestone_merge_request_count: milestone.merge_requests.count }, - disabled: true } - = _('Delete') - = icon('spin spinner', class: 'js-loading-icon hidden' ) + %button.js-delete-milestone-button.btn.btn-xs.btn-grouped.btn-danger{ data: { toggle: 'modal', + target: '#delete-milestone-modal', + milestone_id: milestone.id, + milestone_title: markdown_field(milestone, :title), + milestone_url: project_milestone_path(milestone.project, milestone), + milestone_issue_count: milestone.issues.count, + milestone_merge_request_count: milestone.merge_requests.count }, + disabled: true } + = _('Delete') + = icon('spin spinner', class: 'js-loading-icon hidden' ) diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml index fd0760d83a5..6006ab8b43f 100644 --- a/app/views/shared/milestones/_top.html.haml +++ b/app/views/shared/milestones/_top.html.haml @@ -56,7 +56,7 @@ - milestone.milestones.each do |ms| %tr %td - - project_name = group ? ms.project.name : ms.project.name_with_namespace + - project_name = group ? ms.project.name : ms.project.full_name = link_to project_name, project_milestone_path(ms.project, ms) %td = ms.issues_visible_to_user(current_user).opened.count diff --git a/app/views/shared/plugins/_index.html.haml b/app/views/shared/plugins/_index.html.haml new file mode 100644 index 00000000000..fc643c3ecc2 --- /dev/null +++ b/app/views/shared/plugins/_index.html.haml @@ -0,0 +1,23 @@ +- plugins = Gitlab::Plugin.files + +.row.prepend-top-default + .col-lg-4 + %h4.prepend-top-0 + Plugins + %p + #{link_to 'Plugins', help_page_path('administration/plugins')} are similar to + system hooks but are executed as files instead of sending data to a URL. + + .col-lg-8.append-bottom-default + - if plugins.any? + .panel.panel-default + .panel-heading + Plugins (#{plugins.count}) + %ul.content-list + - plugins.each do |file| + %li + .monospace + = File.basename(file) + - else + %p.light-well.text-center + No plugins found. diff --git a/app/views/shared/projects/_edit_information.html.haml b/app/views/shared/projects/_edit_information.html.haml new file mode 100644 index 00000000000..ec9dc8f62c2 --- /dev/null +++ b/app/views/shared/projects/_edit_information.html.haml @@ -0,0 +1,6 @@ +- unless can?(current_user, :push_code, @project) + .inline.prepend-left-10 + - if @project.branch_allows_maintainer_push?(current_user, selected_branch) + = commit_in_single_accessible_branch + - else + = commit_in_fork_help diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml index 491a8a41090..3acec88c2e3 100644 --- a/app/views/shared/snippets/_snippet.html.haml +++ b/app/views/shared/snippets/_snippet.html.haml @@ -31,7 +31,7 @@ %span.hidden-xs in = link_to project_path(snippet.project) do - = snippet.project.name_with_namespace + = snippet.project.full_name .pull-right.snippet-updated-at %span updated #{time_ago_with_tooltip(snippet.updated_at, placement: 'bottom')} diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 328db19be29..f65e8385ac8 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -43,12 +43,11 @@ - pipeline_cache:expire_pipeline_cache - pipeline_creation:create_pipeline - pipeline_creation:run_pipeline_schedule +- pipeline_background:archive_trace - pipeline_default:build_coverage - pipeline_default:build_trace_sections -- pipeline_default:create_trace_artifact - pipeline_default:pipeline_metrics - pipeline_default:pipeline_notification -- pipeline_default:update_head_pipeline_for_merge_request - pipeline_hooks:build_hooks - pipeline_hooks:pipeline_hooks - pipeline_processing:build_finished @@ -58,6 +57,7 @@ - pipeline_processing:pipeline_success - pipeline_processing:pipeline_update - pipeline_processing:stage_update +- pipeline_processing:update_head_pipeline_for_merge_request - repository_check:repository_check_clear - repository_check:repository_check_single_repository diff --git a/app/workers/archive_trace_worker.rb b/app/workers/archive_trace_worker.rb new file mode 100644 index 00000000000..dea7425ad88 --- /dev/null +++ b/app/workers/archive_trace_worker.rb @@ -0,0 +1,10 @@ +class ArchiveTraceWorker + include ApplicationWorker + include PipelineBackgroundQueue + + def perform(job_id) + Ci::Build.find_by(id: job_id).try do |job| + job.trace.archive! + end + end +end diff --git a/app/workers/build_finished_worker.rb b/app/workers/build_finished_worker.rb index b5ed8d607b3..46f1ac09915 100644 --- a/app/workers/build_finished_worker.rb +++ b/app/workers/build_finished_worker.rb @@ -12,7 +12,7 @@ class BuildFinishedWorker # We execute that async as this are two indepentent operations that can be executed after TraceSections and Coverage BuildHooksWorker.perform_async(build.id) - CreateTraceArtifactWorker.perform_async(build.id) + ArchiveTraceWorker.perform_async(build.id) end end end diff --git a/app/workers/concerns/gitlab/github_import/object_importer.rb b/app/workers/concerns/gitlab/github_import/object_importer.rb index 9a9fbaad653..100d86e38c8 100644 --- a/app/workers/concerns/gitlab/github_import/object_importer.rb +++ b/app/workers/concerns/gitlab/github_import/object_importer.rb @@ -22,7 +22,7 @@ module Gitlab importer_class.new(object, project, client).execute - counter.increment(project: project.path_with_namespace) + counter.increment(project: project.full_path) end def counter diff --git a/app/workers/concerns/pipeline_background_queue.rb b/app/workers/concerns/pipeline_background_queue.rb new file mode 100644 index 00000000000..8bf43de6b26 --- /dev/null +++ b/app/workers/concerns/pipeline_background_queue.rb @@ -0,0 +1,10 @@ +## +# Concern for setting Sidekiq settings for the low priority CI pipeline workers. +# +module PipelineBackgroundQueue + extend ActiveSupport::Concern + + included do + queue_namespace :pipeline_background + end +end diff --git a/app/workers/create_trace_artifact_worker.rb b/app/workers/create_trace_artifact_worker.rb deleted file mode 100644 index 11cda58021e..00000000000 --- a/app/workers/create_trace_artifact_worker.rb +++ /dev/null @@ -1,10 +0,0 @@ -class CreateTraceArtifactWorker - include ApplicationWorker - include PipelineQueue - - def perform(job_id) - Ci::Build.preload(:project, :user).find_by(id: job_id).try do |job| - Ci::CreateTraceArtifactService.new(job.project, job.user).execute(job) - end - end -end diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb index 21da27973fe..2a4d65b5cb3 100644 --- a/app/workers/emails_on_push_worker.rb +++ b/app/workers/emails_on_push_worker.rb @@ -66,7 +66,7 @@ class EmailsOnPushWorker # These are input errors and won't be corrected even if Sidekiq retries rescue Net::SMTPFatalError, Net::SMTPSyntaxError => e - logger.info("Failed to send e-mail for project '#{project.name_with_namespace}' to #{recipient}: #{e}") + logger.info("Failed to send e-mail for project '#{project.full_name}' to #{recipient}: #{e}") end end ensure diff --git a/app/workers/git_garbage_collect_worker.rb b/app/workers/git_garbage_collect_worker.rb index 7ba224d74c8..55fb817ca6e 100644 --- a/app/workers/git_garbage_collect_worker.rb +++ b/app/workers/git_garbage_collect_worker.rb @@ -44,6 +44,10 @@ class GitGarbageCollectWorker # Refresh the branch cache in case garbage collection caused a ref lookup to fail flush_ref_caches(project) if task == :gc + + # In case pack files are deleted, release libgit2 cache and open file + # descriptors ASAP instead of waiting for Ruby garbage collection + project.cleanup ensure cancel_lease(lease_key, lease_uuid) if lease_key.present? && lease_uuid.present? end diff --git a/app/workers/gitlab/github_import/stage/finish_import_worker.rb b/app/workers/gitlab/github_import/stage/finish_import_worker.rb index 073d6608082..a779e631516 100644 --- a/app/workers/gitlab/github_import/stage/finish_import_worker.rb +++ b/app/workers/gitlab/github_import/stage/finish_import_worker.rb @@ -16,7 +16,7 @@ module Gitlab def report_import_time(project) duration = Time.zone.now - project.created_at - path = project.path_with_namespace + path = project.full_path histogram.observe({ project: path }, duration) counter.increment diff --git a/app/workers/pages_worker.rb b/app/workers/pages_worker.rb index d3b95009364..66a0ff83bef 100644 --- a/app/workers/pages_worker.rb +++ b/app/workers/pages_worker.rb @@ -1,7 +1,7 @@ class PagesWorker include ApplicationWorker - sidekiq_options retry: false + sidekiq_options retry: 3 def perform(action, *arg) send(action, *arg) # rubocop:disable GitlabSecurity/PublicSend diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb index f2b2c4428d3..3909dbf7d7f 100644 --- a/app/workers/post_receive.rb +++ b/app/workers/post_receive.rb @@ -55,7 +55,7 @@ class PostReceive end def process_wiki_changes(post_received) - # Nothing defined here yet. + post_received.project.touch(:last_activity_at, :last_repository_updated_at) end def log(message) diff --git a/app/workers/process_commit_worker.rb b/app/workers/process_commit_worker.rb index 5b25d980bdb..201e7f332b4 100644 --- a/app/workers/process_commit_worker.rb +++ b/app/workers/process_commit_worker.rb @@ -30,10 +30,9 @@ class ProcessCommitWorker end def process_commit_message(project, commit, user, author, default = false) - # this is a GitLab generated commit message, ignore it. - return if commit.merged_merge_request?(user) - - closed_issues = default ? commit.closes_issues(user) : [] + # Ignore closing references from GitLab-generated commit messages. + find_closing_issues = default && !commit.merged_merge_request?(user) + closed_issues = find_closing_issues ? commit.closes_issues(user) : [] close_issues(project, user, author, commit, closed_issues) if closed_issues.any? commit.create_cross_references!(author, closed_issues) diff --git a/app/workers/project_export_worker.rb b/app/workers/project_export_worker.rb index c100852374a..0b502143e5d 100644 --- a/app/workers/project_export_worker.rb +++ b/app/workers/project_export_worker.rb @@ -4,10 +4,11 @@ class ProjectExportWorker sidekiq_options retry: 3 - def perform(current_user_id, project_id) + def perform(current_user_id, project_id, params = {}) + params = params.with_indifferent_access current_user = User.find(current_user_id) project = Project.find(project_id) - ::Projects::ImportExport::ExportService.new(project, current_user).execute + ::Projects::ImportExport::ExportService.new(project, current_user, params).execute end end diff --git a/app/workers/update_head_pipeline_for_merge_request_worker.rb b/app/workers/update_head_pipeline_for_merge_request_worker.rb index f09d89aa170..76f84ff920f 100644 --- a/app/workers/update_head_pipeline_for_merge_request_worker.rb +++ b/app/workers/update_head_pipeline_for_merge_request_worker.rb @@ -2,6 +2,8 @@ class UpdateHeadPipelineForMergeRequestWorker include ApplicationWorker include PipelineQueue + queue_namespace :pipeline_processing + def perform(merge_request_id) merge_request = MergeRequest.find(merge_request_id) pipeline = Ci::Pipeline.where(project: merge_request.source_project, ref: merge_request.source_branch).last |