diff options
Diffstat (limited to 'app/assets')
488 files changed, 10653 insertions, 8083 deletions
diff --git a/app/assets/images/icons.json b/app/assets/images/icons.json index 296cb856734..132a373baec 100644 --- a/app/assets/images/icons.json +++ b/app/assets/images/icons.json @@ -1 +1 @@ -{"iconCount":189,"spriteSize":85766,"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-open","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","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_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 +{"iconCount":189,"spriteSize":85900,"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","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 index 8d5426da19c..09efe331f93 100644 --- a/app/assets/images/icons.svg +++ b/app/assets/images/icons.svg @@ -1 +1 @@ -<?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-o-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="folder-open" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M14 6H5.333a2 2 0 0 0-1.664.89l-3.333 5a2 2 0 0 0-.285.662A3.017 3.017 0 0 1 0 12V3a2 2 0 0 1 2-2h2.838a3 3 0 0 1 2.828 2H11a3 3 0 0 1 3 3zM5.333 7h9.53a1 1 0 0 1 .875 1.486l-2.492 4.485A2 2 0 0 1 11.498 14H2a1 1 0 0 1-.832-1.555l3.333-5A1 1 0 0 1 5.333 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="fcfirst-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="fcsecond-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="fcthird-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="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="M11.828 3.536l1.415-1.415a1 1 0 1 1 1.414 1.415l-2.121 2.12a.997.997 0 0 1-1.415 0L9.707 4.244a1 1 0 0 1 1.414-1.415l.707.708zM2 3h5a1 1 0 1 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="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 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 +<?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="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/multi-editor_all_changes_committed_empty.svg b/app/assets/images/illustrations/multi-editor_all_changes_committed_empty.svg new file mode 100644 index 00000000000..06d73941c33 --- /dev/null +++ b/app/assets/images/illustrations/multi-editor_all_changes_committed_empty.svg @@ -0,0 +1 @@ +<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 new file mode 100644 index 00000000000..ebeea1f3dd9 --- /dev/null +++ b/app/assets/images/illustrations/multi-editor_no_changes_empty.svg @@ -0,0 +1 @@ +<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 new file mode 100644 index 00000000000..08321ef526b --- /dev/null +++ b/app/assets/images/illustrations/multi-editor_no_staged_files_empty.svg @@ -0,0 +1 @@ +<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/pending_job_empty.svg b/app/assets/images/illustrations/pending_job_empty.svg new file mode 100644 index 00000000000..8de695afa18 --- /dev/null +++ b/app/assets/images/illustrations/pending_job_empty.svg @@ -0,0 +1 @@ +<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/multi-editor-on.png b/app/assets/images/multi-editor-on.png Binary files differindex 2bcd29abf13..d51b68da985 100644 --- a/app/assets/images/multi-editor-on.png +++ b/app/assets/images/multi-editor-on.png diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 21d8c790e90..1f34c6b50c2 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -1,9 +1,9 @@ -import $ from 'jquery'; +import _ from 'underscore'; import axios from './lib/utils/axios_utils'; const Api = { groupsPath: '/api/:version/groups.json', - groupPath: '/api/:version/groups/:id.json', + groupPath: '/api/:version/groups/:id', namespacesPath: '/api/:version/namespaces.json', groupProjectsPath: '/api/:version/groups/:id/projects.json', projectsPath: '/api/:version/projects.json', @@ -23,42 +23,44 @@ const Api = { group(groupId, callback) { const url = Api.buildUrl(Api.groupPath) .replace(':id', groupId); - return $.ajax({ - url, - dataType: 'json', - }) - .done(group => callback(group)); + return axios.get(url) + .then(({ data }) => { + callback(data); + + return data; + }); }, // Return groups list. Filtered by query groups(query, options, callback) { const url = Api.buildUrl(Api.groupsPath); - return $.ajax({ - url, - data: Object.assign({ + return axios.get(url, { + params: Object.assign({ search: query, per_page: 20, }, options), - dataType: 'json', }) - .done(groups => callback(groups)); + .then(({ data }) => { + callback(data); + + return data; + }); }, // Return namespaces list. Filtered by query namespaces(query, callback) { const url = Api.buildUrl(Api.namespacesPath); - return $.ajax({ - url, - data: { + return axios.get(url, { + params: { search: query, per_page: 20, }, - dataType: 'json', - }).done(namespaces => callback(namespaces)); + }) + .then(({ data }) => callback(data)); }, // Return projects list. Filtered by query - projects(query, options, callback) { + projects(query, options, callback = _.noop) { const url = Api.buildUrl(Api.projectsPath); const defaults = { search: query, @@ -70,12 +72,14 @@ const Api = { defaults.membership = true; } - return $.ajax({ - url, - data: Object.assign(defaults, options), - dataType: 'json', + return axios.get(url, { + params: Object.assign(defaults, options), }) - .done(projects => callback(projects)); + .then(({ data }) => { + callback(data); + + return data; + }); }, // Return single project @@ -97,41 +101,34 @@ const Api = { url = Api.buildUrl(Api.groupLabelsPath).replace(':namespace_path', namespacePath); } - return $.ajax({ - url, - type: 'POST', - data: { label: data }, - dataType: 'json', + return axios.post(url, { + label: data, }) - .done(label => callback(label)) - .fail(message => callback(message.responseJSON)); + .then(res => callback(res.data)) + .catch(e => callback(e.response.data)); }, // Return group projects list. Filtered by query groupProjects(groupId, query, callback) { const url = Api.buildUrl(Api.groupProjectsPath) .replace(':id', groupId); - return $.ajax({ - url, - data: { + return axios.get(url, { + params: { search: query, per_page: 20, }, - dataType: 'json', }) - .done(projects => callback(projects)); + .then(({ data }) => callback(data)); }, commitMultiple(id, data) { // see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions const url = Api.buildUrl(Api.commitPath) .replace(':id', encodeURIComponent(id)); - return this.wrapAjaxCall({ - url, - type: 'POST', - contentType: 'application/json; charset=utf-8', - data: JSON.stringify(data), - dataType: 'json', + return axios.post(url, JSON.stringify(data), { + headers: { + 'Content-Type': 'application/json; charset=utf-8', + }, }); }, @@ -140,65 +137,57 @@ const Api = { .replace(':id', encodeURIComponent(id)) .replace(':branch', branch); - return this.wrapAjaxCall({ - url, - type: 'GET', - contentType: 'application/json; charset=utf-8', - dataType: 'json', - }); + return axios.get(url); }, // Return text for a specific license licenseText(key, data, callback) { const url = Api.buildUrl(Api.licensePath) .replace(':key', key); - return $.ajax({ - url, - data, + return axios.get(url, { + params: data, }) - .done(license => callback(license)); + .then(res => callback(res.data)); }, gitignoreText(key, callback) { const url = Api.buildUrl(Api.gitignorePath) .replace(':key', key); - return $.get(url, gitignore => callback(gitignore)); + return axios.get(url) + .then(({ data }) => callback(data)); }, gitlabCiYml(key, callback) { const url = Api.buildUrl(Api.gitlabCiYmlPath) .replace(':key', key); - return $.get(url, file => callback(file)); + return axios.get(url) + .then(({ data }) => callback(data)); }, dockerfileYml(key, callback) { const url = Api.buildUrl(Api.dockerfilePath).replace(':key', key); - $.get(url, callback); + return axios.get(url) + .then(({ data }) => callback(data)); }, issueTemplate(namespacePath, projectPath, key, type, callback) { const url = Api.buildUrl(Api.issuableTemplatePath) - .replace(':key', key) + .replace(':key', encodeURIComponent(key)) .replace(':type', type) .replace(':project_path', projectPath) .replace(':namespace_path', namespacePath); - $.ajax({ - url, - dataType: 'json', - }) - .done(file => callback(null, file)) - .fail(callback); + return axios.get(url) + .then(({ data }) => callback(null, data)) + .catch(callback); }, users(query, options) { const url = Api.buildUrl(this.usersPath); - return Api.wrapAjaxCall({ - url, - data: Object.assign({ + return axios.get(url, { + params: Object.assign({ search: query, per_page: 20, }, options), - dataType: 'json', }); }, @@ -209,20 +198,6 @@ const Api = { } return urlRoot + url.replace(':version', gon.api_version); }, - - wrapAjaxCall(options) { - return new Promise((resolve, reject) => { - // jQuery 2 is not Promises/A+ compatible (missing catch) - $.ajax(options) // eslint-disable-line promise/catch-or-return - .then(data => resolve(data), - (jqXHR, textStatus, errorThrown) => { - const error = new Error(`${options.url}: ${errorThrown}`); - error.textStatus = textStatus; - reject(error); - }, - ); - }); - }, }; export default Api; diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index 622764107ad..87109a802e5 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -1,8 +1,10 @@ /* eslint-disable class-methods-use-this */ import _ from 'underscore'; import Cookies from 'js-cookie'; +import { __ } from './locale'; import { isInIssuePage, updateTooltipTitle } from './lib/utils/common_utils'; -import Flash from './flash'; +import flash from './flash'; +import axios from './lib/utils/axios_utils'; const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd'; const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd'; @@ -441,13 +443,15 @@ class AwardsHandler { if (this.isUserAuthored($emojiButton)) { this.userAuthored($emojiButton); } else { - $.post(awardUrl, { + axios.post(awardUrl, { name: emoji, - }, (data) => { + }) + .then(({ data }) => { if (data.ok) { callback(); } - }).fail(() => new Flash('Something went wrong on our end.')); + }) + .catch(() => flash(__('Something went wrong on our end.'))); } } diff --git a/app/assets/javascripts/behaviors/copy_as_gfm.js b/app/assets/javascripts/behaviors/copy_as_gfm.js index c6eca72c51b..ffe90595b5d 100644 --- a/app/assets/javascripts/behaviors/copy_as_gfm.js +++ b/app/assets/javascripts/behaviors/copy_as_gfm.js @@ -299,6 +299,13 @@ const gfmRules = { export class CopyAsGFM { constructor() { + // iOS currently does not support clipboardData.setData(). This bug should + // be fixed in iOS 12, but for now we'll disable this for all iOS browsers + // ref: https://trac.webkit.org/changeset/222228/webkit + const userAgent = (typeof navigator !== 'undefined' && navigator.userAgent) || ''; + const isIOS = /\b(iPad|iPhone|iPod)(?=;)/.test(userAgent); + if (isIOS) return; + $(document).on('copy', '.md, .wiki', (e) => { CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformGFMSelection); }); $(document).on('copy', 'pre.code.highlight, .diff-content .line_content', (e) => { CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformCodeSelection); }); $(document).on('paste', '.js-gfm-input', CopyAsGFM.pasteGFM); diff --git a/app/assets/javascripts/behaviors/index.js b/app/assets/javascripts/behaviors/index.js index 34e905222b4..8d021de7998 100644 --- a/app/assets/javascripts/behaviors/index.js +++ b/app/assets/javascripts/behaviors/index.js @@ -7,6 +7,7 @@ import installGlEmojiElement from './gl_emoji'; import './quick_submit'; import './requires_input'; import './toggler_behavior'; +import '../preview_markdown'; installGlEmojiElement(); initCopyAsGFM(); diff --git a/app/assets/javascripts/behaviors/secret_values.js b/app/assets/javascripts/behaviors/secret_values.js index 1cf0b960eb0..7f70fce913a 100644 --- a/app/assets/javascripts/behaviors/secret_values.js +++ b/app/assets/javascripts/behaviors/secret_values.js @@ -2,18 +2,19 @@ import { n__ } from '../locale'; import { convertPermissionToBoolean } from '../lib/utils/common_utils'; export default class SecretValues { - constructor(container) { + constructor({ + container, + valueSelector = '.js-secret-value', + placeholderSelector = '.js-secret-value-placeholder', + }) { this.container = container; + this.valueSelector = valueSelector; + this.placeholderSelector = placeholderSelector; } init() { - this.values = this.container.querySelectorAll('.js-secret-value'); - this.placeholders = this.container.querySelectorAll('.js-secret-value-placeholder'); this.revealButton = this.container.querySelector('.js-secret-value-reveal-button'); - this.revealText = n__('Reveal value', 'Reveal values', this.values.length); - this.hideText = n__('Hide value', 'Hide values', this.values.length); - const isRevealed = convertPermissionToBoolean(this.revealButton.dataset.secretRevealStatus); this.updateDom(isRevealed); @@ -28,15 +29,17 @@ export default class SecretValues { } updateDom(isRevealed) { - this.values.forEach((value) => { + const values = this.container.querySelectorAll(this.valueSelector); + values.forEach((value) => { value.classList.toggle('hide', !isRevealed); }); - this.placeholders.forEach((placeholder) => { + const placeholders = this.container.querySelectorAll(this.placeholderSelector); + placeholders.forEach((placeholder) => { placeholder.classList.toggle('hide', isRevealed); }); - this.revealButton.textContent = isRevealed ? this.hideText : this.revealText; + this.revealButton.textContent = isRevealed ? n__('Hide value', 'Hide values', values.length) : n__('Reveal value', 'Reveal values', values.length); this.revealButton.dataset.secretRevealStatus = isRevealed; } } diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js index f7ae6f1cd12..83cac896f86 100644 --- a/app/assets/javascripts/blob/blob_file_dropzone.js +++ b/app/assets/javascripts/blob/blob_file_dropzone.js @@ -4,6 +4,8 @@ import { visitUrl } from '../lib/utils/url_utility'; import { HIDDEN_CLASS } from '../lib/utils/constants'; import csrf from '../lib/utils/csrf'; +Dropzone.autoDiscover = false; + function toggleLoading($el, $icon, loading) { if (loading) { $el.disable(); diff --git a/app/assets/javascripts/blob/notebook/index.js b/app/assets/javascripts/blob/notebook/index.js index 57b031956e8..6f1350e80fc 100644 --- a/app/assets/javascripts/blob/notebook/index.js +++ b/app/assets/javascripts/blob/notebook/index.js @@ -8,6 +8,9 @@ export default () => { new Vue({ el, + components: { + notebookLab, + }, data() { return { error: false, @@ -16,8 +19,41 @@ export default () => { json: {}, }; }, - components: { - notebookLab, + mounted() { + if (gon.katex_css_url) { + const katexStyles = document.createElement('link'); + katexStyles.setAttribute('rel', 'stylesheet'); + katexStyles.setAttribute('href', gon.katex_css_url); + document.head.appendChild(katexStyles); + } + + if (gon.katex_js_url) { + const katexScript = document.createElement('script'); + katexScript.addEventListener('load', () => { + this.loadFile(); + }); + katexScript.setAttribute('src', gon.katex_js_url); + document.head.appendChild(katexScript); + } else { + this.loadFile(); + } + }, + methods: { + loadFile() { + axios.get(el.dataset.endpoint) + .then(res => res.data) + .then((data) => { + this.json = data; + this.loading = false; + }) + .catch((e) => { + if (e.status !== 200) { + this.loadError = true; + } + + this.error = true; + }); + }, }, template: ` <div class="container-fluid md prepend-top-default append-bottom-default"> @@ -46,41 +82,5 @@ export default () => { </p> </div> `, - methods: { - loadFile() { - axios.get(el.dataset.endpoint) - .then(res => res.data) - .then((data) => { - this.json = data; - this.loading = false; - }) - .catch((e) => { - if (e.status !== 200) { - this.loadError = true; - } - - this.error = true; - }); - }, - }, - mounted() { - if (gon.katex_css_url) { - const katexStyles = document.createElement('link'); - katexStyles.setAttribute('rel', 'stylesheet'); - katexStyles.setAttribute('href', gon.katex_css_url); - document.head.appendChild(katexStyles); - } - - if (gon.katex_js_url) { - const katexScript = document.createElement('script'); - katexScript.addEventListener('load', () => { - this.loadFile(); - }); - katexScript.setAttribute('src', gon.katex_js_url); - document.head.appendChild(katexScript); - } else { - this.loadFile(); - } - }, }); }; diff --git a/app/assets/javascripts/blob/pdf/index.js b/app/assets/javascripts/blob/pdf/index.js index 7109f356540..70136cc4087 100644 --- a/app/assets/javascripts/blob/pdf/index.js +++ b/app/assets/javascripts/blob/pdf/index.js @@ -7,6 +7,9 @@ export default () => { return new Vue({ el, + components: { + pdfLab, + }, data() { return { error: false, @@ -15,9 +18,6 @@ export default () => { pdf: el.dataset.endpoint, }; }, - components: { - pdfLab, - }, methods: { onLoad() { this.loading = false; diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js index 54132e8537b..612f604e725 100644 --- a/app/assets/javascripts/blob/viewer/index.js +++ b/app/assets/javascripts/blob/viewer/index.js @@ -1,5 +1,6 @@ import Flash from '../../flash'; import { handleLocationHash } from '../../lib/utils/common_utils'; +import axios from '../../lib/utils/axios_utils'; export default class BlobViewer { constructor() { @@ -127,25 +128,18 @@ export default class BlobViewer { const viewer = viewerParam; const url = viewer.getAttribute('data-url'); - return new Promise((resolve, reject) => { - if (!url || viewer.getAttribute('data-loaded') || viewer.getAttribute('data-loading')) { - resolve(viewer); - return; - } + if (!url || viewer.getAttribute('data-loaded') || viewer.getAttribute('data-loading')) { + return Promise.resolve(viewer); + } - viewer.setAttribute('data-loading', 'true'); + viewer.setAttribute('data-loading', 'true'); - $.ajax({ - url, - dataType: 'JSON', - }) - .fail(reject) - .done((data) => { + return axios.get(url) + .then(({ data }) => { viewer.innerHTML = data.html; viewer.setAttribute('data-loaded', 'true'); - resolve(viewer); + return viewer; }); - }); } } diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js index b37988a674d..a25f7fb3dcd 100644 --- a/app/assets/javascripts/blob_edit/edit_blob.js +++ b/app/assets/javascripts/blob_edit/edit_blob.js @@ -1,5 +1,8 @@ /* global ace */ +import axios from '~/lib/utils/axios_utils'; +import createFlash from '~/flash'; +import { __ } from '~/locale'; import TemplateSelectorMediator from '../blob/file_template_mediator'; export default class EditBlob { @@ -56,12 +59,14 @@ export default class EditBlob { if (paneId === '#preview') { this.$toggleButton.hide(); - return $.post(currentLink.data('preview-url'), { + axios.post(currentLink.data('preview-url'), { content: this.editor.getValue(), - }, (response) => { - currentPane.empty().append(response); - return currentPane.renderGFM(); - }); + }) + .then(({ data }) => { + currentPane.empty().append(data); + currentPane.renderGFM(); + }) + .catch(() => createFlash(__('An error occurred previewing the blob'))); } this.$toggleButton.show(); diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js index 679c883cdcf..90166b3d3d1 100644 --- a/app/assets/javascripts/boards/boards_bundle.js +++ b/app/assets/javascripts/boards/boards_bundle.js @@ -171,19 +171,14 @@ $(() => { }); gl.IssueBoardsModalAddBtn = new Vue({ - mixins: [gl.issueBoards.ModalMixins], el: document.getElementById('js-add-issues-btn'), + mixins: [gl.issueBoards.ModalMixins], data() { return { modal: ModalStore.store, store: Store.state, }; }, - watch: { - disabled() { - this.updateTooltip(); - }, - }, computed: { disabled() { if (!this.store) { @@ -199,6 +194,14 @@ $(() => { return ''; }, }, + watch: { + disabled() { + this.updateTooltip(); + }, + }, + mounted() { + this.updateTooltip(); + }, methods: { updateTooltip() { const $tooltip = $(this.$refs.addIssuesButton); @@ -217,9 +220,6 @@ $(() => { } }, }, - mounted() { - this.updateTooltip(); - }, template: ` <div class="board-extra-actions"> <button diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js index adb7360327c..a8dafd31f12 100644 --- a/app/assets/javascripts/boards/components/board.js +++ b/app/assets/javascripts/boards/components/board.js @@ -1,5 +1,5 @@ /* eslint-disable comma-dangle, space-before-function-paren, one-var */ -/* global Sortable */ +import Sortable from 'vendor/Sortable'; import Vue from 'vue'; import AccessorUtilities from '../../lib/utils/accessor'; import boardList from './board_list'; diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue index 0b220a56e0b..23fec503586 100644 --- a/app/assets/javascripts/boards/components/board_card.vue +++ b/app/assets/javascripts/boards/components/board_card.vue @@ -10,12 +10,30 @@ export default { 'issue-card-inner': gl.issueBoards.IssueCardInner, }, props: { - list: Object, - issue: Object, - issueLinkBase: String, - disabled: Boolean, - index: Number, - rootPath: String, + list: { + type: Object, + default: () => ({}), + }, + issue: { + type: Object, + default: () => ({}), + }, + issueLinkBase: { + type: String, + default: '', + }, + disabled: { + type: Boolean, + default: false, + }, + index: { + type: Number, + default: 0, + }, + rootPath: { + type: String, + default: '', + }, }, data() { return { @@ -54,8 +72,13 @@ export default { </script> <template> - <li class="card" - :class="{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, 'is-active': issueDetailVisible }" + <li + class="card" + :class="{ + 'user-can-drag': !disabled && issue.id, + 'is-disabled': disabled || !issue.id, + 'is-active': issueDetailVisible + }" :index="index" :data-issue-id="issue.id" @mousedown="mouseDown" @@ -66,6 +89,7 @@ export default { :issue="issue" :issue-link-base="issueLinkBase" :root-path="rootPath" - :update-filters="true" /> + :update-filters="true" + /> </li> </template> diff --git a/app/assets/javascripts/boards/components/board_list.js b/app/assets/javascripts/boards/components/board_list.js index 84b76a6f1b1..591f1dc8313 100644 --- a/app/assets/javascripts/boards/components/board_list.js +++ b/app/assets/javascripts/boards/components/board_list.js @@ -1,4 +1,4 @@ -/* global Sortable */ +import Sortable from 'vendor/Sortable'; import boardNewIssue from './board_new_issue'; import boardCard from './board_card.vue'; import eventHub from '../eventhub'; @@ -187,7 +187,7 @@ export default { <li class="board-list-count text-center" v-if="showCount" - data-id="-1"> + data-issue-id="-1"> <loading-icon v-show="list.loadingMore" diff --git a/app/assets/javascripts/boards/utils/query_data.js b/app/assets/javascripts/boards/utils/query_data.js index 2cd3c146f11..65315979df7 100644 --- a/app/assets/javascripts/boards/utils/query_data.js +++ b/app/assets/javascripts/boards/utils/query_data.js @@ -5,7 +5,7 @@ export default (path, extraData) => path.split('&').reduce((dataParam, filterPar const paramSplit = filterParam.split('='); const paramKeyNormalized = paramSplit[0].replace('[]', ''); const isArray = paramSplit[0].indexOf('[]'); - const value = decodeURIComponent(paramSplit[1]).replace(/\+/g, ' '); + const value = decodeURIComponent(paramSplit[1].replace(/\+/g, ' ')); if (isArray !== -1) { if (!data[paramKeyNormalized]) { diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js index 637d0dbde23..4dddb6eb0d6 100644 --- a/app/assets/javascripts/clusters/clusters_bundle.js +++ b/app/assets/javascripts/clusters/clusters_bundle.js @@ -14,6 +14,7 @@ import { import ClustersService from './services/clusters_service'; import ClustersStore from './stores/clusters_store'; import applications from './components/applications.vue'; +import setupToggleButtons from '../toggle_buttons'; /** * Cluster page has 2 separate parts: @@ -48,12 +49,9 @@ export default class Clusters { installPrometheusEndpoint: installPrometheusPath, }); - this.toggle = this.toggle.bind(this); this.installApplication = this.installApplication.bind(this); this.showToken = this.showToken.bind(this); - this.toggleButton = document.querySelector('.js-toggle-cluster'); - this.toggleInput = document.querySelector('.js-toggle-input'); this.errorContainer = document.querySelector('.js-cluster-error'); this.successContainer = document.querySelector('.js-cluster-success'); this.creatingContainer = document.querySelector('.js-cluster-creating'); @@ -63,6 +61,7 @@ export default class Clusters { this.tokenField = document.querySelector('.js-cluster-token'); initSettingsPanels(); + setupToggleButtons(document.querySelector('.js-cluster-enable-toggle-area')); this.initApplications(); if (this.store.state.status !== 'created') { @@ -101,13 +100,11 @@ export default class Clusters { } addListeners() { - this.toggleButton.addEventListener('click', this.toggle); if (this.showTokenButton) this.showTokenButton.addEventListener('click', this.showToken); eventHub.$on('installApplication', this.installApplication); } removeListeners() { - this.toggleButton.removeEventListener('click', this.toggle); if (this.showTokenButton) this.showTokenButton.removeEventListener('click', this.showToken); eventHub.$off('installApplication', this.installApplication); } @@ -151,11 +148,6 @@ export default class Clusters { this.updateContainer(prevStatus, this.store.state.status, this.store.state.statusReason); } - toggle() { - this.toggleButton.classList.toggle('is-checked'); - this.toggleInput.setAttribute('value', this.toggleButton.classList.contains('is-checked').toString()); - } - showToken() { const type = this.tokenField.getAttribute('type'); diff --git a/app/assets/javascripts/clusters/clusters_index.js b/app/assets/javascripts/clusters/clusters_index.js index 6844d1dbd83..2e3ad244375 100644 --- a/app/assets/javascripts/clusters/clusters_index.js +++ b/app/assets/javascripts/clusters/clusters_index.js @@ -1,58 +1,20 @@ import Flash from '../flash'; import { s__ } from '../locale'; +import setupToggleButtons from '../toggle_buttons'; import ClustersService from './services/clusters_service'; -/** - * Toggles loading and disabled classes. - * @param {HTMLElement} button - */ -const toggleLoadingButton = (button) => { - if (button.getAttribute('disabled')) { - button.removeAttribute('disabled'); - } else { - button.setAttribute('disabled', true); - } - - button.classList.toggle('is-loading'); -}; -/** - * Toggles checked class for the given button - * @param {HTMLElement} button - */ -const toggleValue = (button) => { - button.classList.toggle('is-checked'); +export default () => { + const clusterList = document.querySelector('.js-clusters-list'); + // The empty state won't have a clusterList + if (clusterList) { + setupToggleButtons( + document.querySelector('.js-clusters-list'), + (value, toggle) => + ClustersService.updateCluster(toggle.dataset.endpoint, { cluster: { enabled: value } }) + .catch((err) => { + Flash(s__('ClusterIntegration|Something went wrong on our end.')); + throw err; + }), + ); + } }; - -/** - * Handles toggle buttons in the cluster's table. - * - * When the user clicks the toggle button for each cluster, it: - * - toggles the button - * - shows a loading and disables button - * - Makes a put request to the given endpoint - * Once we receive the response, either: - * 1) Show updated status in case of successfull response - * 2) Show initial status in case of failed response - */ -export default function setClusterTableToggles() { - document.querySelectorAll('.js-toggle-cluster-list') - .forEach(button => button.addEventListener('click', (e) => { - const toggleButton = e.currentTarget; - const endpoint = toggleButton.getAttribute('data-endpoint'); - - toggleValue(toggleButton); - toggleLoadingButton(toggleButton); - - const value = toggleButton.classList.contains('is-checked'); - - ClustersService.updateCluster(endpoint, { cluster: { enabled: value } }) - .then(() => { - toggleLoadingButton(toggleButton); - }) - .catch(() => { - toggleLoadingButton(toggleButton); - toggleValue(toggleButton); - Flash(s__('ClusterIntegration|Something went wrong on our end.')); - }); - })); -} diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue index 872abf03ef1..c13bbcee863 100644 --- a/app/assets/javascripts/clusters/components/application_row.vue +++ b/app/assets/javascripts/clusters/components/application_row.vue @@ -1,108 +1,112 @@ <script> -import { s__, sprintf } from '../../locale'; -import eventHub from '../event_hub'; -import loadingButton from '../../vue_shared/components/loading_button.vue'; -import { - APPLICATION_NOT_INSTALLABLE, - APPLICATION_SCHEDULED, - APPLICATION_INSTALLABLE, - APPLICATION_INSTALLING, - APPLICATION_INSTALLED, - APPLICATION_ERROR, - REQUEST_LOADING, - REQUEST_SUCCESS, - REQUEST_FAILURE, -} from '../constants'; + /* eslint-disable vue/require-default-prop */ + import { s__, sprintf } from '../../locale'; + import eventHub from '../event_hub'; + import loadingButton from '../../vue_shared/components/loading_button.vue'; + import { + APPLICATION_NOT_INSTALLABLE, + APPLICATION_SCHEDULED, + APPLICATION_INSTALLABLE, + APPLICATION_INSTALLING, + APPLICATION_INSTALLED, + APPLICATION_ERROR, + REQUEST_LOADING, + REQUEST_SUCCESS, + REQUEST_FAILURE, + } from '../constants'; -export default { - props: { - id: { - type: String, - required: true, + export default { + components: { + loadingButton, }, - title: { - type: String, - required: true, + props: { + id: { + type: String, + required: true, + }, + title: { + type: String, + required: true, + }, + titleLink: { + type: String, + required: false, + }, + description: { + type: String, + required: true, + }, + status: { + type: String, + required: false, + }, + statusReason: { + type: String, + required: false, + }, + requestStatus: { + type: String, + required: false, + }, + requestReason: { + type: String, + required: false, + }, }, - titleLink: { - type: String, - required: false, - }, - description: { - type: String, - required: true, - }, - status: { - type: String, - required: false, - }, - statusReason: { - type: String, - required: false, - }, - requestStatus: { - type: String, - required: false, - }, - requestReason: { - type: String, - required: false, - }, - }, - components: { - loadingButton, - }, - computed: { - rowJsClass() { - return `js-cluster-application-row-${this.id}`; - }, - installButtonLoading() { - return !this.status || - this.status === APPLICATION_SCHEDULED || - this.status === APPLICATION_INSTALLING || - this.requestStatus === REQUEST_LOADING; - }, - installButtonDisabled() { - // Avoid the potential for the real-time data to say APPLICATION_INSTALLABLE but - // we already made a request to install and are just waiting for the real-time - // to sync up. - return (this.status !== APPLICATION_INSTALLABLE && this.status !== APPLICATION_ERROR) || - this.requestStatus === REQUEST_LOADING || - this.requestStatus === REQUEST_SUCCESS; - }, - installButtonLabel() { - let label; - if ( - this.status === APPLICATION_NOT_INSTALLABLE || - this.status === APPLICATION_INSTALLABLE || - this.status === APPLICATION_ERROR - ) { - label = s__('ClusterIntegration|Install'); - } else if (this.status === APPLICATION_SCHEDULED || this.status === APPLICATION_INSTALLING) { - label = s__('ClusterIntegration|Installing'); - } else if (this.status === APPLICATION_INSTALLED) { - label = s__('ClusterIntegration|Installed'); - } + computed: { + rowJsClass() { + return `js-cluster-application-row-${this.id}`; + }, + installButtonLoading() { + return !this.status || + this.status === APPLICATION_SCHEDULED || + this.status === APPLICATION_INSTALLING || + this.requestStatus === REQUEST_LOADING; + }, + installButtonDisabled() { + // Avoid the potential for the real-time data to say APPLICATION_INSTALLABLE but + // we already made a request to install and are just waiting for the real-time + // to sync up. + return (this.status !== APPLICATION_INSTALLABLE + && this.status !== APPLICATION_ERROR) || + this.requestStatus === REQUEST_LOADING || + this.requestStatus === REQUEST_SUCCESS; + }, + installButtonLabel() { + let label; + if ( + this.status === APPLICATION_NOT_INSTALLABLE || + this.status === APPLICATION_INSTALLABLE || + this.status === APPLICATION_ERROR + ) { + label = s__('ClusterIntegration|Install'); + } else if (this.status === APPLICATION_SCHEDULED || + this.status === APPLICATION_INSTALLING) { + label = s__('ClusterIntegration|Installing'); + } else if (this.status === APPLICATION_INSTALLED) { + label = s__('ClusterIntegration|Installed'); + } - return label; - }, - hasError() { - return this.status === APPLICATION_ERROR || this.requestStatus === REQUEST_FAILURE; - }, - generalErrorDescription() { - return sprintf( - s__('ClusterIntegration|Something went wrong while installing %{title}'), { - title: this.title, - }, - ); + return label; + }, + hasError() { + return this.status === APPLICATION_ERROR || + this.requestStatus === REQUEST_FAILURE; + }, + generalErrorDescription() { + return sprintf( + s__('ClusterIntegration|Something went wrong while installing %{title}'), { + title: this.title, + }, + ); + }, }, - }, - methods: { - installClicked() { - eventHub.$emit('installApplication', this.id); + methods: { + installClicked() { + eventHub.$emit('installApplication', this.id); + }, }, - }, -}; + }; </script> <template> diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue index cd58b88db69..ff2e0768a87 100644 --- a/app/assets/javascripts/clusters/components/applications.vue +++ b/app/assets/javascripts/clusters/components/applications.vue @@ -1,84 +1,93 @@ <script> -import _ from 'underscore'; -import { s__, sprintf } from '../../locale'; -import applicationRow from './application_row.vue'; + import _ from 'underscore'; + import { s__, sprintf } from '../../locale'; + import applicationRow from './application_row.vue'; -export default { - props: { - applications: { - type: Object, - required: false, - default: () => ({}), + export default { + components: { + applicationRow, }, - helpPath: { - type: String, - required: false, + props: { + applications: { + type: Object, + required: false, + default: () => ({}), + }, + helpPath: { + type: String, + required: false, + default: '', + }, }, - }, - components: { - applicationRow, - }, - computed: { - generalApplicationDescription() { - return sprintf( - _.escape(s__('ClusterIntegration|Install applications on your cluster. Read more about %{helpLink}')), { - helpLink: `<a href="${this.helpPath}"> - ${_.escape(s__('ClusterIntegration|installing applications'))} - </a>`, - }, - false, - ); - }, - helmTillerDescription() { - return _.escape(s__( - `ClusterIntegration|Helm streamlines installing and managing Kubernets applications. - Tiller runs inside of your Kubernetes Cluster, and manages - releases of your charts.`, - )); - }, - ingressDescription() { - const descriptionParagraph = _.escape(s__( - `ClusterIntegration|Ingress gives you a way to route requests to services based on the - request host or path, centralizing a number of services into a single entrypoint.`, - )); + computed: { + generalApplicationDescription() { + return sprintf( + _.escape(s__(`ClusterIntegration|Install applications on your cluster. + Read more about %{helpLink}`)), + { + helpLink: `<a href="${this.helpPath}"> + ${_.escape(s__('ClusterIntegration|installing applications'))} + </a>`, + }, + false, + ); + }, + helmTillerDescription() { + return _.escape(s__( + `ClusterIntegration|Helm streamlines installing and managing Kubernets applications. + Tiller runs inside of your Kubernetes Cluster, and manages + releases of your charts.`, + )); + }, + ingressDescription() { + const descriptionParagraph = _.escape(s__( + `ClusterIntegration|Ingress gives you a way to route requests to services based on the + request host or path, centralizing a number of services into a single entrypoint.`, + )); - const extraCostParagraph = sprintf( - _.escape(s__('ClusterIntegration|%{boldNotice} This will add some extra resources like a load balancer, which incur additional costs. See %{pricingLink}')), { - boldNotice: `<strong>${_.escape(s__('ClusterIntegration|Note:'))}</strong>`, - pricingLink: `<a href="https://cloud.google.com/compute/pricing#lb" target="_blank" rel="noopener noreferrer"> - ${_.escape(s__('ClusterIntegration|GKE pricing'))} - </a>`, - }, - false, - ); + const extraCostParagraph = sprintf( + _.escape(s__( + `ClusterIntegration|%{boldNotice} This will add some extra resources + like a load balancer, which may incur additional costs depending on + the hosting provider Kubernetes is installed on. If you are using GKE, + you can %{pricingLink}.`, + )), { + boldNotice: `<strong>${_.escape(s__('ClusterIntegration|Note:'))}</strong>`, + pricingLink: `<a href="https://cloud.google.com/compute/pricing#lb" target="_blank" rel="noopener noreferrer"> + ${_.escape(s__('ClusterIntegration|check the pricing here'))}</a>`, + }, + false, + ); - return ` - <p> - ${descriptionParagraph} - </p> - <p class="append-bottom-0"> - ${extraCostParagraph} - </p> - `; - }, - gitlabRunnerDescription() { - return _.escape(s__( - `ClusterIntegration|GitLab Runner is the open source project that is used to run your jobs - and send the results back to GitLab.`, - )); - }, - prometheusDescription() { - return sprintf( - _.escape(s__('ClusterIntegration|Prometheus is an open-source monitoring system with %{gitlabIntegrationLink} to monitor deployed applications.')), { - gitlabIntegrationLink: `<a href="https://docs.gitlab.com/ce/user/project/integrations/prometheus.html", target="_blank" rel="noopener noreferrer"> - ${_.escape(s__('ClusterIntegration|Gitlab Integration'))} - </a>`, - }, - false, - ); + return ` + <p> + ${descriptionParagraph} + </p> + <p class="append-bottom-0"> + ${extraCostParagraph} + </p> + `; + }, + gitlabRunnerDescription() { + return _.escape(s__( + `ClusterIntegration|GitLab Runner is the open source project that is used to run your jobs + and send the results back to GitLab.`, + )); + }, + prometheusDescription() { + return sprintf( + _.escape(s__(`ClusterIntegration|Prometheus is an open-source monitoring system + with %{gitlabIntegrationLink} to monitor deployed applications.`)), + { + gitlabIntegrationLink: `<a href="https://docs.gitlab.com/ce/user/project/integrations/prometheus.html" +target="_blank" rel="noopener noreferrer"> + ${_.escape(s__('ClusterIntegration|GitLab Integration'))}</a>`, + }, + false, + ); + }, }, - }, -}; + }; </script> <template> @@ -107,26 +116,29 @@ export default { :request-reason="applications.helm.requestReason" /> <application-row - id="ingress" - :title="applications.ingress.title" - title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/" - :description="ingressDescription" - :status="applications.ingress.status" - :status-reason="applications.ingress.statusReason" - :request-status="applications.ingress.requestStatus" - :request-reason="applications.ingress.requestReason" - /> - <application-row - id="prometheus" - :title="applications.prometheus.title" - title-link="https://prometheus.io/docs/introduction/overview/" - :description="prometheusDescription" - :status="applications.prometheus.status" - :status-reason="applications.prometheus.statusReason" - :request-status="applications.prometheus.requestStatus" - :request-reason="applications.prometheus.requestReason" - /> - <!-- NOTE: Don't forget to update `clusters.scss` min-height for this block and uncomment `application_spec` tests --> + id="ingress" + :title="applications.ingress.title" + title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/" + :description="ingressDescription" + :status="applications.ingress.status" + :status-reason="applications.ingress.statusReason" + :request-status="applications.ingress.requestStatus" + :request-reason="applications.ingress.requestReason" + /> + <application-row + id="prometheus" + :title="applications.prometheus.title" + title-link="https://prometheus.io/docs/introduction/overview/" + :description="prometheusDescription" + :status="applications.prometheus.status" + :status-reason="applications.prometheus.statusReason" + :request-status="applications.prometheus.requestStatus" + :request-reason="applications.prometheus.requestReason" + /> + <!-- + NOTE: Don't forget to update `clusters.scss` + min-height for this block and uncomment `application_spec` tests + --> <!-- Add GitLab Runner row, all other plumbing is complete --> </div> </div> diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js index b6a0ece7907..525fbf9dac9 100644 --- a/app/assets/javascripts/commit/image_file.js +++ b/app/assets/javascripts/commit/image_file.js @@ -94,7 +94,7 @@ export default class ImageFile { }); return [maxWidth, maxHeight]; } - + // eslint-disable-next-line views = { 'two-up': function() { return $('.two-up.view .wrap', this.file).each((function(_this) { diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue index e9a0dbaa59d..da0e8063ccb 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue @@ -4,6 +4,10 @@ import pipelinesMixin from '../../pipelines/mixins/pipelines'; export default { + mixins: [ + pipelinesMixin, + ], + props: { endpoint: { type: String, @@ -31,9 +35,6 @@ default: 'child', }, }, - mixins: [ - pipelinesMixin, - ], data() { const store = new PipelineStore(); @@ -95,28 +96,29 @@ label="Loading pipelines" size="3" v-if="isLoading" - /> + /> <empty-state v-if="shouldRenderEmptyState" :help-page-path="helpPagePath" :empty-state-svg-path="emptyStateSvgPath" - /> + /> <error-state v-if="shouldRenderErrorState" :error-state-svg-path="errorStateSvgPath" - /> + /> <div class="table-holder" - v-if="shouldRenderTable"> + v-if="shouldRenderTable" + > <pipelines-table-component :pipelines="state.pipelines" :update-graph-dropdown="updateGraphDropdown" :auto-devops-help-path="autoDevopsHelpPath" :view-type="viewType" - /> + /> </div> </div> </template> diff --git a/app/assets/javascripts/commit_merge_requests.js b/app/assets/javascripts/commit_merge_requests.js new file mode 100644 index 00000000000..f76c9b7e690 --- /dev/null +++ b/app/assets/javascripts/commit_merge_requests.js @@ -0,0 +1,73 @@ +/* global Flash */ + +import axios from './lib/utils/axios_utils'; +import { n__, s__ } from './locale'; + +export function getHeaderText(childElementCount, mergeRequestCount) { + if (childElementCount === 0) { + return `${mergeRequestCount} ${n__('merge request', 'merge requests', mergeRequestCount)}`; + } + return ','; +} + +export function createHeader(childElementCount, mergeRequestCount) { + const headerText = getHeaderText(childElementCount, mergeRequestCount); + + return $('<span />', { + class: 'append-right-5', + text: headerText, + }); +} + +export function createLink(mergeRequest) { + return $('<a />', { + class: 'append-right-5', + href: mergeRequest.path, + text: `!${mergeRequest.iid}`, + }); +} + +export function createTitle(mergeRequest) { + return $('<span />', { + text: mergeRequest.title, + }); +} + +export function createItem(mergeRequest) { + const $item = $('<span />'); + const $link = createLink(mergeRequest); + const $title = createTitle(mergeRequest); + $item.append($link); + $item.append($title); + + return $item; +} + +export function createContent(mergeRequests) { + const $content = $('<span />'); + + if (mergeRequests.length === 0) { + $content.text(s__('Commits|No related merge requests found')); + } else { + mergeRequests.forEach((mergeRequest) => { + const $header = createHeader($content.children().length, mergeRequests.length); + const $item = createItem(mergeRequest); + $content.append($header); + $content.append($item); + }); + } + + return $content; +} + +export function fetchCommitMergeRequests() { + const $container = $('.merge-requests'); + + axios.get($container.data('projectCommitPath')) + .then((response) => { + const $content = createContent(response.data); + + $container.html($content); + }) + .catch(() => Flash(s__('Commits|An error occurred while fetching merge requests data.'))); +} diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js index 3a03cbf6b90..4b2f75fffde 100644 --- a/app/assets/javascripts/commits.js +++ b/app/assets/javascripts/commits.js @@ -5,6 +5,7 @@ import { pluralize } from './lib/utils/text_utility'; import { localTimeAgo } from './lib/utils/datetime_utility'; import Pager from './pager'; +import axios from './lib/utils/axios_utils'; export default (function () { const CommitsList = {}; @@ -43,29 +44,30 @@ export default (function () { CommitsList.filterResults = function () { const form = $('.commits-search-form'); const search = CommitsList.searchField.val(); - if (search === CommitsList.lastSearch) return; + if (search === CommitsList.lastSearch) return Promise.resolve(); const commitsUrl = form.attr('action') + '?' + form.serialize(); CommitsList.content.fadeTo('fast', 0.5); - return $.ajax({ - type: 'GET', - url: form.attr('action'), - data: form.serialize(), - complete: function () { - return CommitsList.content.fadeTo('fast', 1.0); - }, - success: function (data) { + const params = form.serializeArray().reduce((acc, obj) => Object.assign(acc, { + [obj.name]: obj.value, + }), {}); + + return axios.get(form.attr('action'), { + params, + }) + .then(({ data }) => { CommitsList.lastSearch = search; CommitsList.content.html(data.html); - return history.replaceState({ - page: commitsUrl, + CommitsList.content.fadeTo('fast', 1.0); + // Change url so if user reload a page - search results are saved + history.replaceState({ + page: commitsUrl, }, document.title, commitsUrl); - }, - error: function () { + }) + .catch(() => { + CommitsList.content.fadeTo('fast', 1.0); CommitsList.lastSearch = null; - }, - dataType: 'json', - }); + }); }; // Prepare loaded data. diff --git a/app/assets/javascripts/compare.js b/app/assets/javascripts/compare.js index 144caf1d278..e2a008e8904 100644 --- a/app/assets/javascripts/compare.js +++ b/app/assets/javascripts/compare.js @@ -1,5 +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 { localTimeAgo } from './lib/utils/datetime_utility'; +import axios from './lib/utils/axios_utils'; export default class Compare { constructor(opts) { @@ -41,17 +42,14 @@ export default class Compare { } getTargetProject() { - return $.ajax({ - url: this.opts.targetProjectUrl, - data: { - target_project_id: $("input[name='merge_request[target_project_id]']").val() - }, - beforeSend: function() { - return $('.mr_target_commit').empty(); + $('.mr_target_commit').empty(); + + return axios.get(this.opts.targetProjectUrl, { + params: { + target_project_id: $("input[name='merge_request[target_project_id]']").val(), }, - success: function(html) { - return $('.js-target-branch-dropdown .dropdown-content').html(html); - } + }).then(({ data }) => { + $('.js-target-branch-dropdown .dropdown-content').html(data); }); } @@ -68,22 +66,19 @@ export default class Compare { }); } - static sendAjax(url, loading, target, data) { - var $target; - $target = $(target); - return $.ajax({ - url: url, - data: data, - beforeSend: function() { - loading.show(); - return $target.empty(); - }, - success: function(html) { - loading.hide(); - $target.html(html); - var className = '.' + $target[0].className.replace(' ', '.'); - localTimeAgo($('.js-timeago', className)); - } + static sendAjax(url, loading, target, params) { + const $target = $(target); + + loading.show(); + $target.empty(); + + return axios.get(url, { + params, + }).then(({ data }) => { + loading.hide(); + $target.html(data); + const className = '.' + $target[0].className.replace(' ', '.'); + localTimeAgo($('.js-timeago', className)); }); } } diff --git a/app/assets/javascripts/compare_autocomplete.js b/app/assets/javascripts/compare_autocomplete.js index e633ef8a29e..59899e97be1 100644 --- a/app/assets/javascripts/compare_autocomplete.js +++ b/app/assets/javascripts/compare_autocomplete.js @@ -1,4 +1,7 @@ /* 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 './locale'; +import axios from './lib/utils/axios_utils'; +import flash from './flash'; export default function initCompareAutocomplete() { $('.js-compare-dropdown').each(function() { @@ -10,15 +13,14 @@ export default function initCompareAutocomplete() { const $filterInput = $('input[type="search"]', $dropdownContainer); $dropdown.glDropdown({ data: function(term, callback) { - return $.ajax({ - url: $dropdown.data('refs-url'), - data: { + axios.get($dropdown.data('refsUrl'), { + params: { ref: $dropdown.data('ref'), search: term, - } - }).done(function(refs) { - return callback(refs); - }); + }, + }).then(({ data }) => { + callback(data); + }).catch(() => flash(__('Error fetching refs'))); }, selectable: true, filterable: true, diff --git a/app/assets/javascripts/create_item_dropdown.js b/app/assets/javascripts/create_item_dropdown.js new file mode 100644 index 00000000000..42e9e568170 --- /dev/null +++ b/app/assets/javascripts/create_item_dropdown.js @@ -0,0 +1,119 @@ +import _ from 'underscore'; + +export default class CreateItemDropdown { + /** + * @param {Object} options containing + * `$dropdown` target element + * `onSelect` event callback + * $dropdown must be an element created using `dropdown_tag()` rails helper + */ + constructor(options) { + this.defaultToggleLabel = options.defaultToggleLabel; + this.fieldName = options.fieldName; + this.onSelect = options.onSelect || (() => {}); + this.getDataOption = options.getData; + this.createNewItemFromValueOption = options.createNewItemFromValue; + this.$dropdown = options.$dropdown; + this.$dropdownContainer = this.$dropdown.parent(); + this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer'); + this.$createButton = this.$dropdownContainer.find('.js-dropdown-create-new-item'); + + this.buildDropdown(); + this.bindEvents(); + + // Hide footer + this.toggleFooter(true); + } + + buildDropdown() { + this.$dropdown.glDropdown({ + data: this.getData.bind(this), + filterable: true, + remote: false, + search: { + fields: ['text'], + }, + selectable: true, + toggleLabel(selected) { + return (selected && 'id' in selected) ? _.escape(selected.title) : this.defaultToggleLabel; + }, + fieldName: this.fieldName, + text(item) { + return _.escape(item.text); + }, + id(item) { + return _.escape(item.id); + }, + onFilter: this.toggleCreateNewButton.bind(this), + clicked: (options) => { + options.e.preventDefault(); + this.onSelect(); + }, + }); + } + + clearDropdown() { + this.$dropdownContainer.find('.dropdown-content').html(''); + this.$dropdownContainer.find('.dropdown-input-field').val(''); + } + + bindEvents() { + this.$createButton.on('click', this.onClickCreateWildcard.bind(this)); + } + + onClickCreateWildcard(e) { + e.preventDefault(); + + this.refreshData(); + this.$dropdown.data('glDropdown').selectRowAtIndex(); + } + + refreshData() { + // Refresh the dropdown's data, which ends up calling `getData` + this.$dropdown.data('glDropdown').remote.execute(); + } + + getData(term, callback) { + this.getDataOption(term, (data = []) => { + // Ensure the selected item isn't already in the data to avoid duplicates + const alreadyHasSelectedItem = this.selectedItem && data.some(item => + item.id === this.selectedItem.id, + ); + + let uniqueData = data; + if (!alreadyHasSelectedItem) { + uniqueData = data.concat(this.selectedItem || []); + } + + callback(uniqueData); + }); + } + + createNewItemFromValue(newValue) { + if (this.createNewItemFromValueOption) { + return this.createNewItemFromValueOption(newValue); + } + + return { + title: newValue, + id: newValue, + text: newValue, + }; + } + + toggleCreateNewButton(newValue) { + if (newValue) { + this.selectedItem = this.createNewItemFromValue(newValue); + + this.$dropdownContainer + .find('.js-dropdown-create-new-item code') + .text(newValue); + } + + this.toggleFooter(!newValue); + } + + toggleFooter(toggleState) { + this.$dropdownFooter.toggleClass('hidden', toggleState); + } +} diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js index eedbd3feeb5..482d83621e2 100644 --- a/app/assets/javascripts/create_merge_request_dropdown.js +++ b/app/assets/javascripts/create_merge_request_dropdown.js @@ -1,4 +1,6 @@ /* eslint-disable no-new */ +import _ from 'underscore'; +import axios from './lib/utils/axios_utils'; import Flash from './flash'; import DropLab from './droplab/drop_lab'; import ISetter from './droplab/plugins/input_setter'; @@ -73,60 +75,52 @@ export default class CreateMergeRequestDropdown { } checkAbilityToCreateBranch() { - return $.ajax({ - type: 'GET', - dataType: 'json', - url: this.canCreatePath, - beforeSend: () => this.setUnavailableButtonState(), - }) - .done((data) => { - this.setUnavailableButtonState(false); - - if (data.can_create_branch) { - this.available(); - this.enable(); - - if (!this.droplabInitialized) { - this.droplabInitialized = true; - this.initDroplab(); - this.bindEvents(); + this.setUnavailableButtonState(); + + axios.get(this.canCreatePath) + .then(({ data }) => { + this.setUnavailableButtonState(false); + + if (data.can_create_branch) { + this.available(); + this.enable(); + + if (!this.droplabInitialized) { + this.droplabInitialized = true; + this.initDroplab(); + this.bindEvents(); + } + } else if (data.has_related_branch) { + this.hide(); } - } else if (data.has_related_branch) { - this.hide(); - } - }).fail(() => { - this.unavailable(); - this.disable(); - new Flash('Failed to check if a new branch can be created.'); - }); + }) + .catch(() => { + this.unavailable(); + this.disable(); + Flash('Failed to check if a new branch can be created.'); + }); } createBranch() { - return $.ajax({ - method: 'POST', - dataType: 'json', - url: this.createBranchPath, - beforeSend: () => (this.isCreatingBranch = true), - }) - .done((data) => { - this.branchCreated = true; - window.location.href = data.url; - }) - .fail(() => new Flash('Failed to create a branch for this issue. Please try again.')); + this.isCreatingBranch = true; + + return axios.post(this.createBranchPath) + .then(({ data }) => { + this.branchCreated = true; + window.location.href = data.url; + }) + .catch(() => Flash('Failed to create a branch for this issue. Please try again.')); } createMergeRequest() { - return $.ajax({ - method: 'POST', - dataType: 'json', - url: this.createMrPath, - beforeSend: () => (this.isCreatingMergeRequest = true), - }) - .done((data) => { - this.mergeRequestCreated = true; - window.location.href = data.url; - }) - .fail(() => new Flash('Failed to create Merge Request. Please try again.')); + this.isCreatingMergeRequest = true; + + return axios.post(this.createMrPath) + .then(({ data }) => { + this.mergeRequestCreated = true; + window.location.href = data.url; + }) + .catch(() => Flash('Failed to create Merge Request. Please try again.')); } disable() { @@ -199,39 +193,33 @@ export default class CreateMergeRequestDropdown { getRef(ref, target = 'all') { if (!ref) return false; - return $.ajax({ - method: 'GET', - dataType: 'json', - url: this.refsPath + ref, - beforeSend: () => { - this.isGettingRef = true; - }, - }) - .always(() => { - this.isGettingRef = false; - }) - .done((data) => { - const branches = data[Object.keys(data)[0]]; - const tags = data[Object.keys(data)[1]]; - let result; + return axios.get(this.refsPath + ref) + .then(({ data }) => { + const branches = data[Object.keys(data)[0]]; + const tags = data[Object.keys(data)[1]]; + let result; + + if (target === 'branch') { + result = CreateMergeRequestDropdown.findByValue(branches, ref); + } else { + result = CreateMergeRequestDropdown.findByValue(branches, ref, true) || + CreateMergeRequestDropdown.findByValue(tags, ref, true); + this.suggestedRef = result; + } - if (target === 'branch') { - result = CreateMergeRequestDropdown.findByValue(branches, ref); - } else { - result = CreateMergeRequestDropdown.findByValue(branches, ref, true) || - CreateMergeRequestDropdown.findByValue(tags, ref, true); - this.suggestedRef = result; - } + this.isGettingRef = false; - return this.updateInputState(target, ref, result); - }) - .fail(() => { - this.unavailable(); - this.disable(); - new Flash('Failed to get ref.'); + return this.updateInputState(target, ref, result); + }) + .catch(() => { + this.unavailable(); + this.disable(); + new Flash('Failed to get ref.'); - return false; - }); + this.isGettingRef = false; + + return false; + }); } getTargetData(target) { @@ -331,12 +319,12 @@ export default class CreateMergeRequestDropdown { xhr = this.createBranch(); } - xhr.fail(() => { + xhr.catch(() => { this.isCreatingMergeRequest = false; this.isCreatingBranch = false; - }); - xhr.always(() => this.enable()); + this.enable(); + }); this.disable(); } diff --git a/app/assets/javascripts/cycle_analytics/components/banner.vue b/app/assets/javascripts/cycle_analytics/components/banner.vue index 732697c134e..3204b8dd8e7 100644 --- a/app/assets/javascripts/cycle_analytics/components/banner.vue +++ b/app/assets/javascripts/cycle_analytics/components/banner.vue @@ -26,28 +26,34 @@ class="js-ca-dismiss-button dismiss-button" type="button" :aria-label="__('Dismiss Cycle Analytics introduction box')" - @click="dismissOverviewDialog"> + @click="dismissOverviewDialog" + > <i class="fa fa-times" aria-hidden="true"> </i> </button> - <div class="svg-container" v-html="iconCycleAnalyticsSplash"> + <div + class="svg-container" + v-html="iconCycleAnalyticsSplash" + > </div> <div class="inner-content"> <h4> - {{__('Introducing Cycle Analytics')}} + {{ __('Introducing Cycle Analytics') }} </h4> <p> - {{ __('Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.') }} + {{ __(`Cycle Analytics gives an overview +of how much time it takes to go from idea to production in your project.`) }} </p> <p> <a :href="documentationLink" target="_blank" rel="nofollow" - class="btn"> - {{__('Read more')}} + class="btn" + > + {{ __('Read more') }} </a> </p> </div> diff --git a/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue b/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue index 6e94ba929b2..32ae0cc1476 100644 --- a/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue @@ -2,25 +2,34 @@ import tooltip from '../../vue_shared/directives/tooltip'; export default { + directives: { + tooltip, + }, props: { count: { type: Number, required: true, }, }, - directives: { - tooltip, - }, }; </script> <template> - <span v-if="count === 50" class="events-info pull-right"> + <span + v-if="count === 50" + class="events-info pull-right" + > <i class="fa fa-warning" v-tooltip aria-hidden="true" - :title="n__('Limited to showing %d event at most', 'Limited to showing %d events at most', 50)" - data-placement="top"></i> + :title="n__( + 'Limited to showing %d event at most', + 'Limited to showing %d events at most', + 50 + )" + data-placement="top" + > + </i> {{ n__('Showing %d event', 'Showing %d events', 50) }} </span> </template> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_code_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_code_component.vue index 45930145b0a..a71dcf78103 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_code_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/stage_code_component.vue @@ -4,15 +4,21 @@ import totalTime from './total_time_component.vue'; export default { - props: { - items: Array, - stage: Object, - }, components: { userAvatarImage, limitWarning, totalTime, }, + props: { + items: { + type: Array, + default: () => [], + }, + stage: { + type: Object, + default: () => ({}), + }, + }, }; </script> <template> @@ -22,28 +28,44 @@ <limit-warning :count="items.length" /> </div> <ul class="stage-event-list"> - <li v-for="mergeRequest in items" class="stage-event-item"> + <li + v-for="(mergeRequest, i) in items" + :key="i" + class="stage-event-item" + > <div class="item-details"> <!-- FIXME: Pass an alt attribute here for accessibility --> - <user-avatar-image :img-src="mergeRequest.author.avatarUrl"/> + <user-avatar-image :img-src="mergeRequest.author.avatarUrl" /> <h5 class="item-title merge-merquest-title"> <a :href="mergeRequest.url"> {{ mergeRequest.title }} </a> </h5> - <a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a> + <a + :href="mergeRequest.url" + class="issue-link"> + !{{ mergeRequest.iid }} + </a> · <span> {{ s__('OpenedNDaysAgo|Opened') }} - <a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a> + <a + :href="mergeRequest.url" + class="issue-date"> + {{ mergeRequest.createdAt }} + </a> </span> <span> {{ s__('ByAuthor|by') }} - <a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a> + <a + :href="mergeRequest.author.webUrl" + class="issue-author-link"> + {{ mergeRequest.author.name }} + </a> </span> </div> <div class="item-time"> - <total-time :time="mergeRequest.totalTime"></total-time> + <total-time :time="mergeRequest.totalTime" /> </div> </li> </ul> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_component.vue index 8c98bd249a1..907638d798a 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/stage_component.vue @@ -4,15 +4,21 @@ import totalTime from './total_time_component.vue'; export default { - props: { - items: Array, - stage: Object, - }, components: { userAvatarImage, limitWarning, totalTime, }, + props: { + items: { + type: Array, + default: () => [], + }, + stage: { + type: Object, + default: () => ({}), + }, + }, }; </script> <template> @@ -25,30 +31,43 @@ <li v-for="(issue, i) in items" :key="i" - class="stage-event-item"> + class="stage-event-item" + > <div class="item-details"> <!-- FIXME: Pass an alt attribute here for accessibility --> <user-avatar-image :img-src="issue.author.avatarUrl"/> <h5 class="item-title issue-title"> - <a class="issue-title" :href="issue.url"> + <a + class="issue-title" + :href="issue.url" + > {{ issue.title }} </a> </h5> - <a :href="issue.url" class="issue-link">#{{ issue.iid }}</a> + <a + :href="issue.url" + class="issue-link" + >#{{ issue.iid }}</a> · <span> {{ s__('OpenedNDaysAgo|Opened') }} - <a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a> + <a + :href="issue.url" + class="issue-date" + >{{ issue.createdAt }}</a> </span> <span> {{ s__('ByAuthor|by') }} - <a :href="issue.author.webUrl" class="issue-author-link"> + <a + :href="issue.author.webUrl" + class="issue-author-link" + > {{ issue.author.name }} </a> </span> </div> <div class="item-time"> - <total-time :time="issue.totalTime"/> + <total-time :time="issue.totalTime" /> </div> </li> </ul> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.vue index 75d2f1fd70c..cee294b4ac2 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.vue @@ -5,15 +5,21 @@ import totalTime from './total_time_component.vue'; export default { - props: { - items: Array, - stage: Object, - }, components: { userAvatarImage, totalTime, limitWarning, }, + props: { + items: { + type: Array, + default: () => [], + }, + stage: { + type: Object, + default: () => ({}), + }, + }, computed: { iconCommit() { return iconCommit; @@ -31,10 +37,11 @@ <li v-for="(commit, i) in items" :key="i" - class="stage-event-item"> + class="stage-event-item" + > <div class="item-details item-conmmit-component"> <!-- FIXME: Pass an alt attribute here for accessibility --> - <user-avatar-image :img-src="commit.author.avatarUrl"/> + <user-avatar-image :img-src="commit.author.avatarUrl" /> <h5 class="item-title commit-title"> <a :href="commit.commitUrl"> {{ commit.title }} @@ -42,10 +49,20 @@ </h5> <span> {{ s__('FirstPushedBy|First') }} - <span class="commit-icon" v-html="iconCommit"></span> - <a :href="commit.commitUrl" class="commit-hash-link commit-sha">{{ commit.shortSha }}</a> + <span + class="commit-icon" + v-html="iconCommit" + > + </span> + <a + :href="commit.commitUrl" + class="commit-hash-link commit-sha" + >{{ commit.shortSha }}</a> {{ s__('FirstPushedBy|pushed by') }} - <a :href="commit.author.webUrl" class="commit-author-link"> + <a + :href="commit.author.webUrl" + class="commit-author-link" + > {{ commit.author.name }} </a> </span> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue index cbce9205e75..39b699a6395 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue @@ -5,16 +5,22 @@ import icon from '../../vue_shared/components/icon.vue'; export default { - props: { - items: Array, - stage: Object, - }, components: { userAvatarImage, totalTime, limitWarning, icon, }, + props: { + items: { + type: Array, + default: () => [], + }, + stage: { + type: Object, + default: () => ({}), + }, + }, }; </script> <template> @@ -27,7 +33,8 @@ <li v-for="(mergeRequest, i) in items" :key="i" - class="stage-event-item"> + class="stage-event-item" + > <div class="item-details"> <!-- FIXME: Pass an alt attribute here for accessibility --> <user-avatar-image :img-src="mergeRequest.author.avatarUrl"/> @@ -36,34 +43,52 @@ {{ mergeRequest.title }} </a> </h5> - <a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a> + <a + :href="mergeRequest.url" + class="issue-link" + >!{{ mergeRequest.iid }}</a> · <span> {{ s__('OpenedNDaysAgo|Opened') }} - <a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a> + <a + :href="mergeRequest.url" + class="issue-date" + >{{ mergeRequest.createdAt }}</a> </span> <span> {{ s__('ByAuthor|by') }} - <a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a> + <a + :href="mergeRequest.author.webUrl" + class="issue-author-link" + >{{ mergeRequest.author.name }}</a> </span> <template v-if="mergeRequest.state === 'closed'"> <span class="merge-request-state"> - <i class="fa fa-ban"></i> + <i + class="fa fa-ban" + aria-hidden="true" + > + </i> {{ mergeRequest.state.toUpperCase() }} </span> </template> <template v-else> - <span class="merge-request-branch" v-if="mergeRequest.branch"> + <span + class="merge-request-branch" + v-if="mergeRequest.branch" + > <icon name="fork" - :size="16"> - </icon> - <a :href="mergeRequest.branch.url">{{ mergeRequest.branch.name }}</a> + :size="16" + /> + <a :href="mergeRequest.branch.url"> + {{ mergeRequest.branch.name }} + </a> </span> </template> </div> <div class="item-time"> - <total-time :time="mergeRequest.totalTime"/> + <total-time :time="mergeRequest.totalTime" /> </div> </li> </ul> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue index 508a411e599..92f2a95a66a 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue @@ -6,16 +6,22 @@ import icon from '../../vue_shared/components/icon.vue'; export default { - props: { - items: Array, - stage: Object, - }, components: { userAvatarImage, totalTime, limitWarning, icon, }, + props: { + items: { + type: Array, + default: () => [], + }, + stage: { + type: Object, + default: () => ({}), + }, + }, computed: { iconBranch() { return iconBranch; @@ -33,30 +39,58 @@ <li v-for="(build, i) in items" class="stage-event-item item-build-component" - :key="i"> + :key="i" + > <div class="item-details"> <!-- FIXME: Pass an alt attribute here for accessibility --> <user-avatar-image :img-src="build.author.avatarUrl"/> <h5 class="item-title"> - <a :href="build.url" class="pipeline-id">#{{ build.id }}</a> + <a + :href="build.url" + class="pipeline-id" + > + #{{ build.id }} + </a> <icon name="fork" - :size="16"> - </icon> - <a :href="build.branch.url" class="ref-name">{{ build.branch.name }}</a> - <span class="icon-branch" v-html="iconBranch"></span> - <a :href="build.commitUrl" class="commit-sha">{{ build.shortSha }}</a> + :size="16" + /> + <a + :href="build.branch.url" + class="ref-name" + > + {{ build.branch.name }} + </a> + <span + class="icon-branch" + v-html="iconBranch" + > + </span> + <a + :href="build.commitUrl" + class="commit-sha" + > + {{ build.shortSha }} + </a> </h5> <span> - <a :href="build.url" class="build-date">{{ build.date }}</a> + <a + :href="build.url" + class="build-date" + > + {{ build.date }} + </a> {{ s__('ByAuthor|by') }} - <a :href="build.author.webUrl" class="issue-author-link"> + <a + :href="build.author.webUrl" + class="issue-author-link" + > {{ build.author.name }} </a> </span> </div> <div class="item-time"> - <total-time :time="build.totalTime"/> + <total-time :time="build.totalTime" /> </div> </li> </ul> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue index 88fa6b073ca..b84bb6ed792 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue @@ -6,15 +6,21 @@ import icon from '../../vue_shared/components/icon.vue'; export default { - props: { - items: Array, - stage: Object, - }, components: { totalTime, limitWarning, icon, }, + props: { + items: { + type: Array, + default: () => [], + }, + stage: { + type: Object, + default: () => ({}), + }, + }, computed: { iconBuildStatus() { return iconBuildStatus; @@ -35,29 +41,59 @@ <li v-for="(build, i) in items" :key="i" - class="stage-event-item item-build-component"> + class="stage-event-item item-build-component" + > <div class="item-details"> <h5 class="item-title"> - <span class="icon-build-status" v-html="iconBuildStatus"></span> - <a :href="build.url" class="item-build-name">{{ build.name }}</a> + <span + class="icon-build-status" + v-html="iconBuildStatus" + > + </span> + <a + :href="build.url" + class="item-build-name" + > + {{ build.name }} + </a> · - <a :href="build.url" class="pipeline-id">#{{ build.id }}</a> + <a + :href="build.url" + class="pipeline-id" + > + #{{ build.id }} + </a> <icon name="fork" - :size="16"> - </icon> - <a :href="build.branch.url" class="ref-name">{{ build.branch.name }}</a> - <span class="icon-branch" v-html="iconBranch"></span> - <a :href="build.commitUrl" class="commit-sha">{{ build.shortSha }}</a> + :size="16" + /> + <a + :href="build.branch.url" + class="ref-name" + > + {{ build.branch.name }} + </a> + <span + class="icon-branch" + v-html="iconBranch" + > + </span> + <a + :href="build.commitUrl" + class="commit-sha"> + {{ build.shortSha }} + </a> </h5> <span> - <a :href="build.url" class="issue-date"> + <a + :href="build.url" + class="issue-date"> {{ build.date }} </a> </span> </div> <div class="item-time"> - <total-time :time="build.totalTime"/> + <total-time :time="build.totalTime" /> </div> </li> </ul> diff --git a/app/assets/javascripts/cycle_analytics/components/total_time_component.vue b/app/assets/javascripts/cycle_analytics/components/total_time_component.vue index 62efd4f9c28..7758bf0cb3f 100644 --- a/app/assets/javascripts/cycle_analytics/components/total_time_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/total_time_component.vue @@ -17,13 +17,33 @@ <template> <span class="total-time"> <template v-if="hasData"> - <template v-if="time.days">{{ time.days }} <span>{{ n__('day', 'days', time.days) }}</span></template> - <template v-if="time.hours">{{ time.hours }} <span>{{ n__('Time|hr', 'Time|hrs', time.hours) }}</span></template> - <template v-if="time.mins && !time.days">{{ time.mins }} <span>{{ n__('Time|min', 'Time|mins', time.mins) }}</span></template> - <template v-if="time.seconds && hasData === 1 || time.seconds === 0">{{ time.seconds }} <span>{{ s__('Time|s') }}</span></template> + <template v-if="time.days"> + {{ time.days }} + <span> + {{ n__('day', 'days', time.days) }} + </span> + </template> + <template v-if="time.hours"> + {{ time.hours }} + <span> + {{ n__('Time|hr', 'Time|hrs', time.hours) }} + </span> + </template> + <template v-if="time.mins && !time.days"> + {{ time.mins }} + <span> + {{ n__('Time|min', 'Time|mins', time.mins) }} + </span> + </template> + <template v-if="time.seconds && hasData === 1 || time.seconds === 0"> + {{ time.seconds }} + <span> + {{ s__('Time|s') }} + </span> + </template> </template> <template v-else> -- </template> - </span> + </span> </template> diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js index 49bb6c52180..034f2923b3b 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js @@ -20,6 +20,16 @@ $(() => { gl.cycleAnalyticsApp = new Vue({ el: '#cycle-analytics', name: 'CycleAnalytics', + components: { + banner, + 'stage-issue-component': stageComponent, + 'stage-plan-component': stagePlanComponent, + 'stage-code-component': stageCodeComponent, + 'stage-test-component': stageTestComponent, + 'stage-review-component': stageReviewComponent, + 'stage-staging-component': stageStagingComponent, + 'stage-production-component': stageComponent, + }, data() { const cycleAnalyticsEl = document.querySelector('#cycle-analytics'); const cycleAnalyticsService = new CycleAnalyticsService({ @@ -43,16 +53,6 @@ $(() => { return this.store.currentActiveStage(); }, }, - components: { - banner, - 'stage-issue-component': stageComponent, - 'stage-plan-component': stagePlanComponent, - 'stage-code-component': stageCodeComponent, - 'stage-test-component': stageTestComponent, - 'stage-review-component': stageReviewComponent, - 'stage-staging-component': stageStagingComponent, - 'stage-production-component': stageComponent, - }, created() { this.fetchCycleAnalyticsData(); }, diff --git a/app/assets/javascripts/deploy_keys/components/action_btn.vue b/app/assets/javascripts/deploy_keys/components/action_btn.vue index f9f2f9bf693..b839b9f286f 100644 --- a/app/assets/javascripts/deploy_keys/components/action_btn.vue +++ b/app/assets/javascripts/deploy_keys/components/action_btn.vue @@ -3,10 +3,8 @@ import loadingIcon from '../../vue_shared/components/loading_icon.vue'; export default { - data() { - return { - isLoading: false, - }; + components: { + loadingIcon, }, props: { deployKey: { @@ -23,11 +21,16 @@ default: 'btn-default', }, }, - - components: { - loadingIcon, + data() { + return { + isLoading: false, + }; + }, + computed: { + text() { + return `${this.type.charAt(0).toUpperCase()}${this.type.slice(1)}`; + }, }, - methods: { doAction() { this.isLoading = true; @@ -37,11 +40,6 @@ }); }, }, - computed: { - text() { - return `${this.type.charAt(0).toUpperCase()}${this.type.slice(1)}`; - }, - }, }; </script> diff --git a/app/assets/javascripts/deploy_keys/components/app.vue b/app/assets/javascripts/deploy_keys/components/app.vue index fe046449054..5a782237b7d 100644 --- a/app/assets/javascripts/deploy_keys/components/app.vue +++ b/app/assets/javascripts/deploy_keys/components/app.vue @@ -7,11 +7,9 @@ import loadingIcon from '../../vue_shared/components/loading_icon.vue'; export default { - data() { - return { - isLoading: false, - store: new DeployKeysStore(), - }; + components: { + keysPanel, + loadingIcon, }, props: { endpoint: { @@ -19,6 +17,12 @@ required: true, }, }, + data() { + return { + isLoading: false, + store: new DeployKeysStore(), + }; + }, computed: { hasKeys() { return Object.keys(this.keys).length; @@ -27,9 +31,20 @@ return this.store.keys; }, }, - components: { - keysPanel, - loadingIcon, + created() { + this.service = new DeployKeysService(this.endpoint); + + eventHub.$on('enable.key', this.enableKey); + eventHub.$on('remove.key', this.disableKey); + eventHub.$on('disable.key', this.disableKey); + }, + mounted() { + this.fetchKeys(); + }, + beforeDestroy() { + eventHub.$off('enable.key', this.enableKey); + eventHub.$off('remove.key', this.disableKey); + eventHub.$off('disable.key', this.disableKey); }, methods: { fetchKeys() { @@ -59,21 +74,6 @@ } }, }, - created() { - this.service = new DeployKeysService(this.endpoint); - - eventHub.$on('enable.key', this.enableKey); - eventHub.$on('remove.key', this.disableKey); - eventHub.$on('disable.key', this.disableKey); - }, - mounted() { - this.fetchKeys(); - }, - beforeDestroy() { - eventHub.$off('enable.key', this.enableKey); - eventHub.$off('remove.key', this.disableKey); - eventHub.$off('disable.key', this.disableKey); - }, }; </script> @@ -87,6 +87,7 @@ <div v-else-if="hasKeys"> <keys-panel title="Enabled deploy keys for this project" + class="qa-project-deploy-keys" :keys="keys.enabled_keys" :store="store" :endpoint="endpoint" diff --git a/app/assets/javascripts/deploy_keys/components/key.vue b/app/assets/javascripts/deploy_keys/components/key.vue index 2a05c6f001e..c6091efd62f 100644 --- a/app/assets/javascripts/deploy_keys/components/key.vue +++ b/app/assets/javascripts/deploy_keys/components/key.vue @@ -1,8 +1,15 @@ <script> import actionBtn from './action_btn.vue'; import { getTimeago } from '../../lib/utils/datetime_utility'; + import tooltip from '../../vue_shared/directives/tooltip'; export default { + components: { + actionBtn, + }, + directives: { + tooltip, + }, props: { deployKey: { type: Object, @@ -17,9 +24,6 @@ required: true, }, }, - components: { - actionBtn, - }, computed: { timeagoDate() { return getTimeago().format(this.deployKey.created_at); @@ -32,6 +36,9 @@ isEnabled(id) { return this.store.findEnabledKey(id) !== undefined; }, + tooltipTitle(project) { + return project.can_push ? 'Write access allowed' : 'Read access only'; + }, }, }; </script> @@ -46,26 +53,29 @@ </i> </div> <div class="deploy-key-content key-list-item-info"> - <strong class="title"> + <strong class="title qa-key-title"> {{ deployKey.title }} </strong> - <div class="description"> + <div class="description qa-key-fingerprint"> {{ deployKey.fingerprint }} </div> - <div - v-if="deployKey.can_push" - class="write-access-allowed" - > - Write access allowed - </div> </div> <div class="deploy-key-content prepend-left-default deploy-key-projects"> <a - v-for="project in deployKey.projects" + v-for="(deployKeysProject, i) in deployKey.deploy_keys_projects" + :key="i" class="label deploy-project-label" - :href="project.full_path" + :href="deployKeysProject.project.full_path" + :title="tooltipTitle(deployKeysProject)" + v-tooltip > - {{ project.full_name }} + {{ deployKeysProject.project.full_name }} + <i + v-if="!deployKeysProject.can_push" + aria-hidden="true" + class="fa fa-lock" + > + </i> </a> </div> <div class="deploy-key-content"> diff --git a/app/assets/javascripts/deploy_keys/components/keys_panel.vue b/app/assets/javascripts/deploy_keys/components/keys_panel.vue index 9e6fb244af6..822b0323156 100644 --- a/app/assets/javascripts/deploy_keys/components/keys_panel.vue +++ b/app/assets/javascripts/deploy_keys/components/keys_panel.vue @@ -2,6 +2,9 @@ import key from './key.vue'; export default { + components: { + key, + }, props: { title: { type: String, @@ -25,9 +28,6 @@ required: true, }, }, - components: { - key, - }, }; </script> @@ -37,12 +37,14 @@ {{ title }} ({{ keys.length }}) </h5> - <ul class="well-list" + <ul + class="well-list" v-if="keys.length" > <li v-for="deployKey in keys" - :key="deployKey.id"> + :key="deployKey.id" + > <key :deploy-key="deployKey" :store="store" diff --git a/app/assets/javascripts/deploy_keys/index.js b/app/assets/javascripts/deploy_keys/index.js index a5f232f950a..ca8798facc9 100644 --- a/app/assets/javascripts/deploy_keys/index.js +++ b/app/assets/javascripts/deploy_keys/index.js @@ -3,14 +3,14 @@ import deployKeysApp from './components/app.vue'; document.addEventListener('DOMContentLoaded', () => new Vue({ el: document.getElementById('js-deploy-keys'), + components: { + deployKeysApp, + }, data() { return { endpoint: this.$options.el.dataset.endpoint, }; }, - components: { - deployKeysApp, - }, render(createElement) { return createElement('deploy-keys-app', { props: { diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 42f61d33f6e..262ed3783fb 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -1,97 +1,16 @@ /* 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 { s__ } from './locale'; -import projectSelect from './project_select'; -import IssuableIndex from './issuable_index'; -import Milestone from './milestone'; -import IssuableForm from './issuable_form'; -import LabelsSelect from './labels_select'; -import MilestoneSelect from './milestone_select'; -import NewBranchForm from './new_branch_form'; -import NotificationsForm from './notifications_form'; -import notificationsDropdown from './notifications_dropdown'; -import groupAvatar from './group_avatar'; -import GroupLabelSubscription from './group_label_subscription'; -import LineHighlighter from './line_highlighter'; -import BuildArtifacts from './build_artifacts'; -import CILintEditor from './ci_lint_editor'; -import groupsSelect from './groups_select'; -import Search from './search'; -import initAdmin from './admin'; -import NamespaceSelect from './namespace_select'; -import NewCommitForm from './new_commit_form'; -import Project from './project'; -import projectAvatar from './project_avatar'; import MergeRequest from './merge_request'; -import Compare from './compare'; -import initCompareAutocomplete from './compare_autocomplete'; -import ProjectFindFile from './project_find_file'; -import ProjectNew from './project_new'; -import projectImport from './project_import'; -import Labels from './labels'; -import LabelManager from './label_manager'; -import Sidebar from './right_sidebar'; -import IssuableTemplateSelectors from './templates/issuable_template_selectors'; import Flash from './flash'; -import CommitsList from './commits'; -import Issue from './issue'; -import BindInOut from './behaviors/bind_in_out'; -import SecretValues from './behaviors/secret_values'; -import DeleteModal from './branches/branches_delete_modal'; -import Group from './group'; -import GroupsList from './groups_list'; -import ProjectsList from './projects_list'; -import setupProjectEdit from './project_edit'; -import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; -import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater'; -import Landing from './landing'; -import BlobForkSuggestion from './blob/blob_fork_suggestion'; -import UserCallout from './user_callout'; -import ShortcutsWiki from './shortcuts_wiki'; -import Pipelines from './pipelines'; -import BlobViewer from './blob/viewer/index'; -import AutoWidthDropdownSelect from './issuable/auto_width_dropdown_select'; -import UsersSelect from './users_select'; -import RefSelectDropdown from './ref_select_dropdown'; import GfmAutoComplete from './gfm_auto_complete'; -import ShortcutsBlob from './shortcuts_blob'; -import SigninTabsMemoizer from './signin_tabs_memoizer'; -import Star from './star'; -import Todos from './todos'; -import TreeView from './tree'; -import UsagePing from './usage_ping'; -import UsernameValidator from './username_validator'; -import VersionCheckImage from './version_check_image'; -import Wikis from './wikis'; import ZenMode from './zen_mode'; -import initSettingsPanels from './settings_panels'; -import initExperimentalFlags from './experimental_flags'; -import OAuthRememberMe from './oauth_remember_me'; -import PerformanceBar from './performance_bar'; -import initBroadcastMessagesForm from './broadcast_message'; import initNotes from './init_notes'; -import initLegacyFilters from './init_legacy_filters'; import initIssuableSidebar from './init_issuable_sidebar'; -import initProjectVisibilitySelector from './project_visibility'; -import GpgBadges from './gpg_badges'; -import initChangesDropdown from './init_changes_dropdown'; -import NewGroupChild from './groups/new_group_child'; -import AbuseReports from './abuse_reports'; -import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils'; -import AjaxLoadingSpinner from './ajax_loading_spinner'; +import { convertPermissionToBoolean } from './lib/utils/common_utils'; import GlFieldErrors from './gl_field_errors'; -import GLForm from './gl_form'; import Shortcuts from './shortcuts'; -import ShortcutsNavigation from './shortcuts_navigation'; -import ShortcutsFindFile from './shortcuts_find_file'; import ShortcutsIssuable from './shortcuts_issuable'; -import U2FAuthenticate from './u2f/authenticate'; -import Members from './members'; -import memberExpirationDate from './member_expiration_date'; -import DueDateSelectors from './due_date_select'; import Diff from './diff'; -import ProjectLabelSubscription from './project_label_subscription'; import SearchAutocomplete from './search_autocomplete'; -import Activities from './activities'; (function() { var Dispatcher; @@ -104,13 +23,14 @@ import Activities from './activities'; } Dispatcher.prototype.initPageScripts = function() { - var path, shortcut_handler, fileBlobPermalinkUrlElement, fileBlobPermalinkUrl; + var path, shortcut_handler; const page = $('body').attr('data-page'); if (!page) { return false; } const fail = () => Flash('Error loading dynamic module'); + const callDefault = m => m.default(); path = page.split(':'); shortcut_handler = null; @@ -128,198 +48,233 @@ import Activities from './activities'; }); }); - function initBlob() { - new LineHighlighter(); - - new BlobLinePermalinkUpdater( - document.querySelector('#blob-content-holder'), - '.diff-line-num[data-line-number]', - document.querySelectorAll('.js-data-file-blob-permalink-url, .js-blob-blame-link'), - ); - - shortcut_handler = new ShortcutsNavigation(); - fileBlobPermalinkUrlElement = document.querySelector('.js-data-file-blob-permalink-url'); - fileBlobPermalinkUrl = fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href'); - new ShortcutsBlob({ - skipResetBindings: true, - fileBlobPermalinkUrl, - }); - - new BlobForkSuggestion({ - openButtons: document.querySelectorAll('.js-edit-blob-link-fork-toggler'), - forkButtons: document.querySelectorAll('.js-fork-suggestion-button'), - cancelButtons: document.querySelectorAll('.js-cancel-fork-suggestion-button'), - suggestionSections: document.querySelectorAll('.js-file-fork-suggestion-section'), - actionTextPieces: document.querySelectorAll('.js-file-fork-suggestion-section-action'), - }) - .init(); - } - - const filteredSearchEnabled = gl.FilteredSearchManager && document.querySelector('.filtered-search'); - switch (page) { - case 'profiles:preferences:show': - initExperimentalFlags(); - break; case 'sessions:new': - new UsernameValidator(); - new SigninTabsMemoizer(); - new OAuthRememberMe({ container: $(".omniauth-container") }).bindEvents(); + import('./pages/sessions/new') + .then(callDefault) + .catch(fail); break; case 'projects:boards:show': case 'projects:boards:index': - shortcut_handler = new ShortcutsNavigation(); - new UsersSelect(); + import('./pages/projects/boards/index') + .then(callDefault) + .catch(fail); + shortcut_handler = true; + break; + case 'projects:environments:metrics': + import('./pages/projects/environments/metrics') + .then(callDefault) + .catch(fail); break; case 'projects:merge_requests:index': + import('./pages/projects/merge_requests/index') + .then(callDefault) + .catch(fail); + shortcut_handler = true; + break; case 'projects:issues:index': - if (filteredSearchEnabled) { - const filteredSearchManager = new gl.FilteredSearchManager(page === 'projects:issues:index' ? 'issues' : 'merge_requests'); - filteredSearchManager.setup(); - } - const pagePrefix = page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_'; - new IssuableIndex(pagePrefix); - - shortcut_handler = new ShortcutsNavigation(); - new UsersSelect(); + import('./pages/projects/issues/index') + .then(callDefault) + .catch(fail); + shortcut_handler = true; break; case 'projects:issues:show': - new Issue(); - shortcut_handler = new ShortcutsIssuable(); - new ZenMode(); - initIssuableSidebar(); + import('./pages/projects/issues/show') + .then(callDefault) + .catch(fail); + shortcut_handler = true; break; case 'dashboard:milestones:index': - projectSelect(); + import('./pages/dashboard/milestones/index') + .then(callDefault) + .catch(fail); + break; + case 'projects:milestones:index': + import('./pages/projects/milestones/index') + .then(callDefault) + .catch(fail); break; case 'projects:milestones:show': + import('./pages/projects/milestones/show') + .then(callDefault) + .catch(fail); + break; case 'groups:milestones:show': + import('./pages/groups/milestones/show') + .then(callDefault) + .catch(fail); + break; case 'dashboard:milestones:show': - new Milestone(); - new Sidebar(); + import('./pages/dashboard/milestones/show') + .then(callDefault) + .catch(fail); break; case 'dashboard:issues': + import('./pages/dashboard/issues') + .then(callDefault) + .catch(fail); + break; case 'dashboard:merge_requests': - projectSelect(); - initLegacyFilters(); + import('./pages/dashboard/merge_requests') + .then(callDefault) + .catch(fail); break; case 'groups:issues': + import('./pages/groups/issues') + .then(callDefault) + .catch(fail); + break; case 'groups:merge_requests': - if (filteredSearchEnabled) { - const filteredSearchManager = new gl.FilteredSearchManager(page === 'groups:issues' ? 'issues' : 'merge_requests'); - filteredSearchManager.setup(); - } - projectSelect(); + import('./pages/groups/merge_requests') + .then(callDefault) + .catch(fail); break; case 'dashboard:todos:index': - new Todos(); + import('./pages/dashboard/todos/index') + .then(callDefault) + .catch(fail); + break; + case 'admin:jobs:index': + import('./pages/admin/jobs/index') + .then(callDefault) + .catch(fail); break; case 'dashboard:projects:index': case 'dashboard:projects:starred': + import('./pages/dashboard/projects') + .then(callDefault) + .catch(fail); + break; case 'explore:projects:index': case 'explore:projects:trending': case 'explore:projects:starred': - case 'admin:projects:index': - new ProjectsList(); + import('./pages/explore/projects') + .then(callDefault) + .catch(fail); break; case 'explore:groups:index': - new GroupsList(); - const landingElement = document.querySelector('.js-explore-groups-landing'); - if (!landingElement) break; - const exploreGroupsLanding = new Landing( - landingElement, - landingElement.querySelector('.dismiss-button'), - 'explore_groups_landing_dismissed', - ); - exploreGroupsLanding.toggle(); + import('./pages/explore/groups') + .then(callDefault) + .catch(fail); break; case 'projects:milestones:new': + case 'projects:milestones:create': + import('./pages/projects/milestones/new') + .then(callDefault) + .catch(fail); + break; case 'projects:milestones:edit': case 'projects:milestones:update': - new ZenMode(); - new DueDateSelectors(); - new GLForm($('.milestone-form'), true); + import('./pages/projects/milestones/edit') + .then(callDefault) + .catch(fail); break; case 'groups:milestones:new': + case 'groups:milestones:create': + import('./pages/groups/milestones/new') + .then(callDefault) + .catch(fail); + break; case 'groups:milestones:edit': case 'groups:milestones:update': - new ZenMode(); - new DueDateSelectors(); - new GLForm($('.milestone-form'), false); + import('./pages/groups/milestones/edit') + .then(callDefault) + .catch(fail); break; case 'projects:compare:show': - new Diff(); - const paddingTop = 16; - initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - paddingTop); + import('./pages/projects/compare/show') + .then(callDefault) + .catch(fail); break; case 'projects:branches:new': + import('./pages/projects/branches/new') + .then(callDefault) + .catch(fail); + break; case 'projects:branches:create': - new NewBranchForm($('.js-create-branch-form'), JSON.parse(document.getElementById('availableRefs').innerHTML)); + import('./pages/projects/branches/new') + .then(callDefault) + .catch(fail); break; case 'projects:branches:index': - AjaxLoadingSpinner.init(); - new DeleteModal(); + import('./pages/projects/branches/index') + .then(callDefault) + .catch(fail); break; case 'projects:issues:new': + import('./pages/projects/issues/new') + .then(callDefault) + .catch(fail); + shortcut_handler = true; + break; case 'projects:issues:edit': - shortcut_handler = new ShortcutsNavigation(); - new GLForm($('.issue-form'), true); - new IssuableForm($('.issue-form')); - new LabelsSelect(); - new MilestoneSelect(); - new IssuableTemplateSelectors(); + import('./pages/projects/issues/edit') + .then(callDefault) + .catch(fail); + shortcut_handler = true; break; case 'projects:merge_requests:creations:new': - const mrNewCompareNode = document.querySelector('.js-merge-request-new-compare'); - if (mrNewCompareNode) { - new Compare({ - targetProjectUrl: mrNewCompareNode.dataset.targetProjectUrl, - sourceBranchUrl: mrNewCompareNode.dataset.sourceBranchUrl, - targetBranchUrl: mrNewCompareNode.dataset.targetBranchUrl, - }); - } else { - const mrNewSubmitNode = document.querySelector('.js-merge-request-new-submit'); - new MergeRequest({ - action: mrNewSubmitNode.dataset.mrSubmitAction, - }); - } + import('./pages/projects/merge_requests/creations/new') + .then(callDefault) + .catch(fail); case 'projects:merge_requests:creations:diffs': + import('./pages/projects/merge_requests/creations/diffs') + .then(callDefault) + .catch(fail); + shortcut_handler = true; + break; case 'projects:merge_requests:edit': - new Diff(); - shortcut_handler = new ShortcutsNavigation(); - new GLForm($('.merge-request-form'), true); - new IssuableForm($('.merge-request-form')); - new LabelsSelect(); - new MilestoneSelect(); - new IssuableTemplateSelectors(); - new AutoWidthDropdownSelect($('.js-target-branch-select')).init(); + import('./pages/projects/merge_requests/edit') + .then(callDefault) + .catch(fail); + shortcut_handler = true; break; case 'projects:tags:new': - new ZenMode(); - new GLForm($('.tag-form'), true); - new RefSelectDropdown($('.js-branch-select')); + import('./pages/projects/tags/new') + .then(callDefault) + .catch(fail); break; case 'projects:snippets:show': - initNotes(); - new ZenMode(); + import('./pages/projects/snippets/show') + .then(callDefault) + .catch(fail); break; case 'projects:snippets:new': - case 'projects:snippets:edit': case 'projects:snippets:create': + import('./pages/projects/snippets/new') + .then(callDefault) + .catch(fail); + break; + case 'projects:snippets:edit': case 'projects:snippets:update': - new GLForm($('.snippet-form'), true); - new ZenMode(); + import('./pages/projects/snippets/edit') + .then(callDefault) + .catch(fail); break; case 'snippets:new': + import('./pages/snippets/new') + .then(callDefault) + .catch(fail); + break; case 'snippets:edit': + import('./pages/snippets/edit') + .then(callDefault) + .catch(fail); + break; case 'snippets:create': + import('./pages/snippets/new') + .then(callDefault) + .catch(fail); + break; case 'snippets:update': - new GLForm($('.snippet-form'), false); - new ZenMode(); + import('./pages/snippets/edit') + .then(callDefault) + .catch(fail); break; case 'projects:releases:edit': - new ZenMode(); - new GLForm($('.release-form'), true); + import('./pages/projects/releases/edit') + .then(callDefault) + .catch(fail); break; case 'projects:merge_requests:show': new Diff(); @@ -332,166 +287,161 @@ import Activities from './activities'; window.mergeRequest = new MergeRequest({ action: mrShowNode.dataset.mrAction, }); - shortcut_handler = new ShortcutsIssuable(true); break; case 'dashboard:activity': - new Activities(); + import('./pages/dashboard/activity') + .then(callDefault) + .catch(fail); break; case 'projects:commit:show': - new Diff(); - new ZenMode(); - shortcut_handler = new ShortcutsNavigation(); - new MiniPipelineGraph({ - container: '.js-commit-pipeline-graph', - }).bindEvents(); - initNotes(); - const stickyBarPaddingTop = 16; - initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - stickyBarPaddingTop); - $('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath); + import('./pages/projects/commit/show') + .then(callDefault) + .catch(fail); + shortcut_handler = true; break; case 'projects:commit:pipelines': - new MiniPipelineGraph({ - container: '.js-commit-pipeline-graph', - }).bindEvents(); - $('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath); + import('./pages/projects/commit/pipelines') + .then(callDefault) + .catch(fail); break; case 'projects:activity': - new Activities(); - shortcut_handler = new ShortcutsNavigation(); + import('./pages/projects/activity') + .then(callDefault) + .catch(fail); + shortcut_handler = true; break; case 'projects:commits:show': - CommitsList.init(document.querySelector('.js-project-commits-show').dataset.commitsLimit); - shortcut_handler = new ShortcutsNavigation(); - GpgBadges.fetch(); + import('./pages/projects/commits/show') + .then(callDefault) + .catch(fail); + shortcut_handler = true; break; case 'projects:show': - shortcut_handler = new ShortcutsNavigation(); - new NotificationsForm(); - new UserCallout({ - setCalloutPerProject: true, - className: 'js-autodevops-banner', - }); - - if ($('#tree-slider').length) new TreeView(); - if ($('.blob-viewer').length) new BlobViewer(); - if ($('.project-show-activity').length) new Activities(); - $('#tree-slider').waitForImages(function() { - ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath); - }); + import('./pages/projects/show') + .then(callDefault) + .catch(fail); + shortcut_handler = true; break; case 'projects:edit': - setupProjectEdit(); - // Initialize expandable settings panels - initSettingsPanels(); + import('./pages/projects/edit') + .then(callDefault) + .catch(fail); break; case 'projects:imports:show': - projectImport(); + import('./pages/projects/imports/show') + .then(callDefault) + .catch(fail); break; case 'projects:pipelines:new': case 'projects:pipelines:create': - new NewBranchForm($('.js-new-pipeline-form')); + import('./pages/projects/pipelines/new') + .then(callDefault) + .catch(fail); break; case 'projects:pipelines:builds': case 'projects:pipelines:failures': case 'projects:pipelines:show': - const { controllerAction } = document.querySelector('.js-pipeline-container').dataset; - const pipelineStatusUrl = `${document.querySelector('.js-pipeline-tab-link a').getAttribute('href')}/status.json`; - - new Pipelines({ - initTabs: true, - pipelineStatusUrl, - tabsOptions: { - action: controllerAction, - defaultAction: 'pipelines', - parentEl: '.pipelines-tabs', - }, - }); + import('./pages/projects/pipelines/builds') + .then(callDefault) + .catch(fail); break; case 'groups:activity': - new Activities(); + import('./pages/groups/activity') + .then(callDefault) + .catch(fail); break; case 'groups:show': - const newGroupChildWrapper = document.querySelector('.js-new-project-subgroup'); - shortcut_handler = new ShortcutsNavigation(); - new NotificationsForm(); - notificationsDropdown(); - new ProjectsList(); - - if (newGroupChildWrapper) { - new NewGroupChild(newGroupChildWrapper); - } + import('./pages/groups/show') + .then(callDefault) + .catch(fail); + shortcut_handler = true; break; case 'groups:group_members:index': - memberExpirationDate(); - new Members(); - new UsersSelect(); + import('./pages/groups/group_members/index') + .then(callDefault) + .catch(fail); break; case 'projects:project_members:index': - memberExpirationDate('.js-access-expiration-date-groups'); - groupsSelect(); - memberExpirationDate(); - new Members(); - new UsersSelect(); + import('./pages/projects/project_members/') + .then(callDefault) + .catch(fail); break; - case 'groups:new': - case 'admin:groups:new': case 'groups:create': - case 'admin:groups:create': - BindInOut.initAll(); - new Group(); - groupAvatar(); + case 'groups:new': + import('./pages/groups/new') + .then(callDefault) + .catch(fail); break; case 'groups:edit': + import('./pages/groups/edit') + .then(callDefault) + .catch(fail); + break; + case 'admin:groups:create': + case 'admin:groups:new': + import('./pages/admin/groups/new') + .then(callDefault) + .catch(fail); + break; case 'admin:groups:edit': - groupAvatar(); + import('./pages/admin/groups/edit') + .then(callDefault) + .catch(fail); break; case 'projects:tree:show': - shortcut_handler = new ShortcutsNavigation(); - new TreeView(); - new BlobViewer(); - new NewCommitForm($('.js-create-dir-form')); - $('#tree-slider').waitForImages(function() { - ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath); - }); + import('./pages/projects/tree/show') + .then(callDefault) + .catch(fail); + shortcut_handler = true; break; case 'projects:find_file:show': - const findElement = document.querySelector('.js-file-finder'); - const projectFindFile = new ProjectFindFile($(".file-finder-holder"), { - url: findElement.dataset.fileFindUrl, - treeUrl: findElement.dataset.findTreeUrl, - blobUrlTemplate: findElement.dataset.blobUrlTemplate, - }); - new ShortcutsFindFile(projectFindFile); + import('./pages/projects/find_file/show') + .then(callDefault) + .catch(fail); shortcut_handler = true; break; case 'projects:blob:show': - new BlobViewer(); - initBlob(); + import('./pages/projects/blob/show') + .then(callDefault) + .catch(fail); + shortcut_handler = true; break; case 'projects:blame:show': - initBlob(); + import('./pages/projects/blame/show') + .then(callDefault) + .catch(fail); + shortcut_handler = true; break; case 'groups:labels:new': + import('./pages/groups/labels/new') + .then(callDefault) + .catch(fail); + break; case 'groups:labels:edit': + import('./pages/groups/labels/edit') + .then(callDefault) + .catch(fail); + break; case 'projects:labels:new': + import('./pages/projects/labels/new') + .then(callDefault) + .catch(fail); + break; case 'projects:labels:edit': - new Labels(); + import('./pages/projects/labels/edit') + .then(callDefault) + .catch(fail); break; case 'groups:labels:index': + import('./pages/groups/labels/index') + .then(callDefault) + .catch(fail); + break; case 'projects:labels:index': - if ($('.prioritized-labels').length) { - new LabelManager(); - } - $('.label-subscription').each((i, el) => { - const $el = $(el); - - if ($el.find('.dropdown-group-label').length) { - new GroupLabelSubscription($el); - } else { - new ProjectLabelSubscription($el); - } - }); + import('./pages/projects/labels/index') + .then(callDefault) + .catch(fail); break; case 'projects:network:show': // Ensure we don't create a particular shortcut handler here. This is @@ -499,183 +449,189 @@ import Activities from './activities'; shortcut_handler = true; break; case 'projects:forks:new': - import(/* webpackChunkName: 'project_fork' */ './project_fork') - .then(fork => fork.default()) - .catch(() => {}); + import('./pages/projects/forks/new') + .then(callDefault) + .catch(fail); break; case 'projects:artifacts:browse': - new ShortcutsNavigation(); - new BuildArtifacts(); + import('./pages/projects/artifacts/browse') + .then(callDefault) + .catch(fail); + shortcut_handler = true; break; case 'projects:artifacts:file': - new ShortcutsNavigation(); - new BlobViewer(); + import('./pages/projects/artifacts/file') + .then(callDefault) + .catch(fail); + shortcut_handler = true; break; case 'help:index': - VersionCheckImage.bindErrorEvent($('img.js-version-status-badge')); + import('./pages/help') + .then(callDefault) + .catch(fail); break; case 'search:show': - new Search(); + import('./pages/search/show') + .then(callDefault) + .catch(fail); break; case 'projects:settings:repository:show': - // Initialize expandable settings panels - initSettingsPanels(); + import('./pages/projects/settings/repository/show') + .then(callDefault) + .catch(fail); break; case 'projects:settings:ci_cd:show': - // Initialize expandable settings panels - initSettingsPanels(); - - const runnerToken = document.querySelector('.js-secret-runner-token'); - if (runnerToken) { - const runnerTokenSecretValue = new SecretValues(runnerToken); - runnerTokenSecretValue.init(); - } + import('./pages/projects/settings/ci_cd/show') + .then(callDefault) + .catch(fail); + break; case 'groups:settings:ci_cd:show': - const secretVariableTable = document.querySelector('.js-secret-variable-table'); - if (secretVariableTable) { - const secretVariableTableValues = new SecretValues(secretVariableTable); - secretVariableTableValues.init(); - } + import('./pages/groups/settings/ci_cd/show') + .then(callDefault) + .catch(fail); break; case 'ci:lints:create': case 'ci:lints:show': - new CILintEditor(); + import('./pages/ci/lints') + .then(callDefault) + .catch(fail); break; case 'users:show': - import('./pages/users/show').then(m => m.default()).catch(fail); + import('./pages/users/show') + .then(callDefault) + .catch(fail); break; case 'admin:conversational_development_index:show': - new UserCallout(); + import('./pages/admin/conversational_development_index/show') + .then(callDefault) + .catch(fail); break; case 'snippets:show': - new LineHighlighter(); - new BlobViewer(); - initNotes(); - new ZenMode(); + import('./pages/snippets/show') + .then(callDefault) + .catch(fail); break; case 'import:fogbugz:new_user_map': - new UsersSelect(); + import('./pages/import/fogbugz/new_user_map') + .then(callDefault) + .catch(fail); break; case 'profiles:personal_access_tokens:index': + import('./pages/profiles/personal_access_tokens') + .then(callDefault) + .catch(fail); + break; case 'admin:impersonation_tokens:index': - new DueDateSelectors(); + import('./pages/admin/impersonation_tokens') + .then(callDefault) + .catch(fail); break; case 'projects:clusters:show': - import(/* webpackChunkName: "clusters" */ './clusters/clusters_bundle') - .then(cluster => new cluster.default()) // eslint-disable-line new-cap - .catch((err) => { - Flash(s__('ClusterIntegration|Problem setting up the cluster')); - throw err; - }); + case 'projects:clusters:update': + case 'projects:clusters:destroy': + import('./pages/projects/clusters/show') + .then(callDefault) + .catch(fail); break; case 'projects:clusters:index': - import(/* webpackChunkName: "clusters_index" */ './clusters/clusters_index') - .then(clusterIndex => clusterIndex.default()) - .catch((err) => { - Flash(s__('ClusterIntegration|Problem setting up the clusters list')); - throw err; - }); + import('./pages/projects/clusters/index') + .then(callDefault) + .catch(fail); + break; + case 'dashboard:groups:index': + import('./pages/dashboard/groups/index') + .then(callDefault) + .catch(fail); break; } switch (path[0]) { case 'sessions': + import('./pages/sessions') + .then(callDefault) + .catch(fail); + break; case 'omniauth_callbacks': - if (!gon.u2f) break; - const u2fAuthenticate = new U2FAuthenticate( - $('#js-authenticate-u2f'), - '#js-login-u2f-form', - gon.u2f, - document.querySelector('#js-login-2fa-device'), - document.querySelector('.js-2fa-form'), - ); - u2fAuthenticate.start(); - // needed in rspec - gl.u2fAuthenticate = u2fAuthenticate; + import('./pages/omniauth_callbacks') + .then(callDefault) + .catch(fail); + break; case 'admin': - initAdmin(); + import('./pages/admin') + .then(callDefault) + .catch(fail); switch (path[1]) { case 'broadcast_messages': - initBroadcastMessagesForm(); + import('./pages/admin/broadcast_messages') + .then(callDefault) + .catch(fail); break; case 'cohorts': - new UsagePing(); + import('./pages/admin/cohorts') + .then(callDefault) + .catch(fail); break; case 'groups': - new UsersSelect(); + switch (path[2]) { + case 'show': + import('./pages/admin/groups/show') + .then(callDefault) + .catch(fail); + break; + } break; case 'projects': - document.querySelectorAll('.js-namespace-select') - .forEach(dropdown => new NamespaceSelect({ dropdown })); + import('./pages/admin/projects') + .then(callDefault) + .catch(fail); break; case 'labels': switch (path[2]) { case 'new': + import('./pages/admin/labels/new') + .then(callDefault) + .catch(fail); + break; case 'edit': - new Labels(); + import('./pages/admin/labels/edit') + .then(callDefault) + .catch(fail); + break; } case 'abuse_reports': - new AbuseReports(); + import('./pages/admin/abuse_reports') + .then(callDefault) + .catch(fail); break; } break; - case 'dashboard': - case 'root': - new UserCallout(); - break; case 'profiles': - new NotificationsForm(); - notificationsDropdown(); + import('./pages/profiles/index/') + .then(callDefault) + .catch(fail); break; case 'projects': - new Project(); - projectAvatar(); + import('./pages/projects') + .then(callDefault) + .catch(fail); + shortcut_handler = true; switch (path[1]) { case 'compare': - initCompareAutocomplete(); - break; - case 'edit': - shortcut_handler = new ShortcutsNavigation(); - new ProjectNew(); - import(/* webpackChunkName: 'project_permissions' */ './projects/permissions') - .then(permissions => permissions.default()) - .catch(() => {}); + import('./pages/projects/compare') + .then(callDefault) + .catch(fail); break; + case 'create': case 'new': - new ProjectNew(); - initProjectVisibilitySelector(); - break; - case 'show': - new Star(); - new ProjectNew(); - notificationsDropdown(); + import('./pages/projects/new') + .then(callDefault) + .catch(fail); break; case 'wikis': - new Wikis(); - shortcut_handler = new ShortcutsWiki(); - new ZenMode(); - new GLForm($('.wiki-form'), true); - break; - case 'snippets': - shortcut_handler = new ShortcutsNavigation(); - if (path[2] === 'show') { - new ZenMode(); - new LineHighlighter(); - new BlobViewer(); - } + import('./pages/projects/wikis') + .then(callDefault) + .catch(fail); + shortcut_handler = true; break; - case 'labels': - case 'graphs': - case 'compare': - case 'pipelines': - case 'forks': - case 'milestones': - case 'project_members': - case 'deploy_keys': - case 'builds': - case 'hooks': - case 'services': - case 'protected_branches': - shortcut_handler = new ShortcutsNavigation(); } break; } @@ -685,7 +641,9 @@ import Activities from './activities'; } if (document.querySelector('#peek')) { - new PerformanceBar({ container: '#peek' }); + import('./performance_bar') + .then(m => new m.default({ container: '#peek' })) // eslint-disable-line new-cap + .catch(fail); } }; diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js index c84be42649a..ba89e5726fa 100644 --- a/app/assets/javascripts/dropzone_input.js +++ b/app/assets/javascripts/dropzone_input.js @@ -2,6 +2,9 @@ import Dropzone from 'dropzone'; import _ from 'underscore'; import './preview_markdown'; import csrf from './lib/utils/csrf'; +import axios from './lib/utils/axios_utils'; + +Dropzone.autoDiscover = false; export default function dropzoneInput(form) { const divHover = '<div class="div-dropzone-hover"></div>'; @@ -233,25 +236,21 @@ export default function dropzoneInput(form) { uploadFile = (item, filename) => { const formData = new FormData(); formData.append('file', item, filename); - return $.ajax({ - url: uploadsPath, - type: 'POST', - data: formData, - dataType: 'json', - processData: false, - contentType: false, - headers: csrf.headers, - beforeSend: () => { - showSpinner(); - return closeAlertMessage(); - }, - success: (e, text, response) => { - const md = response.responseJSON.link.markdown; + + showSpinner(); + closeAlertMessage(); + + axios.post(uploadsPath, formData) + .then(({ data }) => { + const md = data.link.markdown; + insertToTextArea(filename, md); - }, - error: response => showError(response.responseJSON.message), - complete: () => closeSpinner(), - }); + closeSpinner(); + }) + .catch((e) => { + showError(e.response.data.message); + closeSpinner(); + }); }; updateAttachingMessage = (files, messageContainer) => { diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js index ada985913bb..bd4c58b7cb1 100644 --- a/app/assets/javascripts/due_date_select.js +++ b/app/assets/javascripts/due_date_select.js @@ -1,6 +1,7 @@ /* global dateFormat */ import Pikaday from 'pikaday'; +import axios from './lib/utils/axios_utils'; import { parsePikadayDate, pikadayToString } from './lib/utils/datefix'; class DueDateSelect { @@ -125,37 +126,30 @@ class DueDateSelect { } submitSelectedDate(isDropdown) { - return $.ajax({ - type: 'PUT', - url: this.issueUpdateURL, - data: this.datePayload, - dataType: 'json', - beforeSend: () => { - const selectedDateValue = this.datePayload[this.abilityName].due_date; - const displayedDateStyle = this.displayedDate !== 'No due date' ? 'bold' : 'no-value'; + const selectedDateValue = this.datePayload[this.abilityName].due_date; + const displayedDateStyle = this.displayedDate !== 'No due date' ? 'bold' : 'no-value'; - this.$loading.removeClass('hidden').fadeIn(); + this.$loading.removeClass('hidden').fadeIn(); - if (isDropdown) { - this.$dropdown.trigger('loading.gl.dropdown'); - this.$selectbox.hide(); - } + if (isDropdown) { + this.$dropdown.trigger('loading.gl.dropdown'); + this.$selectbox.hide(); + } - this.$value.css('display', ''); - this.$valueContent.html(`<span class='${displayedDateStyle}'>${this.displayedDate}</span>`); - this.$sidebarValue.html(this.displayedDate); + this.$value.css('display', ''); + this.$valueContent.html(`<span class='${displayedDateStyle}'>${this.displayedDate}</span>`); + this.$sidebarValue.html(this.displayedDate); - return selectedDateValue.length ? - $('.js-remove-due-date-holder').removeClass('hidden') : - $('.js-remove-due-date-holder').addClass('hidden'); - }, - }).done(() => { - if (isDropdown) { - this.$dropdown.trigger('loaded.gl.dropdown'); - this.$dropdown.dropdown('toggle'); - } - return this.$loading.fadeOut(); - }); + $('.js-remove-due-date-holder').toggleClass('hidden', selectedDateValue.length); + + return axios.put(this.issueUpdateURL, this.datePayload) + .then(() => { + if (isDropdown) { + this.$dropdown.trigger('loaded.gl.dropdown'); + this.$dropdown.dropdown('toggle'); + } + return this.$loading.fadeOut(); + }); } } diff --git a/app/assets/javascripts/environments/components/container.vue b/app/assets/javascripts/environments/components/container.vue index 3236077c3cf..dbee81fa320 100644 --- a/app/assets/javascripts/environments/components/container.vue +++ b/app/assets/javascripts/environments/components/container.vue @@ -4,6 +4,11 @@ import environmentTable from '../components/environments_table.vue'; export default { + components: { + environmentTable, + loadingIcon, + tablePagination, + }, props: { isLoading: { type: Boolean, @@ -26,12 +31,6 @@ required: true, }, }, - components: { - environmentTable, - loadingIcon, - tablePagination, - }, - methods: { onChangePage(page) { this.$emit('onChangePage', page); @@ -47,7 +46,7 @@ label="Loading environments" v-if="isLoading" size="3" - /> + /> <slot name="emptyState"></slot> @@ -59,13 +58,13 @@ :environments="environments" :can-create-deployment="canCreateDeployment" :can-read-environment="canReadEnvironment" - /> + /> <table-pagination v-if="pagination && pagination.totalPages > 1" :change="onChangePage" - :pageInfo="pagination" - /> + :page-info="pagination" + /> </div> </div> </template> diff --git a/app/assets/javascripts/environments/components/empty_state.vue b/app/assets/javascripts/environments/components/empty_state.vue index 2646f08c8e6..00e63c3467a 100644 --- a/app/assets/javascripts/environments/components/empty_state.vue +++ b/app/assets/javascripts/environments/components/empty_state.vue @@ -1,6 +1,6 @@ <script> export default { - name: 'environmentsEmptyState', + name: 'EnvironmentsEmptyState', props: { newPath: { type: String, @@ -21,21 +21,23 @@ <div class="blank-state-row"> <div class="blank-state-center"> <h2 class="blank-state-title js-blank-state-title"> - {{s__("Environments|You don't have any environments right now.")}} + {{ s__("Environments|You don't have any environments right now.") }} </h2> <p class="blank-state-text"> - {{s__("Environments|Environments are places where code gets deployed, such as staging or production.")}} + {{ s__(`Environments|Environments are places where +code gets deployed, such as staging or production.`) }} <br /> <a :href="helpPath"> - {{s__("Environments|Read more about environments")}} + {{ s__("Environments|Read more about environments") }} </a> </p> <a v-if="canCreateEnvironment" :href="newPath" - class="btn btn-create js-new-environment-button"> - {{s__("Environments|New environment")}} + class="btn btn-create js-new-environment-button" + > + {{ s__("Environments|New environment") }} </a> </div> </div> diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue index e7495677e7c..16bd2f5feb3 100644 --- a/app/assets/javascripts/environments/components/environment_actions.vue +++ b/app/assets/javascripts/environments/components/environment_actions.vue @@ -1,55 +1,54 @@ <script> -import playIconSvg from 'icons/_icon_play.svg'; -import eventHub from '../event_hub'; -import loadingIcon from '../../vue_shared/components/loading_icon.vue'; -import tooltip from '../../vue_shared/directives/tooltip'; + import playIconSvg from 'icons/_icon_play.svg'; + import eventHub from '../event_hub'; + import loadingIcon from '../../vue_shared/components/loading_icon.vue'; + import tooltip from '../../vue_shared/directives/tooltip'; -export default { - props: { - actions: { - type: Array, - required: false, - default: () => [], + export default { + directives: { + tooltip, }, - }, - directives: { - tooltip, - }, - - components: { - loadingIcon, - }, + components: { + loadingIcon, + }, + props: { + actions: { + type: Array, + required: false, + default: () => [], + }, + }, - data() { - return { - playIconSvg, - isLoading: false, - }; - }, + data() { + return { + playIconSvg, + isLoading: false, + }; + }, - computed: { - title() { - return 'Deploy to...'; + computed: { + title() { + return 'Deploy to...'; + }, }, - }, - methods: { - onClickAction(endpoint) { - this.isLoading = true; + methods: { + onClickAction(endpoint) { + this.isLoading = true; - eventHub.$emit('postAction', endpoint); - }, + eventHub.$emit('postAction', endpoint); + }, - isActionDisabled(action) { - if (action.playable === undefined) { - return false; - } + isActionDisabled(action) { + if (action.playable === undefined) { + return false; + } - return !action.playable; + return !action.playable; + }, }, - }, -}; + }; </script> <template> <div @@ -63,27 +62,33 @@ export default { data-toggle="dropdown" :title="title" :aria-label="title" - :disabled="isLoading"> + :disabled="isLoading" + > <span> <span v-html="playIconSvg"></span> <i class="fa fa-caret-down" - aria-hidden="true"/> + aria-hidden="true" + > + </i> <loading-icon v-if="isLoading" /> </span> </button> <ul class="dropdown-menu dropdown-menu-align-right"> - <li v-for="action in actions"> + <li + v-for="(action, i) in actions" + :key="i"> <button type="button" class="js-manual-action-link no-btn btn" @click="onClickAction(action.play_path)" :class="{ disabled: isActionDisabled(action) }" - :disabled="isActionDisabled(action)"> + :disabled="isActionDisabled(action)" + > <span v-html="playIconSvg"></span> <span> - {{action.name}} + {{ action.name }} </span> </button> </li> diff --git a/app/assets/javascripts/environments/components/environment_external_url.vue b/app/assets/javascripts/environments/components/environment_external_url.vue index 520c3ac8ace..c9a68cface6 100644 --- a/app/assets/javascripts/environments/components/environment_external_url.vue +++ b/app/assets/javascripts/environments/components/environment_external_url.vue @@ -1,28 +1,27 @@ <script> -import tooltip from '../../vue_shared/directives/tooltip'; -import { s__ } from '../../locale'; + import tooltip from '../../vue_shared/directives/tooltip'; + import { s__ } from '../../locale'; -/** - * Renders the external url link in environments table. - */ -export default { - props: { - externalUrl: { - type: String, - required: true, + /** + * Renders the external url link in environments table. + */ + export default { + directives: { + tooltip, + }, + props: { + externalUrl: { + type: String, + required: true, + }, }, - }, - - directives: { - tooltip, - }, - computed: { - title() { - return s__('Environments|Open'); + computed: { + title() { + return s__('Environments|Open'); + }, }, - }, -}; + }; </script> <template> <a @@ -33,9 +32,12 @@ export default { rel="noopener noreferrer nofollow" :title="title" :aria-label="title" - :href="externalUrl"> + :href="externalUrl" + > <i class="fa fa-external-link" - aria-hidden="true" /> + aria-hidden="true" + > + </i> </a> </template> diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue index 2f0e397aa45..a9d554e549e 100644 --- a/app/assets/javascripts/environments/components/environment_item.vue +++ b/app/assets/javascripts/environments/components/environment_item.vue @@ -1,423 +1,424 @@ <script> -import Timeago from 'timeago.js'; -import _ from 'underscore'; -import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; -import { humanize } from '../../lib/utils/text_utility'; -import ActionsComponent from './environment_actions.vue'; -import ExternalUrlComponent from './environment_external_url.vue'; -import StopComponent from './environment_stop.vue'; -import RollbackComponent from './environment_rollback.vue'; -import TerminalButtonComponent from './environment_terminal_button.vue'; -import MonitoringButtonComponent from './environment_monitoring.vue'; -import CommitComponent from '../../vue_shared/components/commit.vue'; -import eventHub from '../event_hub'; - -/** - * Envrionment Item Component - * - * Renders a table row for each environment. - */ -const timeagoInstance = new Timeago(); - -export default { - components: { - userAvatarLink, - 'commit-component': CommitComponent, - 'actions-component': ActionsComponent, - 'external-url-component': ExternalUrlComponent, - 'stop-component': StopComponent, - 'rollback-component': RollbackComponent, - 'terminal-button-component': TerminalButtonComponent, - 'monitoring-button-component': MonitoringButtonComponent, - }, - - props: { - model: { - type: Object, - required: true, - default: () => ({}), + import Timeago from 'timeago.js'; + import _ from 'underscore'; + import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; + import { humanize } from '../../lib/utils/text_utility'; + import ActionsComponent from './environment_actions.vue'; + import ExternalUrlComponent from './environment_external_url.vue'; + import StopComponent from './environment_stop.vue'; + import RollbackComponent from './environment_rollback.vue'; + import TerminalButtonComponent from './environment_terminal_button.vue'; + import MonitoringButtonComponent from './environment_monitoring.vue'; + import CommitComponent from '../../vue_shared/components/commit.vue'; + import eventHub from '../event_hub'; + + /** + * Envrionment Item Component + * + * Renders a table row for each environment. + */ + const timeagoInstance = new Timeago(); + + export default { + components: { + userAvatarLink, + 'commit-component': CommitComponent, + 'actions-component': ActionsComponent, + 'external-url-component': ExternalUrlComponent, + 'stop-component': StopComponent, + 'rollback-component': RollbackComponent, + 'terminal-button-component': TerminalButtonComponent, + 'monitoring-button-component': MonitoringButtonComponent, }, - canCreateDeployment: { - type: Boolean, - required: false, - default: false, + props: { + model: { + type: Object, + required: true, + default: () => ({}), + }, + + canCreateDeployment: { + type: Boolean, + required: false, + default: false, + }, + + canReadEnvironment: { + type: Boolean, + required: false, + default: false, + }, }, - canReadEnvironment: { - type: Boolean, - required: false, - default: false, - }, - }, - - computed: { - /** - * Verifies if `last_deployment` key exists in the current Envrionment. - * This key is required to render most of the html - this method works has - * an helper. - * - * @returns {Boolean} - */ - hasLastDeploymentKey() { - if (this.model && - this.model.last_deployment && - !_.isEmpty(this.model.last_deployment)) { - return true; - } - return false; - }, - - /** - * Verifies is the given environment has manual actions. - * Used to verify if we should render them or nor. - * - * @returns {Boolean|Undefined} - */ - hasManualActions() { - return this.model && - this.model.last_deployment && - this.model.last_deployment.manual_actions && - this.model.last_deployment.manual_actions.length > 0; - }, - - /** - * Returns the value of the `stop_action?` key provided in the response. - * - * @returns {Boolean} - */ - hasStopAction() { - return this.model && this.model['stop_action?']; - }, - - /** - * Verifies if the `deployable` key is present in `last_deployment` key. - * Used to verify whether we should or not render the rollback partial. - * - * @returns {Boolean|Undefined} - */ - canRetry() { - return this.model && - this.hasLastDeploymentKey && - this.model.last_deployment && - this.model.last_deployment.deployable; - }, - - /** - * Verifies if the date to be shown is present. - * - * @returns {Boolean|Undefined} - */ - canShowDate() { - return this.model && - this.model.last_deployment && - this.model.last_deployment.deployable && - this.model.last_deployment.deployable !== undefined; - }, - - /** - * Human readable date. - * - * @returns {String} - */ - createdDate() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.deployable && - this.model.last_deployment.deployable.created_at) { - return timeagoInstance.format(this.model.last_deployment.deployable.created_at); - } - return ''; - }, - - /** - * Returns the manual actions with the name parsed. - * - * @returns {Array.<Object>|Undefined} - */ - manualActions() { - if (this.hasManualActions) { - return this.model.last_deployment.manual_actions.map((action) => { - const parsedAction = { - name: humanize(action.name), - play_path: action.play_path, - playable: action.playable, - }; - return parsedAction; - }); - } - return []; - }, - - /** - * Builds the string used in the user image alt attribute. - * - * @returns {String} - */ - userImageAltDescription() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.user && - this.model.last_deployment.user.username) { - return `${this.model.last_deployment.user.username}'s avatar'`; - } - return ''; - }, - - /** - * If provided, returns the commit tag. - * - * @returns {String|Undefined} - */ - commitTag() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.tag) { - return this.model.last_deployment.tag; - } - return undefined; + computed: { + /** + * Verifies if `last_deployment` key exists in the current Envrionment. + * This key is required to render most of the html - this method works has + * an helper. + * + * @returns {Boolean} + */ + hasLastDeploymentKey() { + if (this.model && + this.model.last_deployment && + !_.isEmpty(this.model.last_deployment)) { + return true; + } + return false; + }, + + /** + * Verifies is the given environment has manual actions. + * Used to verify if we should render them or nor. + * + * @returns {Boolean|Undefined} + */ + hasManualActions() { + return this.model && + this.model.last_deployment && + this.model.last_deployment.manual_actions && + this.model.last_deployment.manual_actions.length > 0; + }, + + /** + * Returns the value of the `stop_action?` key provided in the response. + * + * @returns {Boolean} + */ + hasStopAction() { + return this.model && this.model['stop_action?']; + }, + + /** + * Verifies if the `deployable` key is present in `last_deployment` key. + * Used to verify whether we should or not render the rollback partial. + * + * @returns {Boolean|Undefined} + */ + canRetry() { + return this.model && + this.hasLastDeploymentKey && + this.model.last_deployment && + this.model.last_deployment.deployable; + }, + + /** + * Verifies if the date to be shown is present. + * + * @returns {Boolean|Undefined} + */ + canShowDate() { + return this.model && + this.model.last_deployment && + this.model.last_deployment.deployable && + this.model.last_deployment.deployable !== undefined; + }, + + /** + * Human readable date. + * + * @returns {String} + */ + createdDate() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.deployable && + this.model.last_deployment.deployable.created_at) { + return timeagoInstance.format(this.model.last_deployment.deployable.created_at); + } + return ''; + }, + + /** + * Returns the manual actions with the name parsed. + * + * @returns {Array.<Object>|Undefined} + */ + manualActions() { + if (this.hasManualActions) { + return this.model.last_deployment.manual_actions.map((action) => { + const parsedAction = { + name: humanize(action.name), + play_path: action.play_path, + playable: action.playable, + }; + return parsedAction; + }); + } + return []; + }, + + /** + * Builds the string used in the user image alt attribute. + * + * @returns {String} + */ + userImageAltDescription() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.user && + this.model.last_deployment.user.username) { + return `${this.model.last_deployment.user.username}'s avatar'`; + } + return ''; + }, + + /** + * If provided, returns the commit tag. + * + * @returns {String|Undefined} + */ + commitTag() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.tag) { + return this.model.last_deployment.tag; + } + return undefined; + }, + + /** + * If provided, returns the commit ref. + * + * @returns {Object|Undefined} + */ + commitRef() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.ref) { + return this.model.last_deployment.ref; + } + return undefined; + }, + + /** + * If provided, returns the commit url. + * + * @returns {String|Undefined} + */ + commitUrl() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.commit && + this.model.last_deployment.commit.commit_path) { + return this.model.last_deployment.commit.commit_path; + } + return undefined; + }, + + /** + * If provided, returns the commit short sha. + * + * @returns {String|Undefined} + */ + commitShortSha() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.commit && + this.model.last_deployment.commit.short_id) { + return this.model.last_deployment.commit.short_id; + } + return undefined; + }, + + /** + * If provided, returns the commit title. + * + * @returns {String|Undefined} + */ + commitTitle() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.commit && + this.model.last_deployment.commit.title) { + return this.model.last_deployment.commit.title; + } + return undefined; + }, + + /** + * If provided, returns the commit tag. + * + * @returns {Object|Undefined} + */ + commitAuthor() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.commit && + this.model.last_deployment.commit.author) { + return this.model.last_deployment.commit.author; + } + + return undefined; + }, + + /** + * Verifies if the `retry_path` key is present and returns its value. + * + * @returns {String|Undefined} + */ + retryUrl() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.deployable && + this.model.last_deployment.deployable.retry_path) { + return this.model.last_deployment.deployable.retry_path; + } + return undefined; + }, + + /** + * Verifies if the `last?` key is present and returns its value. + * + * @returns {Boolean|Undefined} + */ + isLastDeployment() { + return this.model && this.model.last_deployment && + this.model.last_deployment['last?']; + }, + + /** + * Builds the name of the builds needed to display both the name and the id. + * + * @returns {String} + */ + buildName() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.deployable) { + const deployable = this.model.last_deployment.deployable; + return `${deployable.name} #${deployable.id}`; + } + return ''; + }, + + /** + * Builds the needed string to show the internal id. + * + * @returns {String} + */ + deploymentInternalId() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.iid) { + return `#${this.model.last_deployment.iid}`; + } + return ''; + }, + + /** + * Verifies if the user object is present under last_deployment object. + * + * @returns {Boolean} + */ + deploymentHasUser() { + return this.model && + !_.isEmpty(this.model.last_deployment) && + !_.isEmpty(this.model.last_deployment.user); + }, + + /** + * Returns the user object nested with the last_deployment object. + * Used to render the template. + * + * @returns {Object} + */ + deploymentUser() { + if (this.model && + !_.isEmpty(this.model.last_deployment) && + !_.isEmpty(this.model.last_deployment.user)) { + return this.model.last_deployment.user; + } + return {}; + }, + + /** + * Verifies if the build name column should be rendered by verifing + * if all the information needed is present + * and if the environment is not a folder. + * + * @returns {Boolean} + */ + shouldRenderBuildName() { + return !this.model.isFolder && + !_.isEmpty(this.model.last_deployment) && + !_.isEmpty(this.model.last_deployment.deployable); + }, + + /** + * Verifies the presence of all the keys needed to render the buil_path. + * + * @return {String} + */ + buildPath() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.deployable && + this.model.last_deployment.deployable.build_path) { + return this.model.last_deployment.deployable.build_path; + } + + return ''; + }, + + /** + * Verifies the presence of all the keys needed to render the external_url. + * + * @return {String} + */ + externalURL() { + if (this.model && this.model.external_url) { + return this.model.external_url; + } + + return ''; + }, + + /** + * Verifies if deplyment internal ID should be rendered by verifing + * if all the information needed is present + * and if the environment is not a folder. + * + * @returns {Boolean} + */ + shouldRenderDeploymentID() { + return !this.model.isFolder && + !_.isEmpty(this.model.last_deployment) && + this.model.last_deployment.iid !== undefined; + }, + + environmentPath() { + if (this.model && this.model.environment_path) { + return this.model.environment_path; + } + + return ''; + }, + + monitoringUrl() { + if (this.model && this.model.metrics_path) { + return this.model.metrics_path; + } + + return ''; + }, + + displayEnvironmentActions() { + return this.hasManualActions || + this.externalURL || + this.monitoringUrl || + this.hasStopAction || + this.canRetry; + }, }, - /** - * If provided, returns the commit ref. - * - * @returns {Object|Undefined} - */ - commitRef() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.ref) { - return this.model.last_deployment.ref; - } - return undefined; + methods: { + onClickFolder() { + eventHub.$emit('toggleFolder', this.model); + }, }, - - /** - * If provided, returns the commit url. - * - * @returns {String|Undefined} - */ - commitUrl() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.commit && - this.model.last_deployment.commit.commit_path) { - return this.model.last_deployment.commit.commit_path; - } - return undefined; - }, - - /** - * If provided, returns the commit short sha. - * - * @returns {String|Undefined} - */ - commitShortSha() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.commit && - this.model.last_deployment.commit.short_id) { - return this.model.last_deployment.commit.short_id; - } - return undefined; - }, - - /** - * If provided, returns the commit title. - * - * @returns {String|Undefined} - */ - commitTitle() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.commit && - this.model.last_deployment.commit.title) { - return this.model.last_deployment.commit.title; - } - return undefined; - }, - - /** - * If provided, returns the commit tag. - * - * @returns {Object|Undefined} - */ - commitAuthor() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.commit && - this.model.last_deployment.commit.author) { - return this.model.last_deployment.commit.author; - } - - return undefined; - }, - - /** - * Verifies if the `retry_path` key is present and returns its value. - * - * @returns {String|Undefined} - */ - retryUrl() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.deployable && - this.model.last_deployment.deployable.retry_path) { - return this.model.last_deployment.deployable.retry_path; - } - return undefined; - }, - - /** - * Verifies if the `last?` key is present and returns its value. - * - * @returns {Boolean|Undefined} - */ - isLastDeployment() { - return this.model && this.model.last_deployment && - this.model.last_deployment['last?']; - }, - - /** - * Builds the name of the builds needed to display both the name and the id. - * - * @returns {String} - */ - buildName() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.deployable) { - return `${this.model.last_deployment.deployable.name} #${this.model.last_deployment.deployable.id}`; - } - return ''; - }, - - /** - * Builds the needed string to show the internal id. - * - * @returns {String} - */ - deploymentInternalId() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.iid) { - return `#${this.model.last_deployment.iid}`; - } - return ''; - }, - - /** - * Verifies if the user object is present under last_deployment object. - * - * @returns {Boolean} - */ - deploymentHasUser() { - return this.model && - !_.isEmpty(this.model.last_deployment) && - !_.isEmpty(this.model.last_deployment.user); - }, - - /** - * Returns the user object nested with the last_deployment object. - * Used to render the template. - * - * @returns {Object} - */ - deploymentUser() { - if (this.model && - !_.isEmpty(this.model.last_deployment) && - !_.isEmpty(this.model.last_deployment.user)) { - return this.model.last_deployment.user; - } - return {}; - }, - - /** - * Verifies if the build name column should be rendered by verifing - * if all the information needed is present - * and if the environment is not a folder. - * - * @returns {Boolean} - */ - shouldRenderBuildName() { - return !this.model.isFolder && - !_.isEmpty(this.model.last_deployment) && - !_.isEmpty(this.model.last_deployment.deployable); - }, - - /** - * Verifies the presence of all the keys needed to render the buil_path. - * - * @return {String} - */ - buildPath() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.deployable && - this.model.last_deployment.deployable.build_path) { - return this.model.last_deployment.deployable.build_path; - } - - return ''; - }, - - /** - * Verifies the presence of all the keys needed to render the external_url. - * - * @return {String} - */ - externalURL() { - if (this.model && this.model.external_url) { - return this.model.external_url; - } - - return ''; - }, - - /** - * Verifies if deplyment internal ID should be rendered by verifing - * if all the information needed is present - * and if the environment is not a folder. - * - * @returns {Boolean} - */ - shouldRenderDeploymentID() { - return !this.model.isFolder && - !_.isEmpty(this.model.last_deployment) && - this.model.last_deployment.iid !== undefined; - }, - - environmentPath() { - if (this.model && this.model.environment_path) { - return this.model.environment_path; - } - - return ''; - }, - - monitoringUrl() { - if (this.model && this.model.metrics_path) { - return this.model.metrics_path; - } - - return ''; - }, - - displayEnvironmentActions() { - return this.hasManualActions || - this.externalURL || - this.monitoringUrl || - this.hasStopAction || - this.canRetry; - }, - }, - - methods: { - onClickFolder() { - eventHub.$emit('toggleFolder', this.model); - }, - }, -}; + }; </script> <template> <div @@ -427,18 +428,22 @@ export default { 'folder-row': model.isFolder, }" role="row"> - <div class="table-section section-10" role="gridcell"> + <div + class="table-section section-10" + role="gridcell" + > <div v-if="!model.isFolder" class="table-mobile-header" - role="rowheader"> - {{s__("Environments|Environment")}} + role="rowheader" + > + {{ s__("Environments|Environment") }} </div> <a v-if="!model.isFolder" class="environment-name flex-truncate-parent table-mobile-content" :href="environmentPath"> - <span class="flex-truncate-child">{{model.name}}</span> + <span class="flex-truncate-child">{{ model.name }}</span> </a> <span v-else @@ -450,32 +455,40 @@ export default { <i v-show="model.isOpen" class="fa fa-caret-down" - aria-hidden="true" /> + aria-hidden="true" + > + </i> <i v-show="!model.isOpen" class="fa fa-caret-right" - aria-hidden="true"/> + aria-hidden="true" + > + </i> </span> <span class="folder-icon"> <i class="fa fa-folder" - aria-hidden="true" /> + aria-hidden="true"> + </i> </span> <span> - {{model.folderName}} + {{ model.folderName }} </span> <span class="badge"> - {{model.size}} + {{ model.size }} </span> </span> </div> - <div class="table-section section-10 deployment-column hidden-xs hidden-sm" role="gridcell"> + <div + class="table-section section-10 deployment-column hidden-xs hidden-sm" + role="gridcell" + > <span v-if="shouldRenderDeploymentID"> - {{deploymentInternalId}} + {{ deploymentInternalId }} </span> <span v-if="!model.isFolder && deploymentHasUser"> @@ -490,22 +503,29 @@ export default { </span> </div> - <div class="table-section section-15 hidden-xs hidden-sm" role="gridcell"> + <div + class="table-section section-15 hidden-xs hidden-sm" + role="gridcell" + > <a v-if="shouldRenderBuildName" class="build-link flex-truncate-parent" - :href="buildPath"> - <span class="flex-truncate-child">{{buildName}}</span> + :href="buildPath" + > + <span class="flex-truncate-child">{{ buildName }}</span> </a> </div> <div v-if="!model.isFolder" - class="table-section section-25" role="gridcell"> + class="table-section section-25" + role="gridcell" + > <div role="rowheader" - class="table-mobile-header"> - {{s__("Environments|Commit")}} + class="table-mobile-header" + > + {{ s__("Environments|Commit") }} </div> <div v-if="hasLastDeploymentKey" @@ -521,22 +541,24 @@ export default { <div v-if="!hasLastDeploymentKey" class="commit-title table-mobile-content"> - {{s__("Environments|No deployments yet")}} + {{ s__("Environments|No deployments yet") }} </div> </div> <div v-if="!model.isFolder" - class="table-section section-10" role="gridcell"> + class="table-section section-10" + role="gridcell" + > <div role="rowheader" class="table-mobile-header"> - {{s__("Environments|Updated")}} + {{ s__("Environments|Updated") }} </div> <span v-if="canShowDate" class="environment-created-date-timeago table-mobile-content"> - {{createdDate}} + {{ createdDate }} </span> </div> @@ -552,33 +574,33 @@ export default { <actions-component v-if="hasManualActions && canCreateDeployment" :actions="manualActions" - /> + /> <external-url-component v-if="externalURL && canReadEnvironment" :external-url="externalURL" - /> + /> <monitoring-button-component v-if="monitoringUrl && canReadEnvironment" :monitoring-url="monitoringUrl" - /> + /> <terminal-button-component v-if="model && model.terminal_path" :terminal-path="model.terminal_path" - /> + /> <stop-component v-if="hasStopAction && canCreateDeployment" :stop-url="model.stop_path" - /> + /> <rollback-component v-if="canRetry && canCreateDeployment" :is-last-deployment="isLastDeployment" :retry-url="retryUrl" - /> + /> </div> </div> </div> diff --git a/app/assets/javascripts/environments/components/environment_monitoring.vue b/app/assets/javascripts/environments/components/environment_monitoring.vue index b45af1a5ebc..081537cf218 100644 --- a/app/assets/javascripts/environments/components/environment_monitoring.vue +++ b/app/assets/javascripts/environments/components/environment_monitoring.vue @@ -1,27 +1,27 @@ <script> -/** - * Renders the Monitoring (Metrics) link in environments table. - */ -import tooltip from '../../vue_shared/directives/tooltip'; + /** + * Renders the Monitoring (Metrics) link in environments table. + */ + import tooltip from '../../vue_shared/directives/tooltip'; -export default { - props: { - monitoringUrl: { - type: String, - required: true, + export default { + directives: { + tooltip, }, - }, - directives: { - tooltip, - }, + props: { + monitoringUrl: { + type: String, + required: true, + }, + }, - computed: { - title() { - return 'Monitoring'; + computed: { + title() { + return 'Monitoring'; + }, }, - }, -}; + }; </script> <template> <a @@ -31,10 +31,12 @@ export default { rel="noopener noreferrer nofollow" :href="monitoringUrl" :title="title" - :aria-label="title"> + :aria-label="title" + > <i class="fa fa-area-chart" aria-hidden="true" - /> + > + </i> </a> </template> diff --git a/app/assets/javascripts/environments/components/environment_rollback.vue b/app/assets/javascripts/environments/components/environment_rollback.vue index 92a596bfd33..605a88e997e 100644 --- a/app/assets/javascripts/environments/components/environment_rollback.vue +++ b/app/assets/javascripts/environments/components/environment_rollback.vue @@ -1,57 +1,58 @@ <script> -/** - * Renders Rollback or Re deploy button in environments table depending - * of the provided property `isLastDeployment`. - * - * Makes a post request when the button is clicked. - */ -import eventHub from '../event_hub'; -import loadingIcon from '../../vue_shared/components/loading_icon.vue'; - -export default { - props: { - retryUrl: { - type: String, - default: '', + /** + * Renders Rollback or Re deploy button in environments table depending + * of the provided property `isLastDeployment`. + * + * Makes a post request when the button is clicked. + */ + import eventHub from '../event_hub'; + import loadingIcon from '../../vue_shared/components/loading_icon.vue'; + + export default { + components: { + loadingIcon, }, - isLastDeployment: { - type: Boolean, - default: true, - }, - }, + props: { + retryUrl: { + type: String, + default: '', + }, - components: { - loadingIcon, - }, + isLastDeployment: { + type: Boolean, + default: true, + }, + }, - data() { - return { - isLoading: false, - }; - }, + data() { + return { + isLoading: false, + }; + }, - methods: { - onClick() { - this.isLoading = true; + methods: { + onClick() { + this.isLoading = true; - eventHub.$emit('postAction', this.retryUrl); + eventHub.$emit('postAction', this.retryUrl); + }, }, - }, -}; + }; </script> <template> <button type="button" class="btn hidden-xs hidden-sm" @click="onClick" - :disabled="isLoading"> + :disabled="isLoading" + > <span v-if="isLastDeployment"> - {{s__("Environments|Re-deploy")}} + {{ s__("Environments|Re-deploy") }} </span> <span v-else> - {{s__("Environments|Rollback")}} + {{ s__("Environments|Rollback") }} </span> <loading-icon v-if="isLoading" /> diff --git a/app/assets/javascripts/environments/components/environment_stop.vue b/app/assets/javascripts/environments/components/environment_stop.vue index 85f11d2071b..1eef17bf1fe 100644 --- a/app/assets/javascripts/environments/components/environment_stop.vue +++ b/app/assets/javascripts/environments/components/environment_stop.vue @@ -1,53 +1,53 @@ <script> -/** - * Renders the stop "button" that allows stop an environment. - * Used in environments table. - */ -import eventHub from '../event_hub'; -import loadingIcon from '../../vue_shared/components/loading_icon.vue'; -import tooltip from '../../vue_shared/directives/tooltip'; + /** + * Renders the stop "button" that allows stop an environment. + * Used in environments table. + */ + import eventHub from '../event_hub'; + import loadingIcon from '../../vue_shared/components/loading_icon.vue'; + import tooltip from '../../vue_shared/directives/tooltip'; -export default { - props: { - stopUrl: { - type: String, - default: '', + export default { + components: { + loadingIcon, }, - }, - directives: { - tooltip, - }, + directives: { + tooltip, + }, - data() { - return { - isLoading: false, - }; - }, + props: { + stopUrl: { + type: String, + default: '', + }, + }, - components: { - loadingIcon, - }, + data() { + return { + isLoading: false, + }; + }, - computed: { - title() { - return 'Stop'; + computed: { + title() { + return 'Stop'; + }, }, - }, - methods: { - onClick() { - // eslint-disable-next-line no-alert - if (confirm('Are you sure you want to stop this environment?')) { - this.isLoading = true; + methods: { + onClick() { + // eslint-disable-next-line no-alert + if (confirm('Are you sure you want to stop this environment?')) { + this.isLoading = true; - $(this.$el).tooltip('destroy'); + $(this.$el).tooltip('destroy'); - eventHub.$emit('postAction', this.stopUrl); - } + eventHub.$emit('postAction', this.stopUrl); + } + }, }, - }, -}; + }; </script> <template> <button @@ -58,10 +58,13 @@ export default { @click="onClick" :disabled="isLoading" :title="title" - :aria-label="title"> + :aria-label="title" + > <i class="fa fa-stop stop-env-icon" - aria-hidden="true" /> + aria-hidden="true" + > + </i> <loading-icon v-if="isLoading" /> </button> </template> diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.vue b/app/assets/javascripts/environments/components/environment_terminal_button.vue index 2037bf618e3..407d5333c0e 100644 --- a/app/assets/javascripts/environments/components/environment_terminal_button.vue +++ b/app/assets/javascripts/environments/components/environment_terminal_button.vue @@ -1,36 +1,36 @@ <script> -/** - * Renders a terminal button to open a web terminal. - * Used in environments table. - */ -import terminalIconSvg from 'icons/_icon_terminal.svg'; -import tooltip from '../../vue_shared/directives/tooltip'; + /** + * Renders a terminal button to open a web terminal. + * Used in environments table. + */ + import terminalIconSvg from 'icons/_icon_terminal.svg'; + import tooltip from '../../vue_shared/directives/tooltip'; -export default { - props: { - terminalPath: { - type: String, - required: false, - default: '', + export default { + directives: { + tooltip, }, - }, - directives: { - tooltip, - }, + props: { + terminalPath: { + type: String, + required: false, + default: '', + }, + }, - data() { - return { - terminalIconSvg, - }; - }, + data() { + return { + terminalIconSvg, + }; + }, - computed: { - title() { - return 'Terminal'; + computed: { + title() { + return 'Terminal'; + }, }, - }, -}; + }; </script> <template> <a @@ -40,6 +40,7 @@ export default { :title="title" :aria-label="title" :href="terminalPath" - v-html="terminalIconSvg"> + v-html="terminalIconSvg" + > </a> </template> diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue index 2592909734f..c0be72f7401 100644 --- a/app/assets/javascripts/environments/components/environments_app.vue +++ b/app/assets/javascripts/environments/components/environments_app.vue @@ -7,6 +7,15 @@ import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin'; export default { + components: { + emptyState, + }, + + mixins: [ + CIPaginationMixin, + environmentsMixin, + ], + props: { endpoint: { type: String, @@ -37,14 +46,6 @@ required: true, }, }, - components: { - emptyState, - }, - - mixins: [ - CIPaginationMixin, - environmentsMixin, - ], created() { eventHub.$on('toggleFolder', this.toggleFolder); @@ -95,15 +96,17 @@ :tabs="tabs" @onChangeTab="onChangeTab" scope="environments" - /> + /> <div v-if="canCreateEnvironment && !isLoading" - class="nav-controls"> + class="nav-controls" + > <a :href="newEnvironmentPath" - class="btn btn-create"> - {{s__("Environments|New environment")}} + class="btn btn-create" + > + {{ s__("Environments|New environment") }} </a> </div> </div> @@ -116,13 +119,13 @@ :can-read-environment="canReadEnvironment" @onChangePage="onChangePage" > - <empty-state + <empty-state slot="emptyState" v-if="!isLoading && state.environments.length === 0" :new-path="newEnvironmentPath" :help-path="helpPagePath" :can-create-environment="canCreateEnvironment" - /> + /> </container> </div> </template> diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue index c04da4b81b7..b4eca47957e 100644 --- a/app/assets/javascripts/environments/components/environments_table.vue +++ b/app/assets/javascripts/environments/components/environments_table.vue @@ -30,63 +30,96 @@ export default { default: false, }, }, - methods: { folderUrl(model) { return `${window.location.pathname}/folders/${model.folderName}`; }, + shouldRenderFolderContent(env) { + return env.isFolder && + env.isOpen && + env.children && + env.children.length > 0; + }, }, }; </script> <template> - <div class="ci-table" role="grid"> - <div class="gl-responsive-table-row table-row-header" role="row"> - <div class="table-section section-10 environments-name" role="columnheader"> - {{s__("Environments|Environment")}} + <div + class="ci-table" + role="grid" + > + <div + class="gl-responsive-table-row table-row-header" + role="row" + > + <div + class="table-section section-10 environments-name" + role="columnheader" + > + {{ s__("Environments|Environment") }} </div> - <div class="table-section section-10 environments-deploy" role="columnheader"> - {{s__("Environments|Deployment")}} + <div + class="table-section section-10 environments-deploy" + role="columnheader" + > + {{ s__("Environments|Deployment") }} </div> - <div class="table-section section-15 environments-build" role="columnheader"> - {{s__("Environments|Job")}} + <div + class="table-section section-15 environments-build" + role="columnheader" + > + {{ s__("Environments|Job") }} </div> - <div class="table-section section-25 environments-commit" role="columnheader"> - {{s__("Environments|Commit")}} + <div + class="table-section section-25 environments-commit" + role="columnheader" + > + {{ s__("Environments|Commit") }} </div> - <div class="table-section section-10 environments-date" role="columnheader"> - {{s__("Environments|Updated")}} + <div + class="table-section section-10 environments-date" + role="columnheader" + > + {{ s__("Environments|Updated") }} </div> </div> <template - v-for="model in environments" - v-bind:model="model"> + v-for="(model, i) in environments" + :model="model"> <div is="environment-item" :model="model" :can-create-deployment="canCreateDeployment" :can-read-environment="canReadEnvironment" - /> + :key="i" + /> - <template v-if="model.isFolder && model.isOpen && model.children && model.children.length > 0"> - <div v-if="model.isLoadingFolderContent"> + <template + v-if="shouldRenderFolderContent(model)" + > + <div + v-if="model.isLoadingFolderContent" + :key="`loading-item-${i}`"> <loading-icon size="2" /> </div> <template v-else> <div is="environment-item" - v-for="children in model.children" + v-for="(children, index) in model.children" :model="children" :can-create-deployment="canCreateDeployment" :can-read-environment="canReadEnvironment" - /> + :key="`env-item-${i}-${index}`" + /> - <div> + <div :key="`sub-div-${i}`"> <div class="text-center prepend-top-10"> <a :href="folderUrl(model)" - class="btn btn-default"> - {{s__("Environments|Show all")}} + class="btn btn-default" + > + {{ s__("Environments|Show all") }} </a> </div> </div> diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.vue b/app/assets/javascripts/environments/folder/environments_folder_view.vue index 27418bad01a..5ef5e347387 100644 --- a/app/assets/javascripts/environments/folder/environments_folder_view.vue +++ b/app/assets/javascripts/environments/folder/environments_folder_view.vue @@ -3,6 +3,10 @@ import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin'; export default { + mixins: [ + environmentsMixin, + CIPaginationMixin, + ], props: { endpoint: { type: String, @@ -25,10 +29,6 @@ required: true, }, }, - mixins: [ - environmentsMixin, - CIPaginationMixin, - ], methods: { successCallback(resp) { this.saveData(resp); @@ -40,17 +40,18 @@ <div :class="cssContainerClass"> <div class="top-area" - v-if="!isLoading"> + v-if="!isLoading" + > <h4 class="js-folder-name environments-folder-name"> - {{s__("Environments|Environments")}} / <b>{{folderName}}</b> + {{ s__("Environments|Environments") }} / <b>{{ folderName }}</b> </h4> <tabs :tabs="tabs" @onChangeTab="onChangeTab" scope="environments" - /> + /> </div> <container diff --git a/app/assets/javascripts/environments/mixins/environments_mixin.js b/app/assets/javascripts/environments/mixins/environments_mixin.js index 7219b076721..34d18d55120 100644 --- a/app/assets/javascripts/environments/mixins/environments_mixin.js +++ b/app/assets/javascripts/environments/mixins/environments_mixin.js @@ -1,7 +1,7 @@ /** * Common code between environmets app and folder view */ - +import _ from 'underscore'; import Visibility from 'visibilityjs'; import Poll from '../../lib/utils/poll'; import { diff --git a/app/assets/javascripts/filterable_list.js b/app/assets/javascripts/filterable_list.js index 9e91f72b2ea..a10f027de53 100644 --- a/app/assets/javascripts/filterable_list.js +++ b/app/assets/javascripts/filterable_list.js @@ -1,4 +1,5 @@ import _ from 'underscore'; +import axios from './lib/utils/axios_utils'; /** * Makes search request for content when user types a value in the search input. @@ -54,32 +55,26 @@ export default class FilterableList { this.listFilterElement.removeEventListener('input', this.debounceFilter); } - filterResults(queryData) { + filterResults(params) { if (this.isBusy) { return false; } $(this.listHolderElement).fadeTo(250, 0.5); - return $.ajax({ - url: this.getFilterEndpoint(), - data: queryData, - type: 'GET', - dataType: 'json', - context: this, - complete: this.onFilterComplete, - beforeSend: () => { - this.isBusy = true; - }, - success: (response, textStatus, xhr) => { - this.onFilterSuccess(response, xhr, queryData); - }, - }); + this.isBusy = true; + + return axios.get(this.getFilterEndpoint(), { + params, + }).then((res) => { + this.onFilterSuccess(res, params); + this.onFilterComplete(); + }).catch(() => this.onFilterComplete()); } - onFilterSuccess(response, xhr, queryData) { - if (response.html) { - this.listHolderElement.innerHTML = response.html; + onFilterSuccess(response, queryData) { + if (response.data.html) { + this.listHolderElement.innerHTML = response.data.html; } // Change url so if user reload a page - search results are saved 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 46c80dfd45e..ff046aa286a 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import DropLab from '~/droplab/drop_lab'; import FilteredSearchContainer from './container'; diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index c05a83176f2..58ed0012f01 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import { visitUrl } from '../lib/utils/url_utility'; import Flash from '../flash'; import FilteredSearchContainer from './container'; 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 6139e81fe6d..2e859d2de3a 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js +++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import AjaxCache from '../lib/utils/ajax_cache'; import Flash from '../flash'; import FilteredSearchContainer from './container'; diff --git a/app/assets/javascripts/filtered_search/recent_searches_root.js b/app/assets/javascripts/filtered_search/recent_searches_root.js index 27e49d4fb96..c99ed63c4af 100644 --- a/app/assets/javascripts/filtered_search/recent_searches_root.js +++ b/app/assets/javascripts/filtered_search/recent_searches_root.js @@ -32,6 +32,9 @@ class RecentSearchesRoot { const state = this.store.state; this.vm = new Vue({ el: this.wrapperElement, + components: { + 'recent-searches-dropdown-content': RecentSearchesDropdownContent, + }, data() { return state; }, template: ` <recent-searches-dropdown-content @@ -40,9 +43,6 @@ class RecentSearchesRoot { :allowed-keys="allowedKeys" /> `, - components: { - 'recent-searches-dropdown-content': RecentSearchesDropdownContent, - }, }); } diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js index 44deab9288e..a0af2875ab5 100644 --- a/app/assets/javascripts/flash.js +++ b/app/assets/javascripts/flash.js @@ -10,6 +10,7 @@ const hideFlash = (flashEl, fadeTransition = true) => { flashEl.addEventListener('transitionend', () => { flashEl.remove(); + if (document.body.classList.contains('flash-shown')) document.body.classList.remove('flash-shown'); }, { once: true, passive: true, @@ -64,6 +65,7 @@ const createFlash = function createFlash( parent = document, actionConfig = null, fadeTransition = true, + addBodyClass = false, ) { const flashContainer = parent.querySelector('.flash-container'); @@ -86,6 +88,8 @@ const createFlash = function createFlash( flashContainer.style.display = 'block'; + if (addBodyClass) document.body.classList.add('flash-shown'); + return flashContainer; }; diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js index abb04d77f8f..8b4f3b05ee7 100644 --- a/app/assets/javascripts/fly_out_nav.js +++ b/app/assets/javascripts/fly_out_nav.js @@ -118,14 +118,14 @@ export const showSubLevelItems = (el) => { moveSubItemsToPosition(el, subItems); }; -export const mouseEnterTopItems = (el) => { +export const mouseEnterTopItems = (el, timeout = getHideSubItemsInterval()) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => { if (currentOpenMenu) hideMenu(currentOpenMenu); showSubLevelItems(el); - }, getHideSubItemsInterval()); + }, timeout); }; export const mouseLeaveTopItem = (el) => { diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 64f258aed64..15df7a7f989 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -2,6 +2,7 @@ /* global fuzzaldrinPlus */ import _ from 'underscore'; import fuzzaldrinPlus from 'fuzzaldrin-plus'; +import axios from './lib/utils/axios_utils'; import { visitUrl } from './lib/utils/url_utility'; import { isObject } from './lib/utils/type_utility'; @@ -212,25 +213,17 @@ GitLabDropdownRemote = (function() { }; GitLabDropdownRemote.prototype.fetchData = function() { - return $.ajax({ - url: this.dataEndpoint, - dataType: this.options.dataType, - beforeSend: (function(_this) { - return function() { - if (_this.options.beforeSend) { - return _this.options.beforeSend(); - } - }; - })(this), - success: (function(_this) { - return function(data) { - if (_this.options.success) { - return _this.options.success(data); - } - }; - })(this) - }); - // Fetch the data through ajax if the data is a string + if (this.options.beforeSend) { + this.options.beforeSend(); + } + + // Fetch the data through ajax if the data is a string + return axios.get(this.dataEndpoint) + .then(({ data }) => { + if (this.options.success) { + return this.options.success(data); + } + }); }; return GitLabDropdownRemote; diff --git a/app/assets/javascripts/graphs/graphs_show.js b/app/assets/javascripts/graphs/graphs_show.js index 36bad6db3e1..b670e907a5c 100644 --- a/app/assets/javascripts/graphs/graphs_show.js +++ b/app/assets/javascripts/graphs/graphs_show.js @@ -1,11 +1,13 @@ +import flash from '../flash'; +import { __ } from '../locale'; +import axios from '../lib/utils/axios_utils'; import ContributorsStatGraph from './stat_graph_contributors'; document.addEventListener('DOMContentLoaded', () => { - $.ajax({ - type: 'GET', - url: document.querySelector('.js-graphs-show').dataset.projectGraphPath, - dataType: 'json', - success(data) { + const url = document.querySelector('.js-graphs-show').dataset.projectGraphPath; + + axios.get(url) + .then(({ data }) => { const graph = new ContributorsStatGraph(); graph.init(data); @@ -16,6 +18,6 @@ document.addEventListener('DOMContentLoaded', () => { $('.stat-graph').fadeIn(); $('.loading-graph').hide(); - }, - }); + }) + .catch(() => flash(__('Error fetching contributors data.'))); }); diff --git a/app/assets/javascripts/group_label_subscription.js b/app/assets/javascripts/group_label_subscription.js index befaebb635e..df9429b1e02 100644 --- a/app/assets/javascripts/group_label_subscription.js +++ b/app/assets/javascripts/group_label_subscription.js @@ -1,3 +1,7 @@ +import axios from './lib/utils/axios_utils'; +import flash from './flash'; +import { __ } from './locale'; + export default class GroupLabelSubscription { constructor(container) { const $container = $(container); @@ -13,14 +17,12 @@ export default class GroupLabelSubscription { event.preventDefault(); const url = this.$unsubscribeButtons.attr('data-url'); - - $.ajax({ - type: 'POST', - url, - }).done(() => { - this.toggleSubscriptionButtons(); - this.$unsubscribeButtons.removeAttr('data-url'); - }); + axios.post(url) + .then(() => { + this.toggleSubscriptionButtons(); + this.$unsubscribeButtons.removeAttr('data-url'); + }) + .catch(() => flash(__('There was an error when unsubscribing from this label.'))); } subscribe(event) { @@ -31,12 +33,9 @@ export default class GroupLabelSubscription { this.$unsubscribeButtons.attr('data-url', url); - $.ajax({ - type: 'POST', - url, - }).done(() => { - this.toggleSubscriptionButtons(); - }); + axios.post(url) + .then(() => this.toggleSubscriptionButtons()) + .catch(() => flash(__('There was an error when subscribing to this label.'))); } toggleSubscriptionButtons() { diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue index 241e026b84c..e035ba462db 100644 --- a/app/assets/javascripts/groups/components/app.vue +++ b/app/assets/javascripts/groups/components/app.vue @@ -1,16 +1,20 @@ <script> /* global Flash */ +import { s__ } from '~/locale'; +import loadingIcon from '~/vue_shared/components/loading_icon.vue'; +import modal from '~/vue_shared/components/modal.vue'; +import { getParameterByName } from '~/lib/utils/common_utils'; +import { mergeUrlParams } from '~/lib/utils/url_utility'; + import eventHub from '../event_hub'; -import { getParameterByName } from '../../lib/utils/common_utils'; -import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import { COMMON_STR } from '../constants'; -import { mergeUrlParams } from '../../lib/utils/url_utility'; import groupsComponent from './groups.vue'; export default { components: { loadingIcon, + modal, groupsComponent, }, props: { @@ -32,6 +36,10 @@ export default { isLoading: true, isSearchEmpty: false, searchEmptyMessage: '', + showModal: false, + groupLeaveConfirmationMessage: '', + targetGroup: null, + targetParentGroup: null, }; }, computed: { @@ -42,6 +50,26 @@ export default { return this.store.getPaginationInfo(); }, }, + created() { + this.searchEmptyMessage = this.hideProjects ? + COMMON_STR.GROUP_SEARCH_EMPTY : COMMON_STR.GROUP_PROJECT_SEARCH_EMPTY; + + eventHub.$on('fetchPage', this.fetchPage); + eventHub.$on('toggleChildren', this.toggleChildren); + eventHub.$on('showLeaveGroupModal', this.showLeaveGroupModal); + eventHub.$on('updatePagination', this.updatePagination); + eventHub.$on('updateGroups', this.updateGroups); + }, + mounted() { + this.fetchAllGroups(); + }, + beforeDestroy() { + eventHub.$off('fetchPage', this.fetchPage); + eventHub.$off('toggleChildren', this.toggleChildren); + eventHub.$off('showLeaveGroupModal', this.showLeaveGroupModal); + eventHub.$off('updatePagination', this.updatePagination); + eventHub.$off('updateGroups', this.updateGroups); + }, methods: { fetchGroups({ parentId, page, filterGroupsBy, sortBy, archived, updatePagination }) { return this.service.getGroups(parentId, page, filterGroupsBy, sortBy, archived) @@ -121,14 +149,23 @@ export default { parentGroup.isOpen = false; } }, - leaveGroup(group, parentGroup) { - const targetGroup = group; - targetGroup.isBeingRemoved = true; - this.service.leaveGroup(targetGroup.leavePath) + showLeaveGroupModal(group, parentGroup) { + this.targetGroup = group; + this.targetParentGroup = parentGroup; + this.showModal = true; + this.groupLeaveConfirmationMessage = s__(`GroupsTree|Are you sure you want to leave the "${group.fullName}" group?`); + }, + hideLeaveGroupModal() { + this.showModal = false; + }, + leaveGroup() { + this.showModal = false; + this.targetGroup.isBeingRemoved = true; + this.service.leaveGroup(this.targetGroup.leavePath) .then(res => res.json()) .then((res) => { $.scrollTo(0); - this.store.removeGroup(targetGroup, parentGroup); + this.store.removeGroup(this.targetGroup, this.targetParentGroup); Flash(res.notice, 'notice'); }) .catch((err) => { @@ -137,7 +174,7 @@ export default { message = COMMON_STR.LEAVE_FORBIDDEN; } Flash(message); - targetGroup.isBeingRemoved = false; + this.targetGroup.isBeingRemoved = false; }); }, updatePagination(headers) { @@ -152,26 +189,6 @@ export default { } }, }, - created() { - this.searchEmptyMessage = this.hideProjects ? - COMMON_STR.GROUP_SEARCH_EMPTY : COMMON_STR.GROUP_PROJECT_SEARCH_EMPTY; - - eventHub.$on('fetchPage', this.fetchPage); - eventHub.$on('toggleChildren', this.toggleChildren); - eventHub.$on('leaveGroup', this.leaveGroup); - eventHub.$on('updatePagination', this.updatePagination); - eventHub.$on('updateGroups', this.updateGroups); - }, - mounted() { - this.fetchAllGroups(); - }, - beforeDestroy() { - eventHub.$off('fetchPage', this.fetchPage); - eventHub.$off('toggleChildren', this.toggleChildren); - eventHub.$off('leaveGroup', this.leaveGroup); - eventHub.$off('updatePagination', this.updatePagination); - eventHub.$off('updateGroups', this.updateGroups); - }, }; </script> @@ -190,5 +207,14 @@ export default { :search-empty-message="searchEmptyMessage" :page-info="pageInfo" /> + <modal + v-show="showModal" + :primary-button-label="__('Leave')" + kind="warning" + :title="__('Are you sure?')" + :text="groupLeaveConfirmationMessage" + @cancel="hideLeaveGroupModal" + @submit="leaveGroup" + /> </div> </template> diff --git a/app/assets/javascripts/groups/components/group_folder.vue b/app/assets/javascripts/groups/components/group_folder.vue index e60221fa08d..647c9d0046d 100644 --- a/app/assets/javascripts/groups/components/group_folder.vue +++ b/app/assets/javascripts/groups/components/group_folder.vue @@ -20,7 +20,11 @@ export default { return this.parentGroup.childrenCount > MAX_CHILDREN_COUNT; }, moreChildrenStats() { - return n__('One more item', '%d more items', this.parentGroup.childrenCount - this.parentGroup.children.length); + return n__( + 'One more item', + '%d more items', + this.parentGroup.childrenCount - this.parentGroup.children.length, + ); }, }, }; @@ -43,8 +47,9 @@ export default { <i class="fa fa-external-link" aria-hidden="true" - /> - {{moreChildrenStats}} + > + </i> + {{ moreChildrenStats }} </a> </li> </ul> diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue index 42e79a9e17a..764b130fdb8 100644 --- a/app/assets/javascripts/groups/components/group_item.vue +++ b/app/assets/javascripts/groups/components/group_item.vue @@ -75,7 +75,7 @@ export default { :id="groupDomId" :class="rowClass" class="group-row" - > + > <div class="group-row-contents" :class="{ 'project-row-contents': !isGroup }"> @@ -88,7 +88,8 @@ export default { :item="group" /> <div - class="folder-toggle-wrap"> + class="folder-toggle-wrap" + > <item-caret :is-group-open="group.isOpen" /> @@ -113,13 +114,14 @@ export default { <identicon v-else size-class="s24" - :entity-id=group.id + :entity-id="group.id" :entity-name="group.name" /> </a> </div> <div - class="title namespace-title"> + class="title namespace-title" + > <a v-tooltip :href="group.relativePath" @@ -135,7 +137,7 @@ export default { v-if="group.permission" class="user-access-role" > - {{group.permission}} + {{ group.permission }} </span> </div> <div diff --git a/app/assets/javascripts/groups/components/groups.vue b/app/assets/javascripts/groups/components/groups.vue index 75a2bf34887..adde8c8cdb3 100644 --- a/app/assets/javascripts/groups/components/groups.vue +++ b/app/assets/javascripts/groups/components/groups.vue @@ -1,47 +1,48 @@ <script> -import tablePagination from '~/vue_shared/components/table_pagination.vue'; -import eventHub from '../event_hub'; -import { getParameterByName } from '../../lib/utils/common_utils'; + import tablePagination from '~/vue_shared/components/table_pagination.vue'; + import eventHub from '../event_hub'; + import { getParameterByName } from '../../lib/utils/common_utils'; -export default { - components: { - tablePagination, - }, - props: { - groups: { - type: Array, - required: true, + export default { + components: { + tablePagination, }, - pageInfo: { - type: Object, - required: true, + props: { + groups: { + type: Array, + required: true, + }, + pageInfo: { + type: Object, + required: true, + }, + searchEmpty: { + type: Boolean, + required: true, + }, + searchEmptyMessage: { + type: String, + required: true, + }, }, - searchEmpty: { - type: Boolean, - required: true, + methods: { + change(page) { + const filterGroupsParam = getParameterByName('filter_groups'); + const sortParam = getParameterByName('sort'); + const archivedParam = getParameterByName('archived'); + eventHub.$emit('fetchPage', page, filterGroupsParam, sortParam, archivedParam); + }, }, - searchEmptyMessage: { - type: String, - required: true, - }, - }, - methods: { - change(page) { - const filterGroupsParam = getParameterByName('filter_groups'); - const sortParam = getParameterByName('sort'); - const archivedParam = getParameterByName('archived'); - eventHub.$emit('fetchPage', page, filterGroupsParam, sortParam, archivedParam); - }, - }, -}; + }; </script> <template> <div class="groups-list-tree-container"> <div v-if="searchEmpty" - class="has-no-search-results"> - {{searchEmptyMessage}} + class="has-no-search-results" + > + {{ searchEmptyMessage }} </div> <group-folder v-if="!searchEmpty" @@ -50,7 +51,7 @@ export default { <table-pagination v-if="!searchEmpty" :change="change" - :pageInfo="pageInfo" + :page-info="pageInfo" /> </div> </template> diff --git a/app/assets/javascripts/groups/components/item_actions.vue b/app/assets/javascripts/groups/components/item_actions.vue index 0dd0783ce06..87065b3d6e3 100644 --- a/app/assets/javascripts/groups/components/item_actions.vue +++ b/app/assets/javascripts/groups/components/item_actions.vue @@ -1,15 +1,12 @@ <script> -import { s__ } from '~/locale'; import tooltip from '~/vue_shared/directives/tooltip'; import icon from '~/vue_shared/components/icon.vue'; -import modal from '~/vue_shared/components/modal.vue'; import eventHub from '../event_hub'; import { COMMON_STR } from '../constants'; export default { components: { icon, - modal, }, directives: { tooltip, @@ -25,11 +22,6 @@ export default { required: true, }, }, - data() { - return { - modalStatus: false, - }; - }, computed: { leaveBtnTitle() { return COMMON_STR.LEAVE_BTN_TITLE; @@ -37,17 +29,10 @@ export default { editBtnTitle() { return COMMON_STR.EDIT_BTN_TITLE; }, - leaveConfirmationMessage() { - return s__(`GroupsTree|Are you sure you want to leave the "${this.group.fullName}" group?`); - }, }, methods: { onLeaveGroup() { - this.modalStatus = true; - }, - leaveGroup() { - this.modalStatus = false; - eventHub.$emit('leaveGroup', this.group, this.parentGroup); + eventHub.$emit('showLeaveGroupModal', this.group, this.parentGroup); }, }, }; @@ -78,14 +63,5 @@ export default { class="leave-group btn no-expand"> <icon name="leave"/> </a> - <modal - v-show="modalStatus" - :primary-button-label="__('Leave')" - kind="warning" - :title="__('Are you sure?')" - :text="__('Are you sure you want to leave this group?')" - :body="leaveConfirmationMessage" - @submit="leaveGroup" - /> </div> </template> diff --git a/app/assets/javascripts/groups/components/item_caret.vue b/app/assets/javascripts/groups/components/item_caret.vue index 9e90fe2b701..2a5bec5e86c 100644 --- a/app/assets/javascripts/groups/components/item_caret.vue +++ b/app/assets/javascripts/groups/components/item_caret.vue @@ -2,6 +2,9 @@ import icon from '~/vue_shared/components/icon.vue'; export default { + components: { + icon, + }, props: { isGroupOpen: { type: Boolean, @@ -9,9 +12,6 @@ export default { default: false, }, }, - components: { - icon, - }, computed: { iconClass() { return this.isGroupOpen ? 'angle-down' : 'angle-right'; diff --git a/app/assets/javascripts/groups/components/item_stats.vue b/app/assets/javascripts/groups/components/item_stats.vue index 2e42fb6c9a6..168b4e4af2c 100644 --- a/app/assets/javascripts/groups/components/item_stats.vue +++ b/app/assets/javascripts/groups/components/item_stats.vue @@ -1,39 +1,44 @@ <script> -import icon from '~/vue_shared/components/icon.vue'; -import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; -import { ITEM_TYPE, VISIBILITY_TYPE_ICON, GROUP_VISIBILITY_TYPE, PROJECT_VISIBILITY_TYPE } from '../constants'; -import itemStatsValue from './item_stats_value.vue'; + import icon from '~/vue_shared/components/icon.vue'; + import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; + import { + ITEM_TYPE, + VISIBILITY_TYPE_ICON, + GROUP_VISIBILITY_TYPE, + PROJECT_VISIBILITY_TYPE, + } from '../constants'; + import itemStatsValue from './item_stats_value.vue'; -export default { - components: { - icon, - timeAgoTooltip, - itemStatsValue, - }, - props: { - item: { - type: Object, - required: true, + export default { + components: { + icon, + timeAgoTooltip, + itemStatsValue, }, - }, - computed: { - visibilityIcon() { - return VISIBILITY_TYPE_ICON[this.item.visibility]; + props: { + item: { + type: Object, + required: true, + }, }, - visibilityTooltip() { - if (this.item.type === ITEM_TYPE.GROUP) { - return GROUP_VISIBILITY_TYPE[this.item.visibility]; - } - return PROJECT_VISIBILITY_TYPE[this.item.visibility]; + computed: { + visibilityIcon() { + return VISIBILITY_TYPE_ICON[this.item.visibility]; + }, + visibilityTooltip() { + if (this.item.type === ITEM_TYPE.GROUP) { + return GROUP_VISIBILITY_TYPE[this.item.visibility]; + } + return PROJECT_VISIBILITY_TYPE[this.item.visibility]; + }, + isProject() { + return this.item.type === ITEM_TYPE.PROJECT; + }, + isGroup() { + return this.item.type === ITEM_TYPE.GROUP; + }, }, - isProject() { - return this.item.type === ITEM_TYPE.PROJECT; - }, - isGroup() { - return this.item.type === ITEM_TYPE.GROUP; - }, - }, -}; + }; </script> <template> diff --git a/app/assets/javascripts/groups/components/item_stats_value.vue b/app/assets/javascripts/groups/components/item_stats_value.vue index f441cabf6d2..08d0bf6e344 100644 --- a/app/assets/javascripts/groups/components/item_stats_value.vue +++ b/app/assets/javascripts/groups/components/item_stats_value.vue @@ -1,52 +1,52 @@ <script> -import tooltip from '~/vue_shared/directives/tooltip'; -import icon from '~/vue_shared/components/icon.vue'; + import tooltip from '~/vue_shared/directives/tooltip'; + import icon from '~/vue_shared/components/icon.vue'; -export default { - props: { - title: { - type: String, - required: false, - default: '', + export default { + components: { + icon, }, - cssClass: { - type: String, - required: false, - default: '', + directives: { + tooltip, }, - iconName: { - type: String, - required: true, + props: { + title: { + type: String, + required: false, + default: '', + }, + cssClass: { + type: String, + required: false, + default: '', + }, + iconName: { + type: String, + required: true, + }, + tooltipPlacement: { + type: String, + required: false, + default: 'bottom', + }, + /** + * value could either be number or string + * as `memberCount` is always passed as string + * while `subgroupCount` & `projectCount` + * are always number + */ + value: { + type: [Number, String], + required: false, + default: '', + }, }, - tooltipPlacement: { - type: String, - required: false, - default: 'bottom', + computed: { + isValuePresent() { + return this.value !== ''; + }, }, - /** - * value could either be number or string - * as `memberCount` is always passed as string - * while `subgroupCount` & `projectCount` - * are always number - */ - value: { - type: [Number, String], - required: false, - default: '', - }, - }, - directives: { - tooltip, - }, - components: { - icon, - }, - computed: { - isValuePresent() { - return this.value !== ''; - }, - }, -}; + }; </script> <template> @@ -57,12 +57,12 @@ export default { :class="cssClass" :title="title" > - <icon :name="iconName"/> + <icon :name="iconName" /> <span v-if="isValuePresent" class="stat-value" > - {{value}} + {{ value }} </span> </span> </template> diff --git a/app/assets/javascripts/groups/groups_filterable_list.js b/app/assets/javascripts/groups/groups_filterable_list.js index 2db233b09da..31d56d15c23 100644 --- a/app/assets/javascripts/groups/groups_filterable_list.js +++ b/app/assets/javascripts/groups/groups_filterable_list.js @@ -1,6 +1,6 @@ import FilterableList from '~/filterable_list'; import eventHub from './event_hub'; -import { getParameterByName } from '../lib/utils/common_utils'; +import { normalizeHeaders, getParameterByName } from '../lib/utils/common_utils'; export default class GroupFilterableList extends FilterableList { constructor({ form, filter, holder, filterEndpoint, pagePath, dropdownSel, filterInputField }) { @@ -94,23 +94,14 @@ export default class GroupFilterableList extends FilterableList { this.form.querySelector(`[name="${this.filterInputField}"]`).value = ''; } - onFilterSuccess(data, xhr, queryData) { + onFilterSuccess(res, queryData) { const currentPath = this.getPagePath(queryData); - const paginationData = { - 'X-Per-Page': xhr.getResponseHeader('X-Per-Page'), - 'X-Page': xhr.getResponseHeader('X-Page'), - 'X-Total': xhr.getResponseHeader('X-Total'), - 'X-Total-Pages': xhr.getResponseHeader('X-Total-Pages'), - 'X-Next-Page': xhr.getResponseHeader('X-Next-Page'), - 'X-Prev-Page': xhr.getResponseHeader('X-Prev-Page'), - }; - window.history.replaceState({ page: currentPath, }, document.title, currentPath); - eventHub.$emit('updateGroups', data, Object.prototype.hasOwnProperty.call(queryData, this.filterInputField)); - eventHub.$emit('updatePagination', paginationData); + eventHub.$emit('updateGroups', res.data, Object.prototype.hasOwnProperty.call(queryData, this.filterInputField)); + eventHub.$emit('updatePagination', normalizeHeaders(res.headers)); } } diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js index 8b850765a1b..57eaac72906 100644 --- a/app/assets/javascripts/groups/index.js +++ b/app/assets/javascripts/groups/index.js @@ -10,7 +10,7 @@ import groupItemComponent from './components/group_item.vue'; Vue.use(Translate); -document.addEventListener('DOMContentLoaded', () => { +export default () => { const el = document.getElementById('js-groups-tree'); // Don't do anything if element doesn't exist (No groups) @@ -71,4 +71,4 @@ document.addEventListener('DOMContentLoaded', () => { }); }, }); -}); +}; diff --git a/app/assets/javascripts/groups/service/groups_service.js b/app/assets/javascripts/groups/service/groups_service.js index 639410384c2..b79ba291463 100644 --- a/app/assets/javascripts/groups/service/groups_service.js +++ b/app/assets/javascripts/groups/service/groups_service.js @@ -1,7 +1,5 @@ import Vue from 'vue'; -import VueResource from 'vue-resource'; - -Vue.use(VueResource); +import '../../vue_shared/vue_resource_interceptor'; export default class GroupsService { constructor(endpoint) { diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue index 704dff981df..a8459b011df 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/list.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/list.vue @@ -32,7 +32,6 @@ this.$emit('toggleCollapsed'); }, }, - }; </script> diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue index 26a70f6e748..89981ab2c65 100644 --- a/app/assets/javascripts/ide/components/ide.vue +++ b/app/assets/javascripts/ide/components/ide.vue @@ -1,79 +1,82 @@ <script> -import { mapState, mapGetters } from 'vuex'; -import ideSidebar from './ide_side_bar.vue'; -import ideContextbar from './ide_context_bar.vue'; -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'; + import { mapState, mapGetters } from 'vuex'; + import ideSidebar from './ide_side_bar.vue'; + import ideContextbar from './ide_context_bar.vue'; + 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 { - props: { - emptyStateSvgPath: { - type: String, - required: true, + export default { + components: { + ideSidebar, + ideContextbar, + repoTabs, + repoFileButtons, + ideStatusBar, + repoEditor, + repoPreview, }, - }, - computed: { - ...mapState([ - 'currentBlobView', - 'selectedFile', - ]), - ...mapGetters([ - 'changedFiles', - 'activeFile', - ]), - }, - components: { - ideSidebar, - ideContextbar, - repoTabs, - repoFileButtons, - ideStatusBar, - repoEditor, - repoPreview, - }, - mounted() { - const returnValue = 'Are you sure you want to lose unsaved changes?'; - window.onbeforeunload = (e) => { - if (!this.changedFiles.length) return undefined; + props: { + emptyStateSvgPath: { + type: String, + required: true, + }, + }, + computed: { + ...mapState([ + 'currentBlobView', + 'selectedFile', + ]), + ...mapGetters([ + 'changedFiles', + 'activeFile', + ]), + }, + mounted() { + const returnValue = 'Are you sure you want to lose unsaved changes?'; + window.onbeforeunload = (e) => { + if (!this.changedFiles.length) return undefined; - Object.assign(e, { - returnValue, - }); - return returnValue; - }; - }, -}; + Object.assign(e, { + returnValue, + }); + return returnValue; + }; + }, + }; </script> <template> - <div + <div class="ide-view" > - <ide-sidebar/> + <ide-sidebar /> <div class="multi-file-edit-pane" > <template - v-if="activeFile"> + v-if="activeFile" + > <repo-tabs/> <component class="multi-file-edit-pane-content" :is="currentBlobView" /> - <repo-file-buttons/> + <repo-file-buttons /> <ide-status-bar - :file="selectedFile"/> + :file="selectedFile" + /> </template> <template - v-else> + v-else + > <div class="ide-empty-state"> <div class="row js-empty-state"> <div class="col-xs-12"> <div class="svg-content svg-250"> - <img :src="emptyStateSvgPath"> + <img :src="emptyStateSvgPath" /> </div> </div> <div class="col-xs-12"> @@ -82,7 +85,8 @@ export default { Welcome to the GitLab IDE </h4> <p> - You can select a file in the left sidebar to begin editing and use the right sidebar to commit your changes. + You can select a file in the left sidebar to begin + editing and use the right sidebar to commit your changes. </p> </div> </div> diff --git a/app/assets/javascripts/ide/components/ide_context_bar.vue b/app/assets/javascripts/ide/components/ide_context_bar.vue index 78c01272af6..dd947f66969 100644 --- a/app/assets/javascripts/ide/components/ide_context_bar.vue +++ b/app/assets/javascripts/ide/components/ide_context_bar.vue @@ -1,59 +1,59 @@ <script> -import { mapGetters, mapState, mapActions } from 'vuex'; -import repoCommitSection from './repo_commit_section.vue'; -import icon from '../../vue_shared/components/icon.vue'; -import panelResizer from '../../vue_shared/components/panel_resizer.vue'; + import { mapGetters, mapState, mapActions } from 'vuex'; + import repoCommitSection from './repo_commit_section.vue'; + import icon from '../../vue_shared/components/icon.vue'; + import panelResizer from '../../vue_shared/components/panel_resizer.vue'; -export default { - data() { - return { - width: 290, - }; - }, - components: { - repoCommitSection, - icon, - panelResizer, - }, - computed: { - ...mapState([ - 'rightPanelCollapsed', - ]), - ...mapGetters([ - 'changedFiles', - ]), - currentIcon() { - return this.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right'; + export default { + components: { + repoCommitSection, + icon, + panelResizer, }, - maxSize() { - return window.innerWidth / 2; + data() { + return { + width: 290, + }; }, - panelStyle() { - if (!this.rightPanelCollapsed) { - return { width: `${this.width}px` }; - } - return {}; + 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, - }); + methods: { + ...mapActions([ + 'setPanelCollapsedStatus', + 'setResizingStatus', + ]), + toggleCollapsed() { + this.setPanelCollapsedStatus({ + side: 'right', + collapsed: !this.rightPanelCollapsed, + }); + }, + resizingStarted() { + this.setResizingStatus(true); + }, + resizingEnded() { + this.setResizingStatus(false); + }, }, - resizingStarted() { - this.setResizingStatus(true); - }, - resizingEnded() { - this.setResizingStatus(false); - }, - }, -}; + }; </script> <template> @@ -64,17 +64,17 @@ export default { }" :style="panelStyle" > - <div - class="multi-file-commit-panel-section"> + <div class="multi-file-commit-panel-section"> <header class="multi-file-commit-panel-header" :class="{ - 'is-collapsed': rightPanelCollapsed, - }" - > + 'is-collapsed': rightPanelCollapsed, + }" + > <div class="multi-file-commit-panel-header-title" - v-if="!rightPanelCollapsed"> + v-if="!rightPanelCollapsed" + > <icon name="list-bulleted" :size="18" @@ -92,8 +92,7 @@ export default { /> </button> </header> - <repo-commit-section - class=""/> + <repo-commit-section /> </div> <panel-resizer :size.sync="width" @@ -103,6 +102,7 @@ export default { :max-size="maxSize" @resize-start="resizingStarted" @resize-end="resizingEnded" - side="left"/> + side="left" + /> </div> </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 bd3a521ff43..af2f7341a91 100644 --- a/app/assets/javascripts/ide/components/ide_project_branches_tree.vue +++ b/app/assets/javascripts/ide/components/ide_project_branches_tree.vue @@ -28,20 +28,20 @@ export default { <div class="branch-header-title"> <icon name="branch" - :size="12"> - </icon> + :size="12" + /> {{ branch.name }} </div> <div class="branch-header-btns"> <new-dropdown :project-id="projectId" :branch="branch.name" - path=""/> + path="" + /> </div> </div> <div> - <repo-tree - :treeId="branch.treeId"/> + <repo-tree :tree-id="branch.treeId" /> </div> </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 61daba6d176..ed49a0e72a2 100644 --- a/app/assets/javascripts/ide/components/ide_project_tree.vue +++ b/app/assets/javascripts/ide/components/ide_project_tree.vue @@ -19,9 +19,10 @@ export default { <template> <div class="projects-sidebar"> <div class="context-header"> - <a - :title="project.name" - :href="project.web_url"> + <a + :title="project.name" + :href="project.web_url" + > <div class="avatar-container s40 project-avatar"> <project-avatar-image class="avatar-container project-avatar" @@ -29,7 +30,7 @@ export default { :img-src="project.avatar_url" :img-alt="project.name" :img-size="40" - /> + /> </div> <div class="sidebar-context-title"> {{ project.name }} @@ -38,10 +39,11 @@ export default { </div> <div class="multi-file-commit-panel-inner-scroll"> <branches-tree - v-for="(branch, index) in project.branches" + v-for="branch in project.branches" :key="branch.name" :project-id="project.path_with_namespace" - :branch="branch"/> + :branch="branch" + /> </div> </div> </template> diff --git a/app/assets/javascripts/ide/components/ide_repo_tree.vue b/app/assets/javascripts/ide/components/ide_repo_tree.vue index bd89ebe47d9..4651e345d75 100644 --- a/app/assets/javascripts/ide/components/ide_repo_tree.vue +++ b/app/assets/javascripts/ide/components/ide_repo_tree.vue @@ -44,28 +44,31 @@ export default { </script> <template> -<div> - <div class="ide-file-list"> - <table class="table"> - <tbody - v-if="treeId"> - <repo-previous-directory - v-if="hasPreviousDirectory" - /> - <div - class="multi-file-loading-container" - v-if="showLoading" - v-for="n in 3" - :key="n"> - <skeleton-loading-container/> - </div> - <repo-file - v-for="file in fetchedList" - :key="file.key" - :file="file" - /> - </tbody> - </table> + <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> -</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 c30018e04b0..a68f8ce0169 100644 --- a/app/assets/javascripts/ide/components/ide_side_bar.vue +++ b/app/assets/javascripts/ide/components/ide_side_bar.vue @@ -1,85 +1,88 @@ <script> -import { mapState, mapActions } from 'vuex'; -import projectTree from './ide_project_tree.vue'; -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 { mapState, mapActions } from 'vuex'; + import projectTree from './ide_project_tree.vue'; + 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'; -export default { - data() { - return { - width: 290, - }; - }, - components: { - projectTree, - icon, - panelResizer, - skeletonLoadingContainer, - }, - computed: { - ...mapState([ - 'loading', - 'projects', - 'leftPanelCollapsed', - ]), - currentIcon() { - return this.leftPanelCollapsed ? 'angle-double-right' : 'angle-double-left'; + export default { + components: { + projectTree, + icon, + panelResizer, + skeletonLoadingContainer, }, - maxSize() { - return window.innerWidth / 2; + data() { + return { + width: 290, + }; }, - panelStyle() { - if (!this.leftPanelCollapsed) { - return { width: `${this.width}px` }; - } - return {}; + 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; + }, }, - showLoading() { - return this.loading; + methods: { + ...mapActions([ + 'setPanelCollapsedStatus', + 'setResizingStatus', + ]), + toggleCollapsed() { + this.setPanelCollapsedStatus({ + side: 'left', + collapsed: !this.leftPanelCollapsed, + }); + }, + resizingStarted() { + this.setResizingStatus(true); + }, + resizingEnded() { + this.setResizingStatus(false); + }, }, - }, - methods: { - ...mapActions([ - 'setPanelCollapsedStatus', - 'setResizingStatus', - ]), - 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" - > + class="multi-file-commit-panel" + :class="{ + 'is-collapsed': leftPanelCollapsed, + }" + :style="panelStyle" + > <div class="multi-file-commit-panel-inner"> - <div - class="multi-file-loading-container" - v-if="showLoading" - v-for="n in 3" - :key="n"> - <skeleton-loading-container/> - </div> + <template v-if="showLoading"> + <div + class="multi-file-loading-container" + v-for="n in 3" + :key="n" + > + <skeleton-loading-container /> + </div> + </template> <project-tree - v-for="(project, index) in projects" + v-for="project in projects" :key="project.id" - :project="project"/> + :project="project" + /> </div> <button type="button" @@ -93,7 +96,9 @@ export default { <span v-if="!leftPanelCollapsed" class="collapse-text" - >Collapse sidebar</span> + > + Collapse sidebar + </span> </button> <panel-resizer :size.sync="width" @@ -103,6 +108,7 @@ export default { :max-size="maxSize" @resize-start="resizingStarted" @resize-end="resizingEnded" - side="right"/> + side="right" + /> </div> </template> diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue index a24abadd936..e48c446c4a4 100644 --- a/app/assets/javascripts/ide/components/ide_status_bar.vue +++ b/app/assets/javascripts/ide/components/ide_status_bar.vue @@ -1,70 +1,65 @@ <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'; + 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'; -export default { - props: { - file: { - type: Object, - required: true, + export default { + components: { + icon, }, - }, - components: { - icon, - }, - directives: { - tooltip, - }, - mixins: [ - timeAgoMixin, - ], - computed: { - ...mapState([ - 'selectedFile', - ]), - }, -}; + directives: { + tooltip, + }, + mixins: [ + timeAgoMixin, + ], + props: { + file: { + type: Object, + required: true, + }, + }, + computed: { + ...mapState([ + 'selectedFile', + ]), + }, + }; </script> <template> - <div - class="ide-status-bar"> + <div class="ide-status-bar"> <div> <icon name="branch" - :size="12"> - </icon> + :size="12" + /> {{ selectedFile.branchId }} </div> <div> - <div - v-if="selectedFile.lastCommit && selectedFile.lastCommit.id"> + <div v-if="selectedFile.lastCommit && selectedFile.lastCommit.id"> Last commit: <a v-tooltip :title="selectedFile.lastCommit.message" - :href="selectedFile.lastCommit.url"> - {{ timeFormated(selectedFile.lastCommit.updatedAt) }} by + :href="selectedFile.lastCommit.url" + > + {{ timeFormated(selectedFile.lastCommit.updatedAt) }} by {{ selectedFile.lastCommit.author }} </a> - </div> + </div> </div> - <div - class="text-right"> + <div class="text-right"> {{ selectedFile.name }} </div> - <div - class="text-right"> + <div class="text-right"> {{ selectedFile.eol }} </div> - <div - class="text-right"> + <div class="text-right"> {{ file.editorRow }}:{{ file.editorColumn }} </div> - <div - class="text-right"> + <div class="text-right"> {{ selectedFile.fileLanguage }} </div> </div> diff --git a/app/assets/javascripts/ide/components/new_branch_form.vue b/app/assets/javascripts/ide/components/new_branch_form.vue index 2119d373d31..56e31256132 100644 --- a/app/assets/javascripts/ide/components/new_branch_form.vue +++ b/app/assets/javascripts/ide/components/new_branch_form.vue @@ -21,6 +21,13 @@ 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', @@ -55,13 +62,6 @@ })); }, }, - 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'); - }, }; </script> diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue index d475813c4f7..ef653357f5f 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/index.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue @@ -4,6 +4,11 @@ import icon from '../../../vue_shared/components/icon.vue'; export default { + components: { + icon, + newModal, + upload, + }, props: { branch: { type: String, @@ -18,11 +23,6 @@ default: null, }, }, - components: { - icon, - newModal, - upload, - }, data() { return { openModal: false, diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue index 0312f56efbd..36cd825c6dd 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue @@ -4,6 +4,9 @@ import modal from '../../../vue_shared/components/modal.vue'; export default { + components: { + modal, + }, props: { branchId: { type: String, @@ -27,28 +30,6 @@ entryName: this.path !== '' ? `${this.path}/` : '', }; }, - components: { - modal, - }, - methods: { - ...mapActions([ - 'createTempEntry', - ]), - createEntryInStore() { - this.createTempEntry({ - projectId: this.currentProjectId, - branchId: this.branchId, - parent: this.parent, - name: this.entryName.replace(new RegExp(`^${this.path}/`), ''), - type: this.type, - }); - - this.hideModal(); - }, - hideModal() { - this.$emit('hide'); - }, - }, computed: { ...mapState([ 'currentProjectId', @@ -78,6 +59,25 @@ mounted() { this.$refs.fieldName.focus(); }, + methods: { + ...mapActions([ + 'createTempEntry', + ]), + createEntryInStore() { + this.createTempEntry({ + projectId: this.currentProjectId, + branchId: this.branchId, + parent: this.parent, + name: this.entryName.replace(new RegExp(`^${this.path}/`), ''), + type: this.type, + }); + + this.hideModal(); + }, + hideModal() { + this.$emit('hide'); + }, + }, }; </script> diff --git a/app/assets/javascripts/ide/components/new_dropdown/upload.vue b/app/assets/javascripts/ide/components/new_dropdown/upload.vue index 2a2f2a241fc..6244737fa43 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/upload.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/upload.vue @@ -18,6 +18,12 @@ 'currentProjectId', ]), }, + mounted() { + this.$refs.fileUpload.addEventListener('change', this.openFile); + }, + beforeDestroy() { + this.$refs.fileUpload.removeEventListener('change', this.openFile); + }, methods: { ...mapActions([ 'createTempEntry', @@ -59,12 +65,6 @@ this.$refs.fileUpload.click(); }, }, - mounted() { - this.$refs.fileUpload.addEventListener('change', this.openFile); - }, - beforeDestroy() { - this.$refs.fileUpload.removeEventListener('change', this.openFile); - }, }; </script> diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue index 979721dcb5a..96b1bb78c1d 100644 --- a/app/assets/javascripts/ide/components/repo_commit_section.vue +++ b/app/assets/javascripts/ide/components/repo_commit_section.vue @@ -49,7 +49,9 @@ export default { const createNewBranch = newBranch || this.startNewMR; const payload = { - branch: createNewBranch ? `${this.currentBranchId}-${new Date().getTime().toString()}` : this.currentBranchId, + branch: createNewBranch ? + `${this.currentBranchId}-${new Date().getTime().toString()}` : + this.currentBranchId, commit_message: this.commitMessage, actions: this.changedFiles.map(f => ({ action: f.tempFile ? 'create' : 'update', @@ -66,12 +68,8 @@ export default { this.commitChanges({ payload, newMr: this.startNewMR }) .then(() => { this.submitCommitsLoading = false; - this.$store.dispatch('getTreeData', { - projectId: this.currentProjectId, - branch: this.currentBranchId, - endpoint: `/tree/${this.currentBranchId}`, - force: true, - }); + this.commitMessage = ''; + this.startNewMR = false; }) .catch(() => { this.submitCommitsLoading = false; @@ -103,69 +101,71 @@ export default { </script> <template> -<div class="multi-file-commit-panel-section"> - <modal - v-if="showNewBranchModal" - :primary-button-label="__('Create new branch')" - kind="primary" - :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)" - /> - <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" - > - <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" - /> - Merge Request - </label> - <button - type="submit" - :disabled="commitButtonDisabled" - class="btn btn-default btn-sm append-right-10 prepend-left-10" - > - <i - v-if="submitCommitsLoading" - class="js-commit-loading-icon fa fa-spinner fa-spin" - aria-hidden="true" - aria-label="loading" + <div class="multi-file-commit-panel-section"> + <modal + v-if="showNewBranchModal" + :primary-button-label="__('Create new branch')" + kind="primary" + :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)" + /> + <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" + > + <div class="multi-file-commit-fieldset"> + <textarea + class="form-control multi-file-commit-message" + name="commit-message" + v-model="commitMessage" + placeholder="Commit message" > - </i> - Commit - </button> - <div - class="multi-file-commit-message-count" - > - {{ commitMessageCount }} + </textarea> </div> - </div> - </form> -</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" + /> + 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" + > + </i> + Commit + </button> + <div + class="multi-file-commit-message-count" + > + {{ commitMessageCount }} + </div> + </div> + </form> + </div> </template> diff --git a/app/assets/javascripts/ide/components/repo_edit_button.vue b/app/assets/javascripts/ide/components/repo_edit_button.vue index 42d5d709209..c43e9163340 100644 --- a/app/assets/javascripts/ide/components/repo_edit_button.vue +++ b/app/assets/javascripts/ide/components/repo_edit_button.vue @@ -40,7 +40,7 @@ export default { aria-hidden="true"> </i> <span> - {{buttonLabel}} + {{ buttonLabel }} </span> </button> <modal diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index 343fd0a5300..f99228012f4 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -6,6 +6,38 @@ import monacoLoader from '../monaco_loader'; import Editor from '../lib/editor'; export default { + computed: { + ...mapGetters([ + 'activeFile', + 'activeFileExtension', + ]), + ...mapState([ + 'leftPanelCollapsed', + 'rightPanelCollapsed', + 'panelResizing', + ]), + shouldHideEditor() { + return this.activeFile.binary && !this.activeFile.raw; + }, + }, + watch: { + activeFile(oldVal, newVal) { + if (newVal && !newVal.active) { + this.initMonaco(); + } + }, + leftPanelCollapsed() { + this.editor.updateDimensions(); + }, + rightPanelCollapsed() { + this.editor.updateDimensions(); + }, + panelResizing(isResizing) { + if (isResizing === false) { + this.editor.updateDimensions(); + } + }, + }, beforeDestroy() { this.editor.dispose(); }, @@ -38,7 +70,10 @@ export default { this.editor.createInstance(this.$refs.editor); }) .then(() => this.setupEditor()) - .catch(() => flash('Error setting up monaco. Please try again.')); + .catch((err) => { + flash('Error setting up monaco. Please try again.', 'alert', document, null, false, true); + throw err; + }); }, setupEditor() { if (!this.activeFile) return; @@ -78,38 +113,6 @@ export default { }); }, }, - watch: { - activeFile(oldVal, newVal) { - if (newVal && !newVal.active) { - this.initMonaco(); - } - }, - leftPanelCollapsed() { - this.editor.updateDimensions(); - }, - rightPanelCollapsed() { - this.editor.updateDimensions(); - }, - panelResizing(isResizing) { - if (isResizing === false) { - this.editor.updateDimensions(); - } - }, - }, - computed: { - ...mapGetters([ - 'activeFile', - 'activeFileExtension', - ]), - ...mapState([ - 'leftPanelCollapsed', - 'rightPanelCollapsed', - 'panelResizing', - ]), - shouldHideEditor() { - return this.activeFile.binary && !this.activeFile.raw; - }, - }, }; </script> diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue index c8b0441d81c..110918872fb 100644 --- a/app/assets/javascripts/ide/components/repo_file.vue +++ b/app/assets/javascripts/ide/components/repo_file.vue @@ -6,14 +6,14 @@ import fileIcon from '../../vue_shared/components/file_icon.vue'; export default { - mixins: [ - timeAgoMixin, - ], components: { skeletonLoadingContainer, newDropdown, fileIcon, }, + mixins: [ + timeAgoMixin, + ], props: { file: { type: Object, @@ -35,9 +35,12 @@ return this.file.type === 'tree'; }, levelIndentation() { - return { - marginLeft: `${this.file.level * 16}px`, - }; + if (this.file.level > 0) { + return { + marginLeft: `${this.file.level * 16}px`, + }; + } + return {}; }, shortId() { return this.file.id.substr(0, 8); @@ -60,6 +63,11 @@ }; }, }, + updated() { + if (this.file.type === 'blob' && this.file.active) { + this.$el.scrollIntoView(); + } + }, methods: { clickFile(row) { // Manual Action if a tree is selected/opened @@ -72,11 +80,6 @@ this.$router.push(`/project${row.url}`); }, }, - updated() { - if (this.file.type === 'blob' && this.file.active) { - this.$el.scrollIntoView(); - } - }, }; </script> @@ -99,8 +102,7 @@ :opened="file.opened" :style="levelIndentation" :size="16" - > - </file-icon> + /> {{ file.name }} </a> <new-dropdown @@ -108,10 +110,11 @@ :project-id="file.projectId" :branch="file.branchId" :path="file.path" - :parent="file"/> + :parent="file" + /> <i class="fa" - v-if="changedClass" + v-if="file.changed || file.tempFile" :class="changedClass" aria-hidden="true" > diff --git a/app/assets/javascripts/ide/components/repo_file_buttons.vue b/app/assets/javascripts/ide/components/repo_file_buttons.vue index 34f0d51819a..aabc0d8eada 100644 --- a/app/assets/javascripts/ide/components/repo_file_buttons.vue +++ b/app/assets/javascripts/ide/components/repo_file_buttons.vue @@ -35,20 +35,24 @@ export default { <div class="btn-group" role="group" - aria-label="File actions"> + aria-label="File actions" + > <a :href="activeFile.blamePath" - class="btn btn-default btn-sm blame"> + class="btn btn-default btn-sm blame" + > Blame </a> <a :href="activeFile.commitsPath" - class="btn btn-default btn-sm history"> + class="btn btn-default btn-sm history" + > History </a> <a :href="activeFile.permalink" - class="btn btn-default btn-sm permalink"> + class="btn btn-default btn-sm permalink" + > Permalink </a> </div> diff --git a/app/assets/javascripts/ide/components/repo_loading_file.vue b/app/assets/javascripts/ide/components/repo_loading_file.vue index 7eb840c7608..3aeb6f0b28f 100644 --- a/app/assets/javascripts/ide/components/repo_loading_file.vue +++ b/app/assets/javascripts/ide/components/repo_loading_file.vue @@ -25,15 +25,13 @@ /> </td> <template v-if="!leftPanelCollapsed"> - <td - class="hidden-sm hidden-xs"> + <td class="hidden-sm hidden-xs"> <skeleton-loading-container :small="true" /> </td> - <td - class="hidden-xs"> + <td class="hidden-xs"> <skeleton-loading-container class="animation-container-right" :small="true" diff --git a/app/assets/javascripts/ide/components/repo_preview.vue b/app/assets/javascripts/ide/components/repo_preview.vue index 3d1e0297bd5..e47270a9855 100644 --- a/app/assets/javascripts/ide/components/repo_preview.vue +++ b/app/assets/javascripts/ide/components/repo_preview.vue @@ -1,65 +1,71 @@ <script> -import { mapGetters } from 'vuex'; -import LineHighlighter from '../../line_highlighter'; -import syntaxHighlight from '../../syntax_highlight'; + 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'; + export default { + computed: { + ...mapGetters([ + 'activeFile', + ]), + renderErrorTooLarge() { + return this.activeFile.renderError === 'too_large'; + }, }, - }, - methods: { - highlightFile() { - syntaxHighlight($(this.$el).find('.file-content')); - }, - }, - mounted() { - this.highlightFile(); - this.lineHighlighter = new LineHighlighter({ - fileHolderSelector: '.blob-viewer-container', - scrollFileHolder: true, - }); - }, - updated() { - this.$nextTick(() => { + 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 + 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> -</div> </template> diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue index e7684884b2c..5ed7bddf6ae 100644 --- a/app/assets/javascripts/ide/components/repo_tab.vue +++ b/app/assets/javascripts/ide/components/repo_tab.vue @@ -1,48 +1,46 @@ <script> -import { mapActions } from 'vuex'; -import fileIcon from '../../vue_shared/components/file_icon.vue'; + import { mapActions } from 'vuex'; + import fileIcon from '../../vue_shared/components/file_icon.vue'; -export default { - props: { - tab: { - type: Object, - required: true, + export default { + components: { + fileIcon, }, - }, - components: { - fileIcon, - }, - computed: { - closeLabel() { - if (this.tab.changed || this.tab.tempFile) { - return `${this.tab.name} changed`; - } - return `Close ${this.tab.name}`; + props: { + tab: { + type: Object, + required: true, + }, }, - 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; + computed: { + closeLabel() { + if (this.tab.changed || this.tab.tempFile) { + return `${this.tab.name} changed`; + } + 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; + }, }, - }, - methods: { - ...mapActions([ - 'closeFile', - ]), - clickFile(tab) { - this.$router.push(`/project${tab.url}`); + methods: { + ...mapActions([ + 'closeFile', + ]), + clickFile(tab) { + this.$router.push(`/project${tab.url}`); + }, }, - }, -}; + }; </script> <template> - <li - @click="clickFile(tab)" - > + <li @click="clickFile(tab)"> <button type="button" class="multi-file-tab-close" @@ -69,8 +67,7 @@ export default { <file-icon :file-name="tab.name" :size="16" - > - </file-icon> + /> {{ tab.name }} </div> </li> diff --git a/app/assets/javascripts/ide/components/repo_tabs.vue b/app/assets/javascripts/ide/components/repo_tabs.vue index ab0bef4f0ac..ca363bba0ef 100644 --- a/app/assets/javascripts/ide/components/repo_tabs.vue +++ b/app/assets/javascripts/ide/components/repo_tabs.vue @@ -20,7 +20,7 @@ > <repo-tab v-for="tab in openFiles" - :key="tab.id" + :key="tab.key" :tab="tab" /> </ul> diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js index a9cbf8e370f..a7fb9e0588a 100644 --- a/app/assets/javascripts/ide/ide_router.js +++ b/app/assets/javascripts/ide/ide_router.js @@ -84,13 +84,13 @@ router.beforeEach((to, from, next) => { } }) .catch((e) => { - flash('Error while loading the branch files. Please try again.'); + 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.'); + flash('Error while loading the project data. Please try again.', 'alert', document, null, false, true); throw e; }); } diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js index 51e202b9348..51255f15658 100644 --- a/app/assets/javascripts/ide/lib/editor.js +++ b/app/assets/javascripts/ide/lib/editor.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import DecorationsController from './decorations/controller'; import DirtyDiffController from './diff/controller'; import Disposable from './common/disposable'; @@ -54,7 +55,7 @@ export default class Editor { attachModel(model) { this.instance.setModel(model.getModel()); - this.dirtyDiffController.attachModel(model); + if (this.dirtyDiffController) this.dirtyDiffController.attachModel(model); this.currentModel = model; @@ -67,7 +68,7 @@ export default class Editor { return acc; }, {})); - this.dirtyDiffController.reDecorate(model); + if (this.dirtyDiffController) this.dirtyDiffController.reDecorate(model); } clearEditor() { diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js index 335882bb6d7..d007d0ae78f 100644 --- a/app/assets/javascripts/ide/stores/actions.js +++ b/app/assets/javascripts/ide/stores/actions.js @@ -3,6 +3,7 @@ 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'; export const redirectToUrl = (_, url) => visitUrl(url); @@ -70,7 +71,7 @@ export const setResizingStatus = ({ commit }, resizing) => { export const checkCommitStatus = ({ state }) => service .getBranchData(state.currentProjectId, state.currentBranchId) - .then((data) => { + .then(({ data }) => { const { id } = data.commit; const selectedBranch = state.projects[state.currentProjectId].branches[state.currentBranchId]; @@ -81,7 +82,7 @@ export const checkCommitStatus = ({ state }) => return false; }) - .catch(() => flash('Error checking branch data. Please try again.')); + .catch(() => flash('Error checking branch data. Please try again.', 'alert', document, null, false, true)); export const commitChanges = ( { commit, state, dispatch, getters }, @@ -89,10 +90,10 @@ export const commitChanges = ( ) => service .commit(state.currentProjectId, payload) - .then((data) => { + .then(({ data }) => { const { branch } = payload; if (!data.short_id) { - flash(data.message); + flash(data.message, 'alert', document, null, false, true); return; } @@ -105,19 +106,25 @@ export const commitChanges = ( }, }; + let commitMsg = `Your changes have been committed. Commit ${data.short_id}`; + if (data.stats) { + commitMsg += ` with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`; + } + flash( - `Your changes have been committed. Commit ${data.short_id} with ${ - data.stats.additions - } additions, ${data.stats.deletions} deletions.`, + commitMsg, 'notice', - ); + 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}`, + `${selectedProject.web_url}/merge_requests/new?merge_request%5Bsource_branch%5D=${branch}`, ); } else { commit(types.SET_BRANCH_WORKING_REFERENCE, { @@ -134,12 +141,18 @@ export const commitChanges = ( }); dispatch('discardAllChanges'); - dispatch('closeAllFiles'); window.scrollTo(0, 0); } }) - .catch(() => flash('Error committing changes. Please try again.')); + .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')); + }); export const createTempEntry = ( { state, dispatch }, diff --git a/app/assets/javascripts/ide/stores/actions/branch.js b/app/assets/javascripts/ide/stores/actions/branch.js index 32bdf7fec22..bc6fd2d4163 100644 --- a/app/assets/javascripts/ide/stores/actions/branch.js +++ b/app/assets/javascripts/ide/stores/actions/branch.js @@ -10,14 +10,14 @@ export const getBranchData = ( !state.projects[`${projectId}`].branches[branchId]) || force) { service.getBranchData(`${projectId}`, branchId) - .then((data) => { + .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.'); + flash('Error loading branch data. Please try again.', 'alert', document, null, false, true); reject(new Error(`Branch not loaded - ${projectId}/${branchId}`)); }); } else { diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js index 0f27d5bf1c3..670af2fb89e 100644 --- a/app/assets/javascripts/ide/stores/actions/file.js +++ b/app/assets/javascripts/ide/stores/actions/file.js @@ -69,7 +69,7 @@ export const getFileData = ({ state, commit, dispatch }, file) => { }) .catch(() => { commit(types.TOGGLE_LOADING, file); - flash('Error loading file data. Please try again.'); + flash('Error loading file data. Please try again.', 'alert', document, null, false, true); }); }; @@ -77,22 +77,28 @@ export const getRawFileData = ({ commit, dispatch }, file) => service.getRawFile .then((raw) => { commit(types.SET_FILE_RAW_DATA, { file, raw }); }) - .catch(() => flash('Error loading file content. Please try again.')); + .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 setFileLanguage = ({ state, commit }, { fileLanguage }) => { - commit(types.SET_FILE_LANGUAGE, { file: state.selectedFile, fileLanguage }); + if (state.selectedFile) { + commit(types.SET_FILE_LANGUAGE, { file: state.selectedFile, fileLanguage }); + } }; export const setFileEOL = ({ state, commit }, { eol }) => { - commit(types.SET_FILE_EOL, { file: state.selectedFile, eol }); + if (state.selectedFile) { + commit(types.SET_FILE_EOL, { file: state.selectedFile, eol }); + } }; export const setEditorPosition = ({ state, commit }, { editorRow, editorColumn }) => { - commit(types.SET_FILE_POSITION, { file: state.selectedFile, editorRow, editorColumn }); + if (state.selectedFile) { + commit(types.SET_FILE_POSITION, { file: state.selectedFile, editorRow, editorColumn }); + } }; export const createTempFile = ({ state, commit, dispatch }, { projectId, branchId, parent, name, content = '', base64 = '' }) => { @@ -112,7 +118,7 @@ export const createTempFile = ({ state, commit, dispatch }, { projectId, branchI url: newUrl, }); - if (findEntry(parent.tree, 'blob', file.name)) return flash(`The name "${file.name}" is already taken in this directory.`); + 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, diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js index 02d4bd87ab0..faeceb430a2 100644 --- a/app/assets/javascripts/ide/stores/actions/project.js +++ b/app/assets/javascripts/ide/stores/actions/project.js @@ -18,7 +18,7 @@ export const getProjectData = ( resolve(data); }) .catch(() => { - flash('Error loading project data. Please try again.'); + flash('Error loading project data. Please try again.', 'alert', document, null, false, true); reject(new Error(`Project not loaded ${namespace}/${projectId}`)); }); } else { diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js index 25909400a75..302ba45edee 100644 --- a/app/assets/javascripts/ide/stores/actions/tree.js +++ b/app/assets/javascripts/ide/stores/actions/tree.js @@ -52,7 +52,7 @@ export const getTreeData = ( resolve(data); }) .catch((e) => { - flash('Error loading tree data. Please try again.'); + flash('Error loading tree data. Please try again.', 'alert', document, null, false, true); if (tree) commit(types.TOGGLE_LOADING, tree); reject(e); }); @@ -151,7 +151,7 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s dispatch('getLastCommitData', tree); }) - .catch(() => flash('Error fetching log data.')); + .catch(() => flash('Error fetching log data.', 'alert', document, null, false, true)); }; export const updateDirectoryData = ( diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js index 5f3655b0092..72db1c180c9 100644 --- a/app/assets/javascripts/ide/stores/mutations/file.js +++ b/app/assets/javascripts/ide/stores/mutations/file.js @@ -64,7 +64,7 @@ export default { }, [types.DISCARD_FILE_CHANGES](state, file) { Object.assign(file, { - content: '', + content: file.raw, changed: false, }); }, diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js index 29e3ab5d040..d556404faa5 100644 --- a/app/assets/javascripts/ide/stores/utils.js +++ b/app/assets/javascripts/ide/stores/utils.js @@ -1,3 +1,5 @@ +import _ from 'underscore'; + export const dataStructure = () => ({ id: '', key: '', diff --git a/app/assets/javascripts/init_labels.js b/app/assets/javascripts/init_labels.js new file mode 100644 index 00000000000..5f20055510f --- /dev/null +++ b/app/assets/javascripts/init_labels.js @@ -0,0 +1,18 @@ +import LabelManager from './label_manager'; +import GroupLabelSubscription from './group_label_subscription'; +import ProjectLabelSubscription from './project_label_subscription'; + +export default () => { + if ($('.prioritized-labels').length) { + new LabelManager(); // eslint-disable-line no-new + } + $('.label-subscription').each((i, el) => { + const $el = $(el); + + if ($el.find('.dropdown-group-label').length) { + new GroupLabelSubscription($el); // eslint-disable-line no-new + } else { + new ProjectLabelSubscription($el); // eslint-disable-line no-new + } + }); +}; diff --git a/app/assets/javascripts/integrations/integration_settings_form.js b/app/assets/javascripts/integrations/integration_settings_form.js index 32415a8791f..3f27cfc2f6d 100644 --- a/app/assets/javascripts/integrations/integration_settings_form.js +++ b/app/assets/javascripts/integrations/integration_settings_form.js @@ -1,4 +1,5 @@ -import Flash from '../flash'; +import axios from '../lib/utils/axios_utils'; +import flash from '../flash'; export default class IntegrationSettingsForm { constructor(formSelector) { @@ -95,29 +96,26 @@ export default class IntegrationSettingsForm { */ testSettings(formData) { this.toggleSubmitBtnState(true); - $.ajax({ - type: 'PUT', - url: this.testEndPoint, - data: formData, - }) - .done((res) => { - if (res.error) { - new Flash(`${res.message} ${res.service_response}`, 'alert', document, { - title: 'Save anyway', - clickHandler: (e) => { - e.preventDefault(); - this.$form.submit(); - }, - }); - } else { - this.$form.submit(); - } - }) - .fail(() => { - new Flash('Something went wrong on our end.'); - }) - .always(() => { - this.toggleSubmitBtnState(false); - }); + + return axios.put(this.testEndPoint, formData) + .then(({ data }) => { + if (data.error) { + flash(`${data.message} ${data.service_response}`, 'alert', document, { + title: 'Save anyway', + clickHandler: (e) => { + e.preventDefault(); + this.$form.submit(); + }, + }); + } else { + this.$form.submit(); + } + + this.toggleSubmitBtnState(false); + }) + .catch(() => { + flash('Something went wrong on our end.'); + this.toggleSubmitBtnState(false); + }); } } diff --git a/app/assets/javascripts/issuable/auto_width_dropdown_select.js b/app/assets/javascripts/issuable/auto_width_dropdown_select.js index 2203a56315e..14a2bfbe4e0 100644 --- a/app/assets/javascripts/issuable/auto_width_dropdown_select.js +++ b/app/assets/javascripts/issuable/auto_width_dropdown_select.js @@ -11,6 +11,14 @@ class AutoWidthDropdownSelect { const dropdownClass = this.dropdownClass; this.$selectElement.select2({ dropdownCssClass: dropdownClass, + ...AutoWidthDropdownSelect.selectOptions(this.dropdownClass), + }); + + return this; + } + + static selectOptions(dropdownClass) { + return { dropdownCss() { let resultantWidth = 'auto'; const $dropdown = $(`.${dropdownClass}`); @@ -29,9 +37,7 @@ class AutoWidthDropdownSelect { maxWidth: offsetParentWidth, }; }, - }); - - return this; + }; } } diff --git a/app/assets/javascripts/issuable_bulk_update_actions.js b/app/assets/javascripts/issuable_bulk_update_actions.js index b124fafec70..8c1b2e78ca4 100644 --- a/app/assets/javascripts/issuable_bulk_update_actions.js +++ b/app/assets/javascripts/issuable_bulk_update_actions.js @@ -1,5 +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 'underscore'; +import axios from './lib/utils/axios_utils'; import Flash from './flash'; export default { @@ -22,15 +23,9 @@ export default { }, submit() { - const _this = this; - const xhr = $.ajax({ - url: this.form.attr('action'), - method: this.form.attr('method'), - dataType: 'JSON', - data: this.getFormDataAsObject() - }); - xhr.done(() => window.location.reload()); - xhr.fail(() => this.onFormSubmitFailure()); + axios[this.form.attr('method')](this.form.attr('action'), this.getFormDataAsObject()) + .then(() => window.location.reload()) + .catch(() => this.onFormSubmitFailure()); }, onFormSubmitFailure() { diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js index 57dcaa0e1ac..fdfad0b6a4f 100644 --- a/app/assets/javascripts/issuable_form.js +++ b/app/assets/javascripts/issuable_form.js @@ -6,6 +6,7 @@ import Autosave from './autosave'; import UsersSelect from './users_select'; import GfmAutoComplete from './gfm_auto_complete'; import ZenMode from './zen_mode'; +import AutoWidthDropdownSelect from './issuable/auto_width_dropdown_select'; import { parsePikadayDate, pikadayToString } from './lib/utils/datefix'; export default class IssuableForm { @@ -46,6 +47,12 @@ export default class IssuableForm { }); calendar.setDate(parsePikadayDate($issuableDueDate.val())); } + + this.$targetBranchSelect = $('.js-target-branch-select', this.form); + + if (this.$targetBranchSelect.length) { + this.initTargetBranchDropdown(); + } } initAutosave() { @@ -104,4 +111,37 @@ export default class IssuableForm { addWip() { this.titleField.val(`WIP: ${(this.titleField.val())}`); } + + initTargetBranchDropdown() { + this.$targetBranchSelect.select2({ + ...AutoWidthDropdownSelect.selectOptions('js-target-branch-select'), + ajax: { + url: this.$targetBranchSelect.data('endpoint'), + dataType: 'JSON', + quietMillis: 250, + data(search) { + return { + search, + }; + }, + results(data) { + return { + // `data` keys are translated so we can't just access them with a string based key + results: data[Object.keys(data)[0]].map(name => ({ + id: name, + text: name, + })), + }; + }, + }, + initSelection(el, callback) { + const val = el.val(); + + callback({ + id: val, + text: val, + }); + }, + }); + } } diff --git a/app/assets/javascripts/issuable_index.js b/app/assets/javascripts/issuable_index.js index c3e0acdff66..0683ca82a38 100644 --- a/app/assets/javascripts/issuable_index.js +++ b/app/assets/javascripts/issuable_index.js @@ -1,3 +1,6 @@ +import axios from './lib/utils/axios_utils'; +import flash from './flash'; +import { __ } from './locale'; import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar'; import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; @@ -20,23 +23,24 @@ export default class IssuableIndex { } static resetIncomingEmailToken() { - $('.incoming-email-token-reset').on('click', (e) => { + const $resetToken = $('.incoming-email-token-reset'); + + $resetToken.on('click', (e) => { e.preventDefault(); - $.ajax({ - type: 'PUT', - url: $('.incoming-email-token-reset').attr('href'), - dataType: 'json', - success(response) { - $('#issuable_email').val(response.new_address).focus(); - }, - beforeSend() { - $('.incoming-email-token-reset').text('resetting...'); - }, - complete() { - $('.incoming-email-token-reset').text('reset it'); - }, - }); + $resetToken.text('resetting...'); + + axios.put($resetToken.attr('href')) + .then(({ data }) => { + $('#issuable_email').val(data.new_address).focus(); + + $resetToken.text('reset it'); + }) + .catch(() => { + flash(__('There was an error when reseting email token.')); + + $resetToken.text('reset it'); + }); }); } } diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue index fc10a43d1bf..e87a8ed7fea 100644 --- a/app/assets/javascripts/issue_show/components/app.vue +++ b/app/assets/javascripts/issue_show/components/app.vue @@ -1,308 +1,324 @@ <script> -import Visibility from 'visibilityjs'; -import { visitUrl } from '../../lib/utils/url_utility'; -import Poll from '../../lib/utils/poll'; -import eventHub from '../event_hub'; -import Service from '../services/index'; -import Store from '../stores'; -import titleComponent from './title.vue'; -import descriptionComponent from './description.vue'; -import editedComponent from './edited.vue'; -import formComponent from './form.vue'; -import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor'; + import Visibility from 'visibilityjs'; + import { visitUrl } from '../../lib/utils/url_utility'; + import Poll from '../../lib/utils/poll'; + import eventHub from '../event_hub'; + import Service from '../services/index'; + import Store from '../stores'; + import titleComponent from './title.vue'; + import descriptionComponent from './description.vue'; + import editedComponent from './edited.vue'; + import formComponent from './form.vue'; + import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor'; -export default { - props: { - endpoint: { - required: true, - type: String, + export default { + components: { + descriptionComponent, + titleComponent, + editedComponent, + formComponent, }, - updateEndpoint: { - required: true, - type: String, - }, - canUpdate: { - required: true, - type: Boolean, - }, - canDestroy: { - required: true, - type: Boolean, - }, - showInlineEditButton: { - type: Boolean, - required: false, - default: true, - }, - showDeleteButton: { - type: Boolean, - required: false, - default: true, - }, - enableAutocomplete: { - type: Boolean, - required: false, - default: true, - }, - issuableRef: { - type: String, - required: true, - }, - initialTitleHtml: { - type: String, - required: true, - }, - initialTitleText: { - type: String, - required: true, - }, - initialDescriptionHtml: { - type: String, - required: false, - default: '', - }, - initialDescriptionText: { - type: String, - required: false, - default: '', - }, - initialTaskStatus: { - type: String, - required: false, - default: '', - }, - updatedAt: { - type: String, - required: false, - default: '', - }, - updatedByName: { - type: String, - required: false, - default: '', - }, - updatedByPath: { - type: String, - required: false, - default: '', - }, - issuableTemplates: { - type: Array, - required: false, - default: () => [], - }, - markdownPreviewPath: { - type: String, - required: true, - }, - markdownDocsPath: { - type: String, - required: true, - }, - projectPath: { - type: String, - required: true, - }, - projectNamespace: { - type: String, - required: true, - }, - issuableType: { - type: String, - required: false, - default: 'issue', - }, - canAttachFile: { - type: Boolean, - required: false, - default: true, + mixins: [ + recaptchaModalImplementor, + ], + props: { + endpoint: { + required: true, + type: String, + }, + updateEndpoint: { + required: true, + type: String, + }, + canUpdate: { + required: true, + type: Boolean, + }, + canDestroy: { + required: true, + type: Boolean, + }, + showInlineEditButton: { + type: Boolean, + required: false, + default: true, + }, + showDeleteButton: { + type: Boolean, + required: false, + default: true, + }, + enableAutocomplete: { + type: Boolean, + required: false, + default: true, + }, + issuableRef: { + type: String, + required: true, + }, + initialTitleHtml: { + type: String, + required: true, + }, + initialTitleText: { + type: String, + required: true, + }, + initialDescriptionHtml: { + type: String, + required: false, + default: '', + }, + initialDescriptionText: { + type: String, + required: false, + default: '', + }, + initialTaskStatus: { + type: String, + required: false, + default: '', + }, + updatedAt: { + type: String, + required: false, + default: '', + }, + updatedByName: { + type: String, + required: false, + default: '', + }, + updatedByPath: { + type: String, + required: false, + default: '', + }, + issuableTemplates: { + type: Array, + required: false, + default: () => [], + }, + markdownPreviewPath: { + type: String, + required: true, + }, + markdownDocsPath: { + type: String, + required: true, + }, + projectPath: { + type: String, + required: true, + }, + projectNamespace: { + type: String, + required: true, + }, + issuableType: { + type: String, + required: false, + default: 'issue', + }, + canAttachFile: { + type: Boolean, + required: false, + default: true, + }, }, - }, - data() { - const store = new Store({ - titleHtml: this.initialTitleHtml, - titleText: this.initialTitleText, - descriptionHtml: this.initialDescriptionHtml, - descriptionText: this.initialDescriptionText, - updatedAt: this.updatedAt, - updatedByName: this.updatedByName, - updatedByPath: this.updatedByPath, - taskStatus: this.initialTaskStatus, - }); + data() { + const store = new Store({ + titleHtml: this.initialTitleHtml, + titleText: this.initialTitleText, + descriptionHtml: this.initialDescriptionHtml, + descriptionText: this.initialDescriptionText, + updatedAt: this.updatedAt, + updatedByName: this.updatedByName, + updatedByPath: this.updatedByPath, + taskStatus: this.initialTaskStatus, + }); - return { - store, - state: store.state, - showForm: false, - }; - }, - computed: { - formState() { - return this.store.formState; + return { + store, + state: store.state, + showForm: false, + }; }, - hasUpdated() { - return !!this.state.updatedAt; + computed: { + formState() { + return this.store.formState; + }, + hasUpdated() { + return !!this.state.updatedAt; + }, + issueChanged() { + const descriptionChanged = + this.initialDescriptionText !== this.store.formState.description; + const titleChanged = + this.initialTitleText !== this.store.formState.title; + return descriptionChanged || titleChanged; + }, }, - }, - components: { - descriptionComponent, - titleComponent, - editedComponent, - formComponent, - }, - - mixins: [ - recaptchaModalImplementor, - ], + created() { + this.service = new Service(this.endpoint); + this.poll = new Poll({ + resource: this.service, + method: 'getData', + successCallback: res => this.store.updateState(res.data), + errorCallback(err) { + throw new Error(err); + }, + }); - methods: { - openForm() { - if (!this.showForm) { - this.showForm = true; - this.store.setFormState({ - title: this.state.titleText, - description: this.state.descriptionText, - lockedWarningVisible: false, - updateLoading: false, - }); + if (!Visibility.hidden()) { + this.poll.makeRequest(); } - }, - closeForm() { - this.showForm = false; - }, - updateIssuable() { - return this.service.updateIssuable(this.store.formState) - .then(res => res.data) - .then(data => this.checkForSpam(data)) - .then((data) => { - if (location.pathname !== data.web_url) { - visitUrl(data.web_url); - } + Visibility.change(() => { + if (!Visibility.hidden()) { + this.poll.restart(); + } else { + this.poll.stop(); + } + }); - return this.service.getData(); - }) - .then(res => res.data) - .then((data) => { - this.store.updateState(data); - eventHub.$emit('close.form'); - }) - .catch((error) => { - if (error && error.name === 'SpamError') { - this.openRecaptcha(); - } else { - eventHub.$emit('close.form'); - window.Flash(`Error updating ${this.issuableType}`); - } - }); + window.addEventListener('beforeunload', this.handleBeforeUnloadEvent); + + eventHub.$on('delete.issuable', this.deleteIssuable); + eventHub.$on('update.issuable', this.updateIssuable); + eventHub.$on('close.form', this.closeForm); + eventHub.$on('open.form', this.openForm); }, + beforeDestroy() { + eventHub.$off('delete.issuable', this.deleteIssuable); + eventHub.$off('update.issuable', this.updateIssuable); + eventHub.$off('close.form', this.closeForm); + eventHub.$off('open.form', this.openForm); + window.removeEventListener('beforeunload', this.handleBeforeUnloadEvent); + }, + methods: { + handleBeforeUnloadEvent(e) { + const event = e; + if (this.showForm && this.issueChanged) { + event.returnValue = 'Are you sure you want to lose your issue information?'; + } + return undefined; + }, - closeRecaptchaModal() { - this.store.setFormState({ - updateLoading: false, - }); + openForm() { + if (!this.showForm) { + this.showForm = true; + this.store.setFormState({ + title: this.state.titleText, + description: this.state.descriptionText, + lockedWarningVisible: false, + updateLoading: false, + }); + } + }, + closeForm() { + this.showForm = false; + }, - this.closeRecaptcha(); - }, + updateIssuable() { + return this.service.updateIssuable(this.store.formState) + .then(res => res.data) + .then(data => this.checkForSpam(data)) + .then((data) => { + if (location.pathname !== data.web_url) { + visitUrl(data.web_url); + } - deleteIssuable() { - this.service.deleteIssuable() - .then(res => res.data) - .then((data) => { - // Stop the poll so we don't get 404's with the issuable not existing - this.poll.stop(); + return this.service.getData(); + }) + .then(res => res.data) + .then((data) => { + this.store.updateState(data); + eventHub.$emit('close.form'); + }) + .catch((error) => { + if (error && error.name === 'SpamError') { + this.openRecaptcha(); + } else { + eventHub.$emit('close.form'); + window.Flash(`Error updating ${this.issuableType}`); + } + }); + }, - visitUrl(data.web_url); - }) - .catch(() => { - eventHub.$emit('close.form'); - window.Flash(`Error deleting ${this.issuableType}`); + closeRecaptchaModal() { + this.store.setFormState({ + updateLoading: false, }); - }, - }, - created() { - this.service = new Service(this.endpoint); - this.poll = new Poll({ - resource: this.service, - method: 'getData', - successCallback: res => this.store.updateState(res.data), - errorCallback(err) { - throw new Error(err); - }, - }); - if (!Visibility.hidden()) { - this.poll.makeRequest(); - } + this.closeRecaptcha(); + }, - Visibility.change(() => { - if (!Visibility.hidden()) { - this.poll.restart(); - } else { - this.poll.stop(); - } - }); + deleteIssuable() { + this.service.deleteIssuable() + .then(res => res.data) + .then((data) => { + // Stop the poll so we don't get 404's with the issuable not existing + this.poll.stop(); - eventHub.$on('delete.issuable', this.deleteIssuable); - eventHub.$on('update.issuable', this.updateIssuable); - eventHub.$on('close.form', this.closeForm); - eventHub.$on('open.form', this.openForm); - }, - beforeDestroy() { - eventHub.$off('delete.issuable', this.deleteIssuable); - eventHub.$off('update.issuable', this.updateIssuable); - eventHub.$off('close.form', this.closeForm); - eventHub.$off('open.form', this.openForm); - }, -}; + visitUrl(data.web_url); + }) + .catch(() => { + eventHub.$emit('close.form'); + window.Flash(`Error deleting ${this.issuableType}`); + }); + }, + }, + }; </script> <template> -<div> - <div v-if="canUpdate && showForm"> - <form-component - :form-state="formState" - :can-destroy="canDestroy" - :issuable-templates="issuableTemplates" - :markdown-docs-path="markdownDocsPath" - :markdown-preview-path="markdownPreviewPath" - :project-path="projectPath" - :project-namespace="projectNamespace" - :show-delete-button="showDeleteButton" - :can-attach-file="canAttachFile" - :enable-autocomplete="enableAutocomplete" - /> + <div> + <div v-if="canUpdate && showForm"> + <form-component + :form-state="formState" + :can-destroy="canDestroy" + :issuable-templates="issuableTemplates" + :markdown-docs-path="markdownDocsPath" + :markdown-preview-path="markdownPreviewPath" + :project-path="projectPath" + :project-namespace="projectNamespace" + :show-delete-button="showDeleteButton" + :can-attach-file="canAttachFile" + :enable-autocomplete="enableAutocomplete" + /> - <recaptcha-modal - v-show="showRecaptcha" - :html="recaptchaHTML" - @close="closeRecaptchaModal" - /> - </div> - <div v-else> - <title-component - :issuable-ref="issuableRef" - :can-update="canUpdate" - :title-html="state.titleHtml" - :title-text="state.titleText" - :show-inline-edit-button="showInlineEditButton" - /> - <description-component - v-if="state.descriptionHtml" - :can-update="canUpdate" - :description-html="state.descriptionHtml" - :description-text="state.descriptionText" - :updated-at="state.updatedAt" - :task-status="state.taskStatus" - :issuable-type="issuableType" - :update-url="updateEndpoint" - /> - <edited-component - v-if="hasUpdated" - :updated-at="state.updatedAt" - :updated-by-name="state.updatedByName" - :updated-by-path="state.updatedByPath" - /> + <recaptcha-modal + v-show="showRecaptcha" + :html="recaptchaHTML" + @close="closeRecaptchaModal" + /> + </div> + <div v-else> + <title-component + :issuable-ref="issuableRef" + :can-update="canUpdate" + :title-html="state.titleHtml" + :title-text="state.titleText" + :show-inline-edit-button="showInlineEditButton" + /> + <description-component + v-if="state.descriptionHtml" + :can-update="canUpdate" + :description-html="state.descriptionHtml" + :description-text="state.descriptionText" + :updated-at="state.updatedAt" + :task-status="state.taskStatus" + :issuable-type="issuableType" + :update-url="updateEndpoint" + /> + <edited-component + v-if="hasUpdated" + :updated-at="state.updatedAt" + :updated-by-name="state.updatedByName" + :updated-by-path="state.updatedByPath" + /> + </div> </div> -</div> </template> diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue index c3f2bf130bb..9afa9dea126 100644 --- a/app/assets/javascripts/issue_show/components/description.vue +++ b/app/assets/javascripts/issue_show/components/description.vue @@ -56,7 +56,10 @@ this.updateTaskStatusText(); }, }, - + mounted() { + this.renderGFM(); + this.updateTaskStatusText(); + }, methods: { renderGFM() { $(this.$refs['gfm-content']).renderGFM(); @@ -88,17 +91,17 @@ if (taskRegexMatches) { $tasks.text(this.taskStatus); - $tasksShort.text(`${taskRegexMatches[1]}/${taskRegexMatches[2]} task${taskRegexMatches[2] > 1 ? 's' : ''}`); + $tasksShort.text( + `${taskRegexMatches[1]}/${taskRegexMatches[2]} task${taskRegexMatches[2] > 1 ? + 's' : + ''}`, + ); } else { $tasks.text(''); $tasksShort.text(''); } }, }, - mounted() { - this.renderGFM(); - this.updateTaskStatusText(); - }, }; </script> @@ -108,7 +111,8 @@ class="description" :class="{ 'js-task-list-container': canUpdate - }"> + }" + > <div class="wiki" :class="{ diff --git a/app/assets/javascripts/issue_show/components/edited.vue b/app/assets/javascripts/issue_show/components/edited.vue index 992b7064c13..01097b5b35e 100644 --- a/app/assets/javascripts/issue_show/components/edited.vue +++ b/app/assets/javascripts/issue_show/components/edited.vue @@ -1,33 +1,33 @@ <script> -import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; + import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; -export default { - props: { - updatedAt: { - type: String, - required: false, - default: '', + export default { + components: { + timeAgoTooltip, }, - updatedByName: { - type: String, - required: false, - default: '', + props: { + updatedAt: { + type: String, + required: false, + default: '', + }, + updatedByName: { + type: String, + required: false, + default: '', + }, + updatedByPath: { + type: String, + required: false, + default: '', + }, }, - updatedByPath: { - type: String, - required: false, - default: '', + computed: { + hasUpdatedBy() { + return this.updatedByName && this.updatedByPath; + }, }, - }, - components: { - timeAgoTooltip, - }, - computed: { - hasUpdatedBy() { - return this.updatedByName && this.updatedByPath; - }, - }, -}; + }; </script> <template> @@ -48,7 +48,7 @@ export default { class="author_link" :href="updatedByPath" > - <span>{{updatedByName}}</span> + <span>{{ updatedByName }}</span> </a> </span> </small> diff --git a/app/assets/javascripts/issue_show/components/fields/description.vue b/app/assets/javascripts/issue_show/components/fields/description.vue index 4e577546551..d9fa2764d65 100644 --- a/app/assets/javascripts/issue_show/components/fields/description.vue +++ b/app/assets/javascripts/issue_show/components/fields/description.vue @@ -3,6 +3,9 @@ import markdownField from '../../../vue_shared/components/markdown/field.vue'; export default { + components: { + markdownField, + }, mixins: [updateMixin], props: { formState: { @@ -28,9 +31,6 @@ default: true, }, }, - components: { - markdownField, - }, mounted() { this.$refs.textarea.focus(); }, diff --git a/app/assets/javascripts/issue_show/components/form.vue b/app/assets/javascripts/issue_show/components/form.vue index 0fa19022336..779705e19ac 100644 --- a/app/assets/javascripts/issue_show/components/form.vue +++ b/app/assets/javascripts/issue_show/components/form.vue @@ -6,6 +6,13 @@ import descriptionTemplate from './fields/description_template.vue'; export default { + components: { + lockedWarning, + titleField, + descriptionField, + descriptionTemplate, + editActions, + }, props: { canDestroy: { type: Boolean, @@ -52,13 +59,6 @@ default: true, }, }, - components: { - lockedWarning, - titleField, - descriptionField, - descriptionTemplate, - editActions, - }, computed: { hasIssuableTemplates() { return this.issuableTemplates.length; @@ -78,16 +78,19 @@ :form-state="formState" :issuable-templates="issuableTemplates" :project-path="projectPath" - :project-namespace="projectNamespace" /> + :project-namespace="projectNamespace" + /> </div> <div :class="{ 'col-sm-8 col-lg-9': hasIssuableTemplates, 'col-xs-12': !hasIssuableTemplates, - }"> + }" + > <title-field :form-state="formState" - :issuable-templates="issuableTemplates" /> + :issuable-templates="issuableTemplates" + /> </div> </div> <description-field @@ -100,6 +103,7 @@ <edit-actions :form-state="formState" :can-destroy="canDestroy" - :show-delete-button="showDeleteButton" /> + :show-delete-button="showDeleteButton" + /> </form> </template> diff --git a/app/assets/javascripts/issue_show/components/title.vue b/app/assets/javascripts/issue_show/components/title.vue index b7e6eadd440..aec890a2ff6 100644 --- a/app/assets/javascripts/issue_show/components/title.vue +++ b/app/assets/javascripts/issue_show/components/title.vue @@ -5,14 +5,10 @@ import { spriteIcon } from '../../lib/utils/common_utils'; export default { - mixins: [animateMixin], - data() { - return { - preAnimation: false, - pulseAnimation: false, - titleEl: document.querySelector('title'), - }; + directives: { + tooltip, }, + mixins: [animateMixin], props: { issuableRef: { type: String, @@ -37,8 +33,17 @@ default: false, }, }, - directives: { - tooltip, + data() { + return { + preAnimation: false, + pulseAnimation: false, + titleEl: document.querySelector('title'), + }; + }, + computed: { + pencilIcon() { + return spriteIcon('pencil', 'link-highlight'); + }, }, watch: { titleHtml() { @@ -46,11 +51,6 @@ this.animateChange(); }, }, - computed: { - pencilIcon() { - return spriteIcon('pencil', 'link-highlight'); - }, - }, methods: { setPageTitle() { const currentPageTitleScope = this.titleEl.innerText.split('·'); @@ -85,7 +85,7 @@ data-placement="bottom" data-container="body" @click="edit" - > + > </button> </div> </template> diff --git a/app/assets/javascripts/job.js b/app/assets/javascripts/job.js index 8f32dcc94e2..9b5092c5e3f 100644 --- a/app/assets/javascripts/job.js +++ b/app/assets/javascripts/job.js @@ -3,7 +3,6 @@ import { visitUrl } from './lib/utils/url_utility'; import bp from './breakpoints'; import { numberToHumanSize } from './lib/utils/number_utils'; import { setCiStatusFavicon } from './lib/utils/common_utils'; -import { timeFor } from './lib/utils/datetime_utility'; export default class Job { constructor(options) { @@ -71,7 +70,6 @@ export default class Job { .off('resize.build') .on('resize.build', _.throttle(this.sidebarOnResize.bind(this), 100)); - this.updateArtifactRemoveDate(); this.initAffixTopArea(); this.getBuildTrace(); @@ -261,16 +259,7 @@ export default class Job { sidebarOnClick() { if (this.shouldHideSidebarForViewport()) this.toggleSidebar(); } - // eslint-disable-next-line class-methods-use-this, consistent-return - updateArtifactRemoveDate() { - const $date = $('.js-artifacts-remove'); - if ($date.length) { - const date = $date.text(); - return $date.text( - timeFor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3'))), - ); - } - } + // eslint-disable-next-line class-methods-use-this populateJobs(stage) { $('.build-job').hide(); diff --git a/app/assets/javascripts/jobs/components/header.vue b/app/assets/javascripts/jobs/components/header.vue index c660828b30e..357bc9aab17 100644 --- a/app/assets/javascripts/jobs/components/header.vue +++ b/app/assets/javascripts/jobs/components/header.vue @@ -3,7 +3,11 @@ import loadingIcon from '../../vue_shared/components/loading_icon.vue'; export default { - name: 'jobHeaderSection', + name: 'JobHeaderSection', + components: { + ciHeader, + loadingIcon, + }, props: { job: { type: Object, @@ -14,10 +18,6 @@ required: true, }, }, - components: { - ciHeader, - loadingIcon, - }, data() { return { actions: this.getActions(), @@ -30,8 +30,17 @@ shouldRenderContent() { return !this.isLoading && Object.keys(this.job).length; }, + /** + * When job has not started the key will be `false` + * When job started the key will be a string with a date. + */ jobStarted() { - return this.job.started; + return !this.job.started === false; + }, + }, + watch: { + job() { + this.actions = this.getActions(); }, }, methods: { @@ -49,11 +58,6 @@ return actions; }, }, - watch: { - job() { - this.actions = this.getActions(); - }, - }, }; </script> <template> @@ -72,6 +76,7 @@ <loading-icon v-if="isLoading" size="2" - /> + class="prepend-top-default append-bottom-default" + /> </div> </template> diff --git a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue index ab2bcd728a8..a6819aaeb12 100644 --- a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue +++ b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue @@ -23,9 +23,10 @@ <p class="build-detail-row"> <span v-if="hasTitle" - class="build-light-text"> - {{title}}: + class="build-light-text" + > + {{ title }}: </span> - {{value}} + {{ value }} </p> </template> diff --git a/app/assets/javascripts/jobs/components/sidebar_details_block.vue b/app/assets/javascripts/jobs/components/sidebar_details_block.vue index d0145fed396..56814a52525 100644 --- a/app/assets/javascripts/jobs/components/sidebar_details_block.vue +++ b/app/assets/javascripts/jobs/components/sidebar_details_block.vue @@ -6,6 +6,13 @@ export default { name: 'SidebarDetailsBlock', + components: { + detailRow, + loadingIcon, + }, + mixins: [ + timeagoMixin, + ], props: { job: { type: Object, @@ -16,13 +23,6 @@ required: true, }, }, - mixins: [ - timeagoMixin, - ], - components: { - detailRow, - loadingIcon, - }, computed: { shouldRenderContent() { return !this.isLoading && Object.keys(this.job).length > 0; @@ -58,11 +58,13 @@ <template v-if="shouldRenderContent"> <div class="block retry-link" - v-if="job.retry_path || job.new_issue_path"> + v-if="job.retry_path || job.new_issue_path" + > <a v-if="job.new_issue_path" class="js-new-issue btn btn-new btn-inverted" - :href="job.new_issue_path"> + :href="job.new_issue_path" + > New issue </a> <a @@ -70,20 +72,21 @@ class="js-retry-job btn btn-inverted-secondary" :href="job.retry_path" data-method="post" - rel="nofollow"> + rel="nofollow" + > Retry </a> </div> <div :class="{block : renderBlock }"> <p class="build-detail-row js-job-mr" - v-if="job.merge_request"> - <span - class="build-light-text"> + v-if="job.merge_request" + > + <span class="build-light-text"> Merge Request: </span> <a :href="job.merge_request.path"> - !{{job.merge_request.iid}} + !{{ job.merge_request.iid }} </a> </p> @@ -92,49 +95,49 @@ v-if="job.duration" title="Duration" :value="duration" - /> + /> <detail-row class="js-job-finished" v-if="job.finished_at" title="Finished" :value="timeFormated(job.finished_at)" - /> + /> <detail-row class="js-job-erased" v-if="job.erased_at" title="Erased" :value="timeFormated(job.erased_at)" - /> + /> <detail-row class="js-job-queued" v-if="job.queued" title="Queued" :value="queued" - /> + /> <detail-row class="js-job-runner" v-if="job.runner" title="Runner" :value="runnerId" - /> + /> <detail-row class="js-job-coverage" v-if="job.coverage" title="Coverage" :value="coverage" - /> + /> <p class="build-detail-row js-job-tags" - v-if="job.tags.length"> - <span - class="build-light-text"> + v-if="job.tags.length" + > + <span class="build-light-text"> Tags: </span> <span - v-for="tag in job.tags" - key="tag" + v-for="(tag, i) in job.tags" + :key="i" class="label label-primary"> - {{tag}} + {{ tag }} </span> </p> @@ -146,7 +149,8 @@ class="js-cancel-job btn btn-sm btn-default" :href="job.cancel_path" data-method="post" - rel="nofollow"> + rel="nofollow" + > Cancel </a> </div> @@ -156,6 +160,6 @@ class="prepend-top-10" v-if="isLoading" size="2" - /> + /> </div> </template> diff --git a/app/assets/javascripts/jobs/job_details_bundle.js b/app/assets/javascripts/jobs/job_details_bundle.js index baaf5641200..db53b04de0e 100644 --- a/app/assets/javascripts/jobs/job_details_bundle.js +++ b/app/assets/javascripts/jobs/job_details_bundle.js @@ -13,14 +13,14 @@ document.addEventListener('DOMContentLoaded', () => { // eslint-disable-next-line no-new new Vue({ el: '#js-build-header-vue', + components: { + jobHeader, + }, data() { return { mediator, }; }, - components: { - jobHeader, - }, mounted() { this.mediator.initBuildClass(); }, @@ -38,14 +38,14 @@ document.addEventListener('DOMContentLoaded', () => { // eslint-disable-next-line new Vue({ el: '#js-details-block-vue', + components: { + detailsBlock, + }, data() { return { mediator, }; }, - components: { - detailsBlock, - }, render(createElement) { return createElement('details-block', { props: { diff --git a/app/assets/javascripts/label_manager.js b/app/assets/javascripts/label_manager.js index c929dc98c10..61b40f79db1 100644 --- a/app/assets/javascripts/label_manager.js +++ b/app/assets/javascripts/label_manager.js @@ -1,7 +1,8 @@ /* 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 */ -/* global Sortable */ +import Sortable from 'vendor/Sortable'; -import Flash from './flash'; +import flash from './flash'; +import axios from './lib/utils/axios_utils'; export default class LabelManager { constructor({ togglePriorityButton, prioritizedLabels, otherLabels } = {}) { @@ -50,11 +51,12 @@ export default class LabelManager { if (persistState == null) { persistState = true; } - let xhr; const _this = this; const url = $label.find('.js-toggle-priority').data('url'); let $target = this.prioritizedLabels; let $from = this.otherLabels; + const rollbackLabelPosition = this.rollbackLabelPosition.bind(this, $label, action); + if (action === 'remove') { $target = this.otherLabels; $from = this.prioritizedLabels; @@ -71,40 +73,34 @@ export default class LabelManager { return; } if (action === 'remove') { - xhr = $.ajax({ - url, - type: 'DELETE' - }); + axios.delete(url) + .catch(rollbackLabelPosition); + // Restore empty message if (!$from.find('li').length) { $from.find('.empty-message').removeClass('hidden'); } } else { - xhr = this.savePrioritySort($label, action); + this.savePrioritySort($label, action) + .catch(rollbackLabelPosition); } - return xhr.fail(this.rollbackLabelPosition.bind(this, $label, action)); } onPrioritySortUpdate() { - const xhr = this.savePrioritySort(); - return xhr.fail(function() { - return new Flash(this.errorMessage, 'alert'); - }); + this.savePrioritySort() + .catch(() => flash(this.errorMessage)); } savePrioritySort() { - return $.post({ - url: this.prioritizedLabels.data('url'), - data: { - label_ids: this.getSortedLabelsIds() - } + return axios.post(this.prioritizedLabels.data('url'), { + label_ids: this.getSortedLabelsIds(), }); } rollbackLabelPosition($label, originalAction) { const action = originalAction === 'remove' ? 'add' : 'remove'; this.toggleLabelPriority($label, action, false); - return new Flash(this.errorMessage, 'alert'); + flash(this.errorMessage); } getSortedLabelsIds() { diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index f7a1c9f1e40..664e793fc8e 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -231,7 +231,7 @@ export default class LabelsSelect { selectedClass.push('label-item'); $a.attr('data-label-id', label.id); } - $a.addClass(selectedClass.join(' ')).html(colorEl + " " + label.title); + $a.addClass(selectedClass.join(' ')).html(`${colorEl} ${_.escape(label.title)}`); // Return generated html return $li.html($a).prop('outerHTML'); }, diff --git a/app/assets/javascripts/lib/utils/axios_utils.js b/app/assets/javascripts/lib/utils/axios_utils.js index 8aff0556011..585214049c7 100644 --- a/app/assets/javascripts/lib/utils/axios_utils.js +++ b/app/assets/javascripts/lib/utils/axios_utils.js @@ -22,3 +22,11 @@ axios.interceptors.response.use((config) => { }); export default axios; + +/** + * @return The adapter that axios uses for dispatching requests. This may be overwritten in tests. + * + * @see https://github.com/axios/axios/tree/master/lib/adapters + * @see https://github.com/ctimmerm/axios-mock-adapter/blob/v1.12.0/src/index.js#L39 + */ +export const getDefaultAdapter = () => axios.defaults.adapter; diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 03918762842..8018ec411c1 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -1,4 +1,5 @@ import { getLocationHash } from './url_utility'; +import axios from './axios_utils'; export const getPagePath = (index = 0) => $('body').attr('data-page').split(':')[index]; @@ -382,22 +383,16 @@ export const resetFavicon = () => { } }; -export const setCiStatusFavicon = (pageUrl) => { - $.ajax({ - url: pageUrl, - dataType: 'json', - success: (data) => { +export const setCiStatusFavicon = pageUrl => + axios.get(pageUrl) + .then(({ data }) => { if (data && data.favicon) { setFavicon(data.favicon); } else { resetFavicon(); } - }, - error: () => { - resetFavicon(); - }, - }); -}; + }) + .catch(resetFavicon); export const spriteIcon = (icon, className = '') => { const classAttribute = className.length > 0 ? `class="${className}"` : ''; diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index cb6e06ea584..62d80c4a649 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -72,4 +72,4 @@ export function capitalizeFirstCharacter(text) { * @param {*} replace * @returns {String} */ -export const stripeHtml = (string, replace = '') => string.replace(/<[^>]*>/g, replace); +export const stripHtml = (string, replace = '') => string.replace(/<[^>]*>/g, replace); diff --git a/app/assets/javascripts/lib/utils/users_cache.js b/app/assets/javascripts/lib/utils/users_cache.js index 88f8a622c00..b01ec6b81a3 100644 --- a/app/assets/javascripts/lib/utils/users_cache.js +++ b/app/assets/javascripts/lib/utils/users_cache.js @@ -8,16 +8,16 @@ class UsersCache extends Cache { } return Api.users('', { username }) - .then((users) => { - if (!users.length) { + .then(({ data }) => { + if (!data.length) { throw new Error(`User "${username}" could not be found!`); } - if (users.length > 1) { + if (data.length > 1) { throw new Error(`Expected username "${username}" to be unique!`); } - const user = users[0]; + const user = data[0]; this.internalStorage[username] = user; return user; }); diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 59bfa482bb0..d8b881a8fac 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -1,33 +1,17 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len, no-multi-spaces, import/newline-after-import, import/first */ +/* eslint-disable import/first */ /* global ConfirmDangerModal */ import jQuery from 'jquery'; -import _ from 'underscore'; import Cookies from 'js-cookie'; -import Dropzone from 'dropzone'; -import Sortable from 'vendor/Sortable'; import svg4everybody from 'svg4everybody'; -// libraries with import side-effects -import 'mousetrap'; -import 'mousetrap/plugins/pause/mousetrap-pause'; - // expose common libraries as globals (TODO: remove these) window.jQuery = jQuery; window.$ = jQuery; -window._ = _; -window.Dropzone = Dropzone; -window.Sortable = Sortable; - -// templates -import './templates/issuable_template_selector'; -import './templates/issuable_template_selectors'; - -import './commit/image_file'; // lib/utils import { handleLocationHash } from './lib/utils/common_utils'; -import { localTimeAgo, renderTimeago } from './lib/utils/datetime_utility'; +import { localTimeAgo } from './lib/utils/datetime_utility'; import { getLocationHash, visitUrl } from './lib/utils/url_utility'; // behaviors @@ -43,10 +27,8 @@ import initTodoToggle from './header'; import initImporterStatus from './importer_status'; import initLayoutNav from './layout_nav'; import LazyLoader from './lazy_loader'; -import './line_highlighter'; import initLogoAnimation from './logo'; import './milestone_select'; -import './preview_markdown'; import './projects_dropdown'; import './render_gfm'; import initBreadcrumbs from './breadcrumb'; @@ -56,11 +38,9 @@ import './dispatcher'; // eslint-disable-next-line global-require, import/no-commonjs if (process.env.NODE_ENV !== 'production') require('./test_utils/'); -Dropzone.autoDiscover = false; - svg4everybody(); -document.addEventListener('beforeunload', function () { +document.addEventListener('beforeunload', () => { // Unbind scroll events $(document).off('scroll'); // Close any open tooltips @@ -77,16 +57,15 @@ window.addEventListener('load', function onLoad() { gl.lazyLoader = new LazyLoader({ scrollContainer: window, - observerNode: '#content-body' + observerNode: '#content-body', }); -$(function () { - var $body = $('body'); - var $document = $(document); - var $window = $(window); - var $sidebarGutterToggle = $('.js-sidebar-toggle'); - var bootstrapBreakpoint = bp.getBreakpointSize(); - var fitSidebarForSize; +$(() => { + const $body = $('body'); + const $document = $(document); + const $window = $(window); + const $sidebarGutterToggle = $('.js-sidebar-toggle'); + let bootstrapBreakpoint = bp.getBreakpointSize(); initBreadcrumbs(); initLayoutNav(); @@ -98,8 +77,8 @@ $(function () { Cookies.defaults.path = gon.relative_url_root || '/'; // `hashchange` is not triggered when link target is already in window.location - $body.on('click', 'a[href^="#"]', function() { - var href = this.getAttribute('href'); + $body.on('click', 'a[href^="#"]', function clickHashLinkCallback() { + const href = this.getAttribute('href'); if (href.substr(1) === getLocationHash()) { setTimeout(handleLocationHash, 1); } @@ -114,155 +93,162 @@ $(function () { } // prevent default action for disabled buttons - $('.btn').click(function(e) { + $('.btn').click(function clickDisabledButtonCallback(e) { if ($(this).hasClass('disabled')) { e.preventDefault(); e.stopImmediatePropagation(); return false; } + + return true; }); - $('.js-select-on-focus').on('focusin', function () { - return $(this).select().one('mouseup', function (e) { - return e.preventDefault(); - }); // Click a .js-select-on-focus field, select the contents // Prevent a mouseup event from deselecting the input + $('.js-select-on-focus').on('focusin', function selectOnFocusCallback() { + $(this).select().one('mouseup', (e) => { + e.preventDefault(); + }); }); - $('.remove-row').bind('ajax:success', function () { + + $('.remove-row').on('ajax:success', function removeRowAjaxSuccessCallback() { $(this).tooltip('destroy') .closest('li') .fadeOut(); }); - $('.js-remove-tr').bind('ajax:before', function () { - return $(this).hide(); + + $('.js-remove-tr').on('ajax:before', function removeTRAjaxBeforeCallback() { + $(this).hide(); }); - $('.js-remove-tr').bind('ajax:success', function () { - return $(this).closest('tr').fadeOut(); + + $('.js-remove-tr').on('ajax:success', function removeTRAjaxSuccessCallback() { + $(this).closest('tr').fadeOut(); }); + + // Initialize select2 selects $('select.select2').select2({ width: 'resolve', - // Initialize select2 selects - dropdownAutoWidth: true + dropdownAutoWidth: true, }); - $('.js-select2').bind('select2-close', function () { - return setTimeout((function () { - $('.select2-container-active').removeClass('select2-container-active'); - return $(':focus').blur(); - }), 1); + // Close select2 on escape + $('.js-select2').on('select2-close', () => { + setTimeout(() => { + $('.select2-container-active').removeClass('select2-container-active'); + $(':focus').blur(); + }, 1); }); + // Initialize tooltips $.fn.tooltip.Constructor.DEFAULTS.trigger = 'hover'; $body.tooltip({ selector: '.has-tooltip, [data-toggle="tooltip"]', - placement: function (tip, el) { + placement(tip, el) { return $(el).data('placement') || 'bottom'; - } + }, }); + // Initialize popovers $body.popover({ selector: '[data-toggle="popover"]', trigger: 'focus', // set the viewport to the main content, excluding the navigation bar, so // the navigation can't overlap the popover - viewport: '.layout-page' + viewport: '.layout-page', }); - $('.trigger-submit').on('change', function () { - return $(this).parents('form').submit(); + // Form submitter + $('.trigger-submit').on('change', function triggerSubmitCallback() { + $(this).parents('form').submit(); }); + localTimeAgo($('abbr.timeago, .js-timeago'), true); + // Disable form buttons while a form is submitting - $body.on('ajax:complete, ajax:beforeSend, submit', 'form', function (e) { - var buttons; - buttons = $('[type="submit"], .js-disable-on-submit', this); + $body.on('ajax:complete, ajax:beforeSend, submit', 'form', function ajaxCompleteCallback(e) { + const $buttons = $('[type="submit"], .js-disable-on-submit', this); switch (e.type) { case 'ajax:beforeSend': case 'submit': - return buttons.disable(); + return $buttons.disable(); default: - return buttons.enable(); + return $buttons.enable(); } }); - $(document).ajaxError(function (e, xhrObj) { - var ref = xhrObj.status; - if (xhrObj.status === 401) { - return new Flash('You need to be logged in.', 'alert'); + + $(document).ajaxError((e, xhrObj) => { + const ref = xhrObj.status; + + if (ref === 401) { + Flash('You need to be logged in.'); } else if (ref === 404 || ref === 500) { - return new Flash('Something went wrong on our end.', 'alert'); + Flash('Something went wrong on our end.'); } }); - $('.account-box').hover(function () { - // Show/Hide the profile menu when hovering the account box - return $(this).toggleClass('hover'); - }); - $document.on('click', '.diff-content .js-show-suppressed-diff', function () { - var $container; - $container = $(this).parent(); - $container.next('table').show(); - return $container.remove(); + // Commit show suppressed diff + $document.on('click', '.diff-content .js-show-suppressed-diff', function showDiffCallback() { + const $container = $(this).parent(); + $container.next('table').show(); + $container.remove(); }); + $('.navbar-toggle').on('click', () => { $('.header-content').toggleClass('menu-expanded'); gl.lazyLoader.loadCheck(); }); + // Show/hide comments on diff - $body.on('click', '.js-toggle-diff-comments', function (e) { - var $this = $(this); - var notesHolders = $this.closest('.diff-file').find('.notes_holder'); + $body.on('click', '.js-toggle-diff-comments', function toggleDiffCommentsCallback(e) { + const $this = $(this); + const notesHolders = $this.closest('.diff-file').find('.notes_holder'); + + e.preventDefault(); + $this.toggleClass('active'); + if ($this.hasClass('active')) { notesHolders.show().find('.hide, .content').show(); } else { notesHolders.hide().find('.content').hide(); } + $(document).trigger('toggle.comments'); - return e.preventDefault(); }); - $document.off('click', '.js-confirm-danger'); - $document.on('click', '.js-confirm-danger', function (e) { - var btn = $(e.target); - var form = btn.closest('form'); - var text = btn.data('confirm-danger-message'); + + $document.on('click', '.js-confirm-danger', (e) => { + const btn = $(e.target); + const form = btn.closest('form'); + const text = btn.data('confirm-danger-message'); e.preventDefault(); - return new ConfirmDangerModal(form, text); - }); - $('input[type="search"]').each(function () { - var $this = $(this); - $this.attr('value', $this.val()); - }); - $document.off('keyup', 'input[type="search"]').on('keyup', 'input[type="search"]', function () { - var $this; - $this = $(this); - return $this.attr('value', $this.val()); + + // eslint-disable-next-line no-new + new ConfirmDangerModal(form, text); }); - $document.off('breakpoint:change').on('breakpoint:change', function (e, breakpoint) { - var $gutterIcon; + + $document.on('breakpoint:change', (e, breakpoint) => { if (breakpoint === 'sm' || breakpoint === 'xs') { - $gutterIcon = $sidebarGutterToggle.find('i'); + const $gutterIcon = $sidebarGutterToggle.find('i'); if ($gutterIcon.hasClass('fa-angle-double-right')) { - return $sidebarGutterToggle.trigger('click'); + $sidebarGutterToggle.trigger('click'); } } }); - fitSidebarForSize = function () { - var oldBootstrapBreakpoint; - oldBootstrapBreakpoint = bootstrapBreakpoint; + + function fitSidebarForSize() { + const oldBootstrapBreakpoint = bootstrapBreakpoint; bootstrapBreakpoint = bp.getBreakpointSize(); + if (bootstrapBreakpoint !== oldBootstrapBreakpoint) { - return $document.trigger('breakpoint:change', [bootstrapBreakpoint]); + $document.trigger('breakpoint:change', [bootstrapBreakpoint]); } - }; - $window.off('resize.app').on('resize.app', function () { - return fitSidebarForSize(); - }); - loadAwardsHandler(); + } - renderTimeago(); + $window.on('resize.app', fitSidebarForSize); + + loadAwardsHandler(); - $('form.filter-form').on('submit', function (event) { + $('form.filter-form').on('submit', function filterFormSubmitCallback(event) { const link = document.createElement('a'); link.href = this.action; diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js index 94561d6b7c3..792b7523889 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js +++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js @@ -25,12 +25,12 @@ $(() => { gl.MergeConflictsResolverApp = new Vue({ el: '#conflicts', - data: mergeConflictsStore.state, components: { 'diff-file-editor': gl.mergeConflicts.diffFileEditor, 'inline-conflict-lines': gl.mergeConflicts.inlineConflictLines, 'parallel-conflict-lines': gl.mergeConflicts.parallelConflictLines }, + data: mergeConflictsStore.state, computed: { conflictsCountText() { return mergeConflictsStore.getConflictsCountText(); }, readyToCommit() { return mergeConflictsStore.isReadyToCommit(); }, diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index cb3cdea8111..bedd50de1bb 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -1,6 +1,7 @@ /* 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 'vendor/jquery.waitforimages'; +import { __ } from '~/locale'; import TaskList from './task_list'; import MergeRequestTabs from './merge_request_tabs'; import IssuablesHelper from './helpers/issuables_helper'; @@ -110,22 +111,22 @@ MergeRequest.prototype.initCommitMessageListeners = function() { }); }; -MergeRequest.prototype.updateStatusText = function(classToRemove, classToAdd, newStatusText) { +MergeRequest.setStatusBoxToMerged = function() { $('.detail-page-header .status-box') - .removeClass(classToRemove) - .addClass(classToAdd) + .removeClass('status-box-open') + .addClass('status-box-mr-merged') .find('span') - .text(newStatusText); + .text(__('Merged')); }; -MergeRequest.prototype.decreaseCounter = function(by = 1) { - const $el = $('.nav-links .js-merge-counter'); +MergeRequest.decreaseCounter = function(by = 1) { + const $el = $('.js-merge-counter'); const count = Math.max((parseInt($el.text().replace(/[^\d]/, ''), 10) - by), 0); $el.text(addDelimiter(count)); }; -MergeRequest.prototype.hideCloseButton = function() { +MergeRequest.hideCloseButton = function() { const el = document.querySelector('.merge-request .js-issuable-actions'); const closeDropdownItem = el.querySelector('li.close-item'); if (closeDropdownItem) { diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js index f76a998bf8c..dd6c6b854bc 100644 --- a/app/assets/javascripts/milestone.js +++ b/app/assets/javascripts/milestone.js @@ -1,5 +1,3 @@ -/* global Sortable */ - import Flash from './flash'; export default class Milestone { diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js b/app/assets/javascripts/mini_pipeline_graph_dropdown.js index ca3d271663b..c7bccd483ac 100644 --- a/app/assets/javascripts/mini_pipeline_graph_dropdown.js +++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js @@ -1,5 +1,6 @@ /* eslint-disable no-new */ -import Flash from './flash'; +import flash from './flash'; +import axios from './lib/utils/axios_utils'; /** * In each pipelines table we have a mini pipeline graph for each pipeline. @@ -78,27 +79,22 @@ export default class MiniPipelineGraph { const button = e.relatedTarget; const endpoint = button.dataset.stageEndpoint; - return $.ajax({ - dataType: 'json', - type: 'GET', - url: endpoint, - beforeSend: () => { - this.renderBuildsList(button, ''); - this.toggleLoading(button); - }, - success: (data) => { + this.renderBuildsList(button, ''); + this.toggleLoading(button); + + axios.get(endpoint) + .then(({ data }) => { this.toggleLoading(button); this.renderBuildsList(button, data.html); this.stopDropdownClickPropagation(); - }, - error: () => { + }) + .catch(() => { this.toggleLoading(button); if ($(button).parent().hasClass('open')) { $(button).dropdown('toggle'); } - new Flash('An error occurred while fetching the builds.', 'alert'); - }, - }); + flash('An error occurred while fetching the builds.', 'alert'); + }); } /** diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 8da723ced03..5afae93724b 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -11,6 +11,12 @@ export default { + components: { + Graph, + GraphGroup, + EmptyState, + }, + data() { const metricsData = document.querySelector('#prometheus-graphs').dataset; const store = new MonitoringStore(); @@ -36,12 +42,30 @@ }; }, - components: { - Graph, - GraphGroup, - EmptyState, + created() { + this.service = new MonitoringService({ + metricsEndpoint: this.metricsEndpoint, + deploymentEndpoint: this.deploymentEndpoint, + }); + eventHub.$on('toggleAspectRatio', this.toggleAspectRatio); + eventHub.$on('hoverChanged', this.hoverChanged); + }, + + beforeDestroy() { + eventHub.$off('toggleAspectRatio', this.toggleAspectRatio); + eventHub.$off('hoverChanged', this.hoverChanged); + window.removeEventListener('resize', this.resizeThrottled, false); }, + mounted() { + this.resizeThrottled = _.throttle(this.resize, 600); + if (!this.hasMetrics) { + this.state = 'gettingStarted'; + } else { + this.getGraphsData(); + window.addEventListener('resize', this.resizeThrottled, false); + } + }, methods: { getGraphsData() { this.state = 'loading'; @@ -52,7 +76,13 @@ .then(data => this.store.storeDeploymentData(data)) .catch(() => new Flash('Error getting deployment information.')), ]) - .then(() => { this.showEmptyState = false; }) + .then(() => { + if (this.store.groups.length < 1) { + this.state = 'noData'; + return; + } + this.showEmptyState = false; + }) .catch(() => { this.state = 'unableToConnect'; }); }, @@ -72,36 +102,14 @@ this.hoverData = data; }, }, - - created() { - this.service = new MonitoringService({ - metricsEndpoint: this.metricsEndpoint, - deploymentEndpoint: this.deploymentEndpoint, - }); - eventHub.$on('toggleAspectRatio', this.toggleAspectRatio); - eventHub.$on('hoverChanged', this.hoverChanged); - }, - - beforeDestroy() { - eventHub.$off('toggleAspectRatio', this.toggleAspectRatio); - eventHub.$off('hoverChanged', this.hoverChanged); - window.removeEventListener('resize', this.resizeThrottled, false); - }, - - mounted() { - this.resizeThrottled = _.throttle(this.resize, 600); - if (!this.hasMetrics) { - this.state = 'gettingStarted'; - } else { - this.getGraphsData(); - window.addEventListener('resize', this.resizeThrottled, false); - } - }, }; </script> <template> - <div v-if="!showEmptyState" class="prometheus-graphs"> + <div + v-if="!showEmptyState" + class="prometheus-graphs" + > <graph-group v-for="(groupData, index) in store.groups" :key="index" diff --git a/app/assets/javascripts/monitoring/components/empty_state.vue b/app/assets/javascripts/monitoring/components/empty_state.vue index a18164482a2..56cd60c583b 100644 --- a/app/assets/javascripts/monitoring/components/empty_state.vue +++ b/app/assets/javascripts/monitoring/components/empty_state.vue @@ -33,15 +33,24 @@ gettingStarted: { svgUrl: this.emptyGettingStartedSvgPath, title: 'Get started with performance monitoring', - description: 'Stay updated about the performance and health of your environment by configuring Prometheus to monitor your deployments.', + description: `Stay updated about the performance and health + of your environment by configuring Prometheus to monitor your deployments.`, buttonText: 'Configure Prometheus', }, loading: { svgUrl: this.emptyLoadingSvgPath, title: 'Waiting for performance data', - description: 'Creating graphs uses the data from the Prometheus server. If this takes a long time, ensure that data is available.', + description: `Creating graphs uses the data from the Prometheus server. + If this takes a long time, ensure that data is available.`, buttonText: 'View documentation', }, + noData: { + svgUrl: this.emptyUnableToConnectSvgPath, + title: 'No data found', + description: `You are connected to the Prometheus server, but there is currently + no data to display.`, + buttonText: 'Configure Prometheus', + }, unableToConnect: { svgUrl: this.emptyUnableToConnectSvgPath, title: 'Unable to connect to Prometheus server', @@ -74,20 +83,26 @@ <template> <div class="prometheus-state"> <div class="state-svg svg-content"> - <img :src="currentState.svgUrl"/> + <img :src="currentState.svgUrl" /> </div> <h4 class="state-title"> - {{currentState.title}} + {{ currentState.title }} </h4> <p class="state-description"> - {{currentState.description}} - <a v-if="showButtonDescription" :href="settingsPath"> + {{ currentState.description }} + <a + v-if="showButtonDescription" + :href="settingsPath" + > Prometheus server </a> </p> <div class="state-button"> - <a class="btn btn-success" :href="buttonPath"> - {{currentState.buttonText}} + <a + class="btn btn-success" + :href="buttonPath" + > + {{ currentState.buttonText }} </a> </div> </div> diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue index a50b80c23d0..ea5c24efaf9 100644 --- a/app/assets/javascripts/monitoring/components/graph.vue +++ b/app/assets/javascripts/monitoring/components/graph.vue @@ -17,6 +17,15 @@ const d3 = { scaleLinear, scaleTime, axisLeft, axisBottom, max, extent, select }; export default { + components: { + GraphLegend, + GraphFlag, + GraphDeployment, + GraphPath, + }, + + mixins: [MonitoringMixin], + props: { graphData: { type: Object, @@ -45,8 +54,6 @@ }, }, - mixins: [MonitoringMixin], - data() { return { baseGraphHeight: 450, @@ -74,13 +81,6 @@ }; }, - components: { - GraphLegend, - GraphFlag, - GraphDeployment, - GraphPath, - }, - computed: { outerViewBox() { return `0 0 ${this.baseGraphWidth} ${this.baseGraphHeight}`; @@ -105,6 +105,26 @@ }, }, + watch: { + updateAspectRatio() { + if (this.updateAspectRatio) { + this.graphHeight = 450; + this.graphWidth = 600; + this.measurements = measurements.large; + this.draw(); + eventHub.$emit('toggleAspectRatio'); + } + }, + + hoverData() { + this.positionFlag(); + }, + }, + + mounted() { + this.draw(); + }, + methods: { draw() { const breakpointSize = bp.getBreakpointSize(); @@ -197,51 +217,34 @@ }); // This will select all of the ticks once they're rendered }, }, - - watch: { - updateAspectRatio() { - if (this.updateAspectRatio) { - this.graphHeight = 450; - this.graphWidth = 600; - this.measurements = measurements.large; - this.draw(); - eventHub.$emit('toggleAspectRatio'); - } - }, - - hoverData() { - this.positionFlag(); - }, - }, - - mounted() { - this.draw(); - }, }; </script> <template> - <div + <div class="prometheus-graph" @mouseover="showFlagContent = true" - @mouseleave="showFlagContent = false"> + @mouseleave="showFlagContent = false" + > <h5 class="text-center graph-title"> - {{graphData.title}} + {{ graphData.title }} </h5> <div class="prometheus-svg-container" - :style="paddingBottomRootSvg"> + :style="paddingBottomRootSvg" + > <svg :viewBox="outerViewBox" - ref="baseSvg"> + ref="baseSvg" + > <g class="x-axis" - :transform="axisTransform"> - </g> + :transform="axisTransform" + /> <g class="y-axis" - transform="translate(70, 20)"> - </g> + transform="translate(70, 20)" + /> <graph-legend :graph-width="graphWidth" :graph-height="graphHeight" @@ -256,29 +259,30 @@ <svg class="graph-data" :viewBox="innerViewBox" - ref="graphData"> - <graph-path - v-for="(path, index) in timeSeries" - :key="index" - :generated-line-path="path.linePath" - :generated-area-path="path.areaPath" - :line-style="path.lineStyle" - :line-color="path.lineColor" - :area-color="path.areaColor" - /> - <graph-deployment - :deployment-data="reducedDeploymentData" - :graph-height="graphHeight" - :graph-height-offset="graphHeightOffset" - /> - <rect - class="prometheus-graph-overlay" - :width="(graphWidth - 70)" - :height="(graphHeight - 100)" - transform="translate(-5, 20)" - ref="graphOverlay" - @mousemove="handleMouseOverGraph($event)"> - </rect> + ref="graphData" + > + <graph-path + v-for="(path, index) in timeSeries" + :key="index" + :generated-line-path="path.linePath" + :generated-area-path="path.areaPath" + :line-style="path.lineStyle" + :line-color="path.lineColor" + :area-color="path.areaColor" + /> + <graph-deployment + :deployment-data="reducedDeploymentData" + :graph-height="graphHeight" + :graph-height-offset="graphHeightOffset" + /> + <rect + class="prometheus-graph-overlay" + :width="(graphWidth - 70)" + :height="(graphHeight - 100)" + transform="translate(-5, 20)" + ref="graphOverlay" + @mousemove="handleMouseOverGraph($event)" + /> </svg> </svg> <graph-flag diff --git a/app/assets/javascripts/monitoring/components/graph/deployment.vue b/app/assets/javascripts/monitoring/components/graph/deployment.vue index 8d6393d4ce5..98c25307b74 100644 --- a/app/assets/javascripts/monitoring/components/graph/deployment.vue +++ b/app/assets/javascripts/monitoring/components/graph/deployment.vue @@ -39,33 +39,35 @@ y="0" :height="calculatedHeight" width="3" - fill="url(#shadow-gradient)"> - </rect> + fill="url(#shadow-gradient)" + /> <line class="deployment-line" x1="0" y1="0" x2="0" :y2="calculatedHeight" - stroke="#000"> - </line> + stroke="#000" + /> </g> <svg height="0" - width="0"> + width="0" + > <defs> <linearGradient - id="shadow-gradient"> + id="shadow-gradient" + > <stop offset="0%" stop-color="#000" - stop-opacity="0.4"> - </stop> + stop-opacity="0.4" + /> <stop offset="100%" stop-color="#000" - stop-opacity="0"> - </stop> + stop-opacity="0" + /> </linearGradient> </defs> </svg> diff --git a/app/assets/javascripts/monitoring/components/graph/flag.vue b/app/assets/javascripts/monitoring/components/graph/flag.vue index 62ebc3f419c..07aa6a3e5de 100644 --- a/app/assets/javascripts/monitoring/components/graph/flag.vue +++ b/app/assets/javascripts/monitoring/components/graph/flag.vue @@ -1,9 +1,12 @@ <script> import { dateFormat, timeFormat } from '../../utils/date_time_formatters'; import { formatRelevantDigits } from '../../../lib/utils/number_utils'; - import Icon from '../../../vue_shared/components/icon.vue'; + import icon from '../../../vue_shared/components/icon.vue'; export default { + components: { + icon, + }, props: { currentXCoordinate: { type: Number, @@ -52,10 +55,6 @@ }, }, - components: { - Icon, - }, - computed: { formatTime() { return this.deploymentFlagData ? @@ -137,33 +136,34 @@ > <div class="arrow"></div> <div class="popover-title"> - <h5 v-if="this.deploymentFlagData"> + <h5 v-if="deploymentFlagData"> Deployed </h5> - {{formatDate}} at - <strong>{{formatTime}}</strong> + {{ formatDate }} at + <strong>{{ formatTime }}</strong> </div> <div - v-if="this.deploymentFlagData" + v-if="deploymentFlagData" class="popover-content deploy-meta-content" > <div> <icon name="commit" - :size="12"> - </icon> + :size="12" + /> <a :href="deploymentFlagData.commitUrl"> - {{deploymentFlagData.sha.slice(0, 8)}} + {{ deploymentFlagData.sha.slice(0, 8) }} </a> </div> <div - v-if="deploymentFlagData.tag"> + v-if="deploymentFlagData.tag" + > <icon name="label" - :size="12"> - </icon> + :size="12" + /> <a :href="deploymentFlagData.tagUrl"> - {{deploymentFlagData.ref}} + {{ deploymentFlagData.ref }} </a> </div> </div> @@ -174,7 +174,10 @@ :key="index" > <td> - <svg width="15" height="6"> + <svg + width="15" + height="6" + > <line :stroke="series.lineColor" :stroke-dasharray="strokeDashArray(series.lineStyle)" @@ -182,13 +185,13 @@ x1="0" x2="15" y1="2" - y2="2"> - </line> + y2="2" + /> </svg> </td> - <td>{{seriesMetricLabel(index, series)}}</td> + <td>{{ seriesMetricLabel(index, series) }}</td> <td> - <strong>{{seriesMetricValue(series)}}</strong> + <strong>{{ seriesMetricValue(series) }}</strong> </td> </tr> </table> diff --git a/app/assets/javascripts/monitoring/components/graph/legend.vue b/app/assets/javascripts/monitoring/components/graph/legend.vue index 440b1b12631..c6e8d726ffc 100644 --- a/app/assets/javascripts/monitoring/components/graph/legend.vue +++ b/app/assets/javascripts/monitoring/components/graph/legend.vue @@ -73,6 +73,21 @@ }, }, + mounted() { + this.$nextTick(() => { + const bbox = this.$refs.ylabel.getBBox(); + this.metricUsageXPosition = 0; + this.seriesXPosition = 0; + if (this.$refs.legendTitleSvg != null) { + this.seriesXPosition = this.$refs.legendTitleSvg[0].getBBox().width; + } + if (this.$refs.seriesTitleSvg != null) { + this.metricUsageXPosition = this.$refs.seriesTitleSvg[0].getBBox().width; + } + this.yLabelWidth = bbox.width + 10; // Added some padding + this.yLabelHeight = bbox.height + 5; + }); + }, methods: { translateLegendGroup(index) { return `translate(0, ${12 * (index)})`; @@ -100,26 +115,10 @@ return null; }, }, - mounted() { - this.$nextTick(() => { - const bbox = this.$refs.ylabel.getBBox(); - this.metricUsageXPosition = 0; - this.seriesXPosition = 0; - if (this.$refs.legendTitleSvg != null) { - this.seriesXPosition = this.$refs.legendTitleSvg[0].getBBox().width; - } - if (this.$refs.seriesTitleSvg != null) { - this.metricUsageXPosition = this.$refs.seriesTitleSvg[0].getBBox().width; - } - this.yLabelWidth = bbox.width + 10; // Added some padding - this.yLabelHeight = bbox.height + 5; - }); - }, }; </script> <template> - <g - class="axis-label-container"> + <g class="axis-label-container"> <line class="label-x-axis-line" stroke="#000000" @@ -127,8 +126,8 @@ x1="10" :y1="yPosition" :x2="graphWidth + 20" - :y2="yPosition"> - </line> + :y2="yPosition" + /> <line class="label-y-axis-line" stroke="#000000" @@ -136,39 +135,43 @@ x1="10" y1="0" :x2="10" - :y2="yPosition"> - </line> + :y2="yPosition" + /> <rect class="rect-axis-text" :transform="rectTransform" :width="yLabelWidth" - :height="yLabelHeight"> - </rect> + :height="yLabelHeight" + /> <text class="label-axis-text y-label-text" text-anchor="middle" :transform="textTransform" - ref="ylabel"> - {{yAxisLabel}} + ref="ylabel" + > + {{ yAxisLabel }} </text> <rect class="rect-axis-text" :x="xPosition + 60" :y="graphHeight - 80" width="35" - height="50"> - </rect> + height="50" + /> <text class="label-axis-text x-label-text" :x="xPosition + 60" :y="yPosition" - dy=".35em"> + dy=".35em" + > Time </text> - <g class="legend-group" + <g + class="legend-group" v-for="(series, index) in timeSeries" :key="index" - :transform="translateLegendGroup(index)"> + :transform="translateLegendGroup(index)" + > <line :stroke="series.lineColor" :stroke-width="measurements.legends.height" @@ -176,23 +179,25 @@ :x1="measurements.legends.offsetX" :x2="measurements.legends.offsetX + measurements.legends.width" :y1="graphHeight - measurements.legends.offsetY" - :y2="graphHeight - measurements.legends.offsetY"> - </line> + :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)}} + :y="graphHeight - 30" + > + {{ createSeriesString(index, series) }} </text> <text v-else class="legend-metric-title" ref="legendTitleSvg" x="38" - :y="graphHeight - 30"> - {{legendTitle}} {{formatMetricUsage(series)}} + :y="graphHeight - 30" + > + {{ legendTitle }} {{ formatMetricUsage(series) }} </text> </g> </g> diff --git a/app/assets/javascripts/monitoring/components/graph/path.vue b/app/assets/javascripts/monitoring/components/graph/path.vue index 5e6d409033a..c9721c4cb01 100644 --- a/app/assets/javascripts/monitoring/components/graph/path.vue +++ b/app/assets/javascripts/monitoring/components/graph/path.vue @@ -12,6 +12,7 @@ lineStyle: { type: String, required: false, + default: '', }, lineColor: { type: String, @@ -37,8 +38,8 @@ class="metric-area" :d="generatedAreaPath" :fill="areaColor" - transform="translate(-5, 20)"> - </path> + transform="translate(-5, 20)" + /> <path class="metric-line" :d="generatedLinePath" @@ -46,7 +47,7 @@ fill="none" stroke-width="1" :stroke-dasharray="strokeDashArray" - transform="translate(-5, 20)"> - </path> + transform="translate(-5, 20)" + /> </g> </template> diff --git a/app/assets/javascripts/monitoring/components/graph_group.vue b/app/assets/javascripts/monitoring/components/graph_group.vue index 958f537d31b..079351a69af 100644 --- a/app/assets/javascripts/monitoring/components/graph_group.vue +++ b/app/assets/javascripts/monitoring/components/graph_group.vue @@ -1,21 +1,21 @@ <script> -export default { - props: { - name: { - type: String, - required: true, + export default { + props: { + name: { + type: String, + required: true, + }, }, - }, -}; + }; </script> <template> <div class="panel panel-default prometheus-panel"> <div class="panel-heading"> - <h4>{{name}}</h4> + <h4>{{ name }}</h4> </div> <div class="panel-body prometheus-graph-group"> - <slot /> + <slot></slot> </div> </div> </template> diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js index 104432ef5de..c3b0ef7e9ca 100644 --- a/app/assets/javascripts/monitoring/monitoring_bundle.js +++ b/app/assets/javascripts/monitoring/monitoring_bundle.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import Dashboard from './components/dashboard.vue'; -document.addEventListener('DOMContentLoaded', () => new Vue({ +export default () => new Vue({ el: '#prometheus-graphs', render: createElement => createElement(Dashboard), -})); +}); diff --git a/app/assets/javascripts/network/branch_graph.js b/app/assets/javascripts/network/branch_graph.js index 5aad3908eb6..d3edcb724f1 100644 --- a/app/assets/javascripts/network/branch_graph.js +++ b/app/assets/javascripts/network/branch_graph.js @@ -1,5 +1,8 @@ /* 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 '../locale'; +import axios from '../lib/utils/axios_utils'; +import flash from '../flash'; import Raphael from './raphael'; export default (function() { @@ -26,16 +29,13 @@ export default (function() { } BranchGraph.prototype.load = function() { - return $.ajax({ - url: this.options.url, - method: "get", - dataType: "json", - success: $.proxy(function(data) { + axios.get(this.options.url) + .then(({ data }) => { $(".loading", this.element).hide(); this.prepareData(data.days, data.commits); - return this.buildGraph(); - }, this) - }); + this.buildGraph(); + }) + .catch(() => __('Error fetching network graph.')); }; BranchGraph.prototype.prepareData = function(days, commits) { diff --git a/app/assets/javascripts/notebook/cells/markdown.vue b/app/assets/javascripts/notebook/cells/markdown.vue index 82c51a1068c..3d09d24b6ab 100644 --- a/app/assets/javascripts/notebook/cells/markdown.vue +++ b/app/assets/javascripts/notebook/cells/markdown.vue @@ -1,6 +1,7 @@ <script> /* global katex */ import marked from 'marked'; + import sanitize from 'sanitize-html'; import Prompt from './prompt.vue'; const renderer = new marked.Renderer(); @@ -82,7 +83,12 @@ }, computed: { markdown() { - return marked(this.cell.source.join('').replace(/\\/g, '\\\\')); + return sanitize(marked(this.cell.source.join('').replace(/\\/g, '\\\\')), { + allowedTags: false, + allowedAttributes: { + '*': ['class'], + }, + }); }, }, }; @@ -91,18 +97,21 @@ <template> <div class="cell text-cell"> <prompt /> - <div class="markdown" v-html="markdown"></div> + <div + class="markdown" + v-html="markdown"> + </div> </div> </template> <style> -.markdown .katex { - display: block; - text-align: center; -} + .markdown .katex { + display: block; + text-align: center; + } -.markdown .inline-katex .katex { - display: inline; - text-align: initial; -} + .markdown .inline-katex .katex { + display: inline; + text-align: initial; + } </style> diff --git a/app/assets/javascripts/notebook/cells/output/html.vue b/app/assets/javascripts/notebook/cells/output/html.vue index 2110a9de7ed..0535ee7afa8 100644 --- a/app/assets/javascripts/notebook/cells/output/html.vue +++ b/app/assets/javascripts/notebook/cells/output/html.vue @@ -1,22 +1,35 @@ <script> -import Prompt from '../prompt.vue'; + import sanitize from 'sanitize-html'; + import Prompt from '../prompt.vue'; -export default { - props: { - rawCode: { - type: String, - required: true, + export default { + components: { + prompt: Prompt, }, - }, - components: { - prompt: Prompt, - }, -}; + props: { + rawCode: { + type: String, + required: true, + }, + }, + computed: { + sanitizedOutput() { + return sanitize(this.rawCode, { + allowedTags: sanitize.defaults.allowedTags.concat([ + 'img', 'svg', + ]), + allowedAttributes: { + img: ['src'], + }, + }); + }, + }, + }; </script> <template> <div class="output"> <prompt /> - <div v-html="rawCode"></div> + <div v-html="sanitizedOutput"></div> </div> </template> diff --git a/app/assets/javascripts/notebook/cells/output/image.vue b/app/assets/javascripts/notebook/cells/output/image.vue index fbb39ea6e2d..67d6c5ad12b 100644 --- a/app/assets/javascripts/notebook/cells/output/image.vue +++ b/app/assets/javascripts/notebook/cells/output/image.vue @@ -1,27 +1,26 @@ <script> -import Prompt from '../prompt.vue'; + import Prompt from '../prompt.vue'; -export default { - props: { - outputType: { - type: String, - required: true, + export default { + components: { + prompt: Prompt, }, - rawCode: { - type: String, - required: true, + props: { + outputType: { + type: String, + required: true, + }, + rawCode: { + type: String, + required: true, + }, }, - }, - components: { - prompt: Prompt, - }, -}; + }; </script> <template> <div class="output"> <prompt /> - <img - :src="'data:' + outputType + ';base64,' + rawCode" /> + <img :src="'data:' + outputType + ';base64,' + rawCode" /> </div> </template> diff --git a/app/assets/javascripts/notebook/cells/output/index.vue b/app/assets/javascripts/notebook/cells/output/index.vue index 05af0bf1e8e..91b2269a83a 100644 --- a/app/assets/javascripts/notebook/cells/output/index.vue +++ b/app/assets/javascripts/notebook/cells/output/index.vue @@ -1,83 +1,87 @@ <script> -import CodeCell from '../code/index.vue'; -import Html from './html.vue'; -import Image from './image.vue'; + import CodeCell from '../code/index.vue'; + import Html from './html.vue'; + import Image from './image.vue'; -export default { - props: { - codeCssClass: { - type: String, - required: false, - default: '', + export default { + components: { + 'code-cell': CodeCell, + 'html-output': Html, + 'image-output': Image, }, - count: { - type: Number, - required: false, - default: 0, + props: { + codeCssClass: { + type: String, + required: false, + default: '', + }, + count: { + type: Number, + required: false, + default: 0, + }, + output: { + type: Object, + requred: true, + default: () => ({}), + }, }, - output: { - type: Object, - requred: true, - }, - }, - components: { - 'code-cell': CodeCell, - 'html-output': Html, - 'image-output': Image, - }, - data() { - return { - outputType: '', - }; - }, - computed: { - componentName() { - if (this.output.text) { - return 'code-cell'; - } else if (this.output.data['image/png']) { - this.outputType = 'image/png'; - - return 'image-output'; - } else if (this.output.data['text/html']) { - this.outputType = 'text/html'; + computed: { + componentName() { + if (this.output.text) { + return 'code-cell'; + } else if (this.output.data['image/png']) { + return 'image-output'; + } else if (this.output.data['text/html']) { + return 'html-output'; + } else if (this.output.data['image/svg+xml']) { + return 'html-output'; + } - return 'html-output'; - } else if (this.output.data['image/svg+xml']) { - this.outputType = 'image/svg+xml'; - - return 'html-output'; - } + return 'code-cell'; + }, + rawCode() { + if (this.output.text) { + return this.output.text.join(''); + } - this.outputType = 'text/plain'; - return 'code-cell'; - }, - rawCode() { - if (this.output.text) { - return this.output.text.join(''); - } + return this.dataForType(this.outputType); + }, + outputType() { + if (this.output.text) { + return ''; + } else if (this.output.data['image/png']) { + return 'image/png'; + } else if (this.output.data['text/html']) { + return 'text/html'; + } else if (this.output.data['image/svg+xml']) { + return 'image/svg+xml'; + } - return this.dataForType(this.outputType); + return 'text/plain'; + }, }, - }, - methods: { - dataForType(type) { - let data = this.output.data[type]; + methods: { + dataForType(type) { + let data = this.output.data[type]; - if (typeof data === 'object') { - data = data.join(''); - } + if (typeof data === 'object') { + data = data.join(''); + } - return data; + return data; + }, }, - }, -}; + }; </script> <template> - <component :is="componentName" + <component + :is="componentName" type="output" - :outputType="outputType" + :output-type="outputType" :count="count" :raw-code="rawCode" - :code-css-class="codeCssClass" /> + :code-css-class="codeCssClass" + /> </template> diff --git a/app/assets/javascripts/notebook/cells/prompt.vue b/app/assets/javascripts/notebook/cells/prompt.vue index 039fb99293d..fe1fc37e1dc 100644 --- a/app/assets/javascripts/notebook/cells/prompt.vue +++ b/app/assets/javascripts/notebook/cells/prompt.vue @@ -4,10 +4,17 @@ type: { type: String, required: false, + default: '', }, count: { type: Number, required: false, + default: 0, + }, + }, + computed: { + hasKeys() { + return this.type !== '' && this.count; }, }, }; @@ -15,16 +22,16 @@ <template> <div class="prompt"> - <span v-if="type && count"> + <span v-if="hasKeys"> {{ type }} [{{ count }}]: </span> </div> </template> <style scoped> -.prompt { - padding: 0 10px; - min-width: 7em; - font-family: monospace; -} + .prompt { + padding: 0 10px; + min-width: 7em; + font-family: monospace; + } </style> diff --git a/app/assets/javascripts/notebook/index.vue b/app/assets/javascripts/notebook/index.vue index e88806431af..e2e3b08c77f 100644 --- a/app/assets/javascripts/notebook/index.vue +++ b/app/assets/javascripts/notebook/index.vue @@ -20,11 +20,6 @@ default: '', }, }, - methods: { - cellType(type) { - return `${type}-cell`; - }, - }, computed: { cells() { if (this.notebook.worksheets) { @@ -45,6 +40,11 @@ return Object.keys(this.notebook).length; }, }, + methods: { + cellType(type) { + return `${type}-cell`; + }, + }, }; </script> diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index a2b8e6f6495..bcb342f407f 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -16,6 +16,7 @@ import Autosize from 'autosize'; import 'vendor/jquery.caret'; // required by jquery.atwho import 'vendor/jquery.atwho'; import AjaxCache from '~/lib/utils/ajax_cache'; +import axios from './lib/utils/axios_utils'; import { getLocationHash } from './lib/utils/url_utility'; import Flash from './flash'; import CommentTypeToggle from './comment_type_toggle'; @@ -252,26 +253,20 @@ export default class Notes { return; } this.refreshing = true; - return $.ajax({ - url: this.notes_url, - headers: { 'X-Last-Fetched-At': this.last_fetched_at }, - dataType: 'json', - success: (function(_this) { - return function(data) { - var notes; - notes = data.notes; - _this.last_fetched_at = data.last_fetched_at; - _this.setPollingInterval(data.notes.length); - return $.each(notes, function(i, note) { - _this.renderNote(note); - }); - }; - })(this) - }).always((function(_this) { - return function() { - return _this.refreshing = false; - }; - })(this)); + axios.get(this.notes_url, { + 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; + }); } /** diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index e594377bc40..3c8452ac808 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -15,7 +15,17 @@ import issuableStateMixin from '../mixins/issuable_state'; export default { - name: 'commentForm', + name: 'CommentForm', + components: { + issueWarning, + noteSignedOutWidget, + discussionLockedWidget, + markdownField, + userAvatarLink, + }, + mixins: [ + issuableStateMixin, + ], data() { return { note: '', @@ -27,21 +37,6 @@ isSubmitButtonDisabled: true, }; }, - components: { - issueWarning, - noteSignedOutWidget, - discussionLockedWidget, - markdownField, - userAvatarLink, - }, - watch: { - note(newNote) { - this.setIsSubmitButtonDisabled(newNote, this.isSubmitting); - }, - isSubmitting(newValue) { - this.setIsSubmitButtonDisabled(this.note, newValue); - }, - }, computed: { ...mapGetters([ 'getCurrentUserLastNote', @@ -65,7 +60,9 @@ if (this.note.length) { const actionText = this.isIssueOpen ? 'close' : 'reopen'; - return this.noteType === constants.COMMENT ? `Comment & ${actionText} issue` : `Start discussion & ${actionText} issue`; + return this.noteType === constants.COMMENT ? + `Comment & ${actionText} issue` : + `Start discussion & ${actionText} issue`; } return this.isIssueOpen ? 'Close issue' : 'Reopen issue'; @@ -97,6 +94,23 @@ 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.issueState = isClosed ? constants.CLOSED : constants.REOPENED; + }); + + this.initAutoSave(); + this.initTaskList(); + }, methods: { ...mapActions([ 'saveNote', @@ -159,7 +173,9 @@ .catch(() => { this.isSubmitting = false; this.discard(false); - const msg = 'Your comment could not be submitted! Please check your network connection and try again.'; + 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(); @@ -207,7 +223,11 @@ }, initAutoSave() { if (this.isLoggedIn) { - this.autosave = new Autosave($(this.$refs.textarea), ['Note', 'Issue', this.getNoteableData.id], 'issue'); + this.autosave = new Autosave( + $(this.$refs.textarea), + ['Note', 'Issue', this.getNoteableData.id], + 'issue', + ); } }, initTaskList() { @@ -223,18 +243,6 @@ }); }, }, - mixins: [ - issuableStateMixin, - ], - mounted() { - // jQuery is needed here because it is a custom event being dispatched with jQuery. - $(document).on('issuable:change', (e, isClosed) => { - this.issueState = isClosed ? constants.CLOSED : constants.REOPENED; - }); - - this.initAutoSave(); - this.initTaskList(); - }, }; </script> @@ -258,12 +266,12 @@ :img-src="author.avatar_url" :img-alt="author.name" :img-size="40" - /> + /> </div> <div class="timeline-content timeline-content-form"> <form ref="commentForm" - class="new-note js-quick-submit common-note-form gfm-form js-main-target-form" + class="new-note common-note-form gfm-form js-main-target-form" > <div class="error-alert"></div> @@ -283,7 +291,8 @@ <textarea id="note-body" name="note[note]" - class="note-textarea js-vue-comment-form js-gfm-input js-autosize markdown-area js-vue-textarea" + class="note-textarea js-vue-comment-form +js-gfm-input js-autosize markdown-area js-vue-textarea" data-supports-quick-actions="true" aria-label="Description" v-model="note" @@ -292,17 +301,20 @@ :disabled="isSubmitting" placeholder="Write a comment or drag your files here..." @keydown.up="editCurrentUserLastNote()" - @keydown.meta.enter="handleSave()"> + @keydown.meta.enter="handleSave()" + @keydown.ctrl.enter="handleSave()"> </textarea> </markdown-field> <div class="note-form-actions"> - <div class="pull-left btn-group append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"> + <div + class="pull-left btn-group +append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"> <button @click.prevent="handleSave()" :disabled="isSubmitButtonDisabled" class="btn btn-create comment-btn js-comment-button js-comment-submit-button" type="submit"> - {{commentButtonTitle}} + {{ commentButtonTitle }} </button> <button :disabled="isSubmitButtonDisabled" @@ -344,7 +356,7 @@ <i aria-hidden="true" class="fa fa-check icon"> - </i> + </i> <div class="description"> <strong>Start discussion</strong> <p> @@ -362,7 +374,7 @@ :class="actionButtonClassNames" :disabled="isSubmitting" class="btn btn-comment btn-comment-and-close js-action-button"> - {{issueActionButtonTitle}} + {{ issueActionButtonTitle }} </button> <button type="button" diff --git a/app/assets/javascripts/notes/components/discussion_locked_widget.vue b/app/assets/javascripts/notes/components/discussion_locked_widget.vue index e6f7ee56ff3..fc0722042cc 100644 --- a/app/assets/javascripts/notes/components/discussion_locked_widget.vue +++ b/app/assets/javascripts/notes/components/discussion_locked_widget.vue @@ -3,12 +3,12 @@ import Issuable from '~/vue_shared/mixins/issuable'; export default { - mixins: [ - Issuable, - ], components: { Icon, }, + mixins: [ + Issuable, + ], }; </script> @@ -18,9 +18,11 @@ <icon name="lock" :size="16" - class="icon"> - </icon> - <span>This {{ issuableDisplayName }} is locked. Only <b>project members</b> can comment.</span> - </span> + class="icon" + /> + <span> + This {{ issuableDisplayName }} is locked. Only <b>project members</b> can comment. + </span> + </span> </div> </template> diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue index 7fb45ed4d4b..46ffb60aa60 100644 --- a/app/assets/javascripts/notes/components/note_actions.vue +++ b/app/assets/javascripts/notes/components/note_actions.vue @@ -9,7 +9,13 @@ import tooltip from '~/vue_shared/directives/tooltip'; export default { - name: 'noteActions', + name: 'NoteActions', + directives: { + tooltip, + }, + components: { + loadingIcon, + }, props: { authorId: { type: Number, @@ -41,12 +47,6 @@ required: true, }, }, - directives: { - tooltip, - }, - components: { - loadingIcon, - }, computed: { ...mapGetters([ 'getUserDataByProp', @@ -64,6 +64,13 @@ return this.getUserDataByProp('id'); }, }, + created() { + this.emojiSmiling = emojiSmiling; + this.emojiSmile = emojiSmile; + this.emojiSmiley = emojiSmiley; + this.editSvg = editSvg; + this.ellipsisSvg = ellipsisSvg; + }, methods: { onEdit() { this.$emit('handleEdit'); @@ -72,13 +79,6 @@ this.$emit('handleDelete'); }, }, - created() { - this.emojiSmiling = emojiSmiling; - this.emojiSmile = emojiSmile; - this.emojiSmiley = emojiSmiley; - this.editSvg = editSvg; - this.ellipsisSvg = ellipsisSvg; - }, }; </script> @@ -86,7 +86,9 @@ <div class="note-actions"> <span v-if="accessLevel" - class="note-role user-access-role">{{accessLevel}}</span> + class="note-role user-access-role"> + {{ accessLevel }} + </span> <div v-if="canAddAwardEmoji" class="note-actions-item"> @@ -98,20 +100,21 @@ data-placement="bottom" data-container="body" href="#" - title="Add reaction"> - <loading-icon :inline="true" /> - <span - v-html="emojiSmiling" - class="link-highlight award-control-icon-neutral"> - </span> - <span - v-html="emojiSmiley" - class="link-highlight award-control-icon-positive"> - </span> - <span - v-html="emojiSmile" - class="link-highlight award-control-icon-super-positive"> - </span> + title="Add reaction" + > + <loading-icon :inline="true" /> + <span + v-html="emojiSmiling" + class="link-highlight award-control-icon-neutral"> + </span> + <span + v-html="emojiSmiley" + class="link-highlight award-control-icon-positive"> + </span> + <span + v-html="emojiSmile" + class="link-highlight award-control-icon-super-positive"> + </span> </a> </div> <div @@ -125,9 +128,10 @@ class="note-action-button js-note-edit btn btn-transparent" data-container="body" data-placement="bottom"> - <span - v-html="editSvg" - class="link-highlight"></span> + <span + v-html="editSvg" + class="link-highlight"> + </span> </button> </div> <div @@ -141,9 +145,10 @@ data-toggle="dropdown" data-container="body" data-placement="bottom"> - <span - class="icon" - v-html="ellipsisSvg"></span> + <span + class="icon" + v-html="ellipsisSvg"> + </span> </button> <ul class="dropdown-menu more-actions-dropdown dropdown-open-left"> <li v-if="canReportAsAbuse"> diff --git a/app/assets/javascripts/notes/components/note_attachment.vue b/app/assets/javascripts/notes/components/note_attachment.vue index cd9571a4002..618b807b9cc 100644 --- a/app/assets/javascripts/notes/components/note_attachment.vue +++ b/app/assets/javascripts/notes/components/note_attachment.vue @@ -1,6 +1,6 @@ <script> export default { - name: 'noteAttachment', + name: 'NoteAttachment', props: { attachment: { type: Object, @@ -19,7 +19,8 @@ rel="noopener noreferrer"> <img :src="attachment.url" - class="note-image-attach" /> + class="note-image-attach" + /> </a> <div class="attachment"> <a @@ -29,8 +30,9 @@ rel="noopener noreferrer"> <i class="fa fa-paperclip" - aria-hidden="true"></i> - {{attachment.filename}} + aria-hidden="true"> + </i> + {{ attachment.filename }} </a> </div> </div> diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue index c3a340139e7..caa9701e03f 100644 --- a/app/assets/javascripts/notes/components/note_awards_list.vue +++ b/app/assets/javascripts/notes/components/note_awards_list.vue @@ -8,6 +8,9 @@ import tooltip from '../../vue_shared/directives/tooltip'; export default { + directives: { + tooltip, + }, props: { awards: { type: Array, @@ -26,9 +29,6 @@ required: true, }, }, - directives: { - tooltip, - }, computed: { ...mapGetters([ 'getUserData', @@ -73,6 +73,11 @@ return this.getUserData.id; }, }, + created() { + this.emojiSmiling = emojiSmiling; + this.emojiSmile = emojiSmile; + this.emojiSmiley = emojiSmiley; + }, methods: { ...mapActions([ 'toggleAwardRequest', @@ -168,11 +173,6 @@ .catch(() => Flash('Something went wrong on our end.')); }, }, - created() { - this.emojiSmiling = emojiSmiling; - this.emojiSmile = emojiSmile; - this.emojiSmiley = emojiSmiley; - }, }; </script> @@ -191,7 +191,7 @@ type="button"> <span v-html="getAwardHTML(awardName)"></span> <span class="award-control-text js-counter"> - {{awardList.length}} + {{ awardList.length }} </span> </button> <div diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue index ac4e1ffe53a..2d7cd30115d 100644 --- a/app/assets/javascripts/notes/components/note_body.vue +++ b/app/assets/javascripts/notes/components/note_body.vue @@ -7,6 +7,15 @@ import autosave from '../mixins/autosave'; export default { + components: { + noteEditedText, + noteAwardsList, + noteAttachment, + noteForm, + }, + mixins: [ + autosave, + ], props: { note: { type: Object, @@ -22,40 +31,11 @@ default: false, }, }, - mixins: [ - autosave, - ], - components: { - noteEditedText, - noteAwardsList, - noteAttachment, - noteForm, - }, computed: { noteBody() { return this.note.note; }, }, - 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); - }, - }, mounted() { this.renderGFM(); this.initTaskList(); @@ -76,6 +56,26 @@ } } }, + 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); + }, + }, }; </script> @@ -95,7 +95,7 @@ :is-editing="isEditing" :note-body="noteBody" :note-id="note.id" - /> + /> <textarea v-if="canEdit" v-model="note.note" @@ -106,17 +106,17 @@ :edited-at="note.last_edited_at" :edited-by="note.last_edited_by" action-text="Edited" - /> + /> <note-awards-list v-if="note.award_emoji.length" :note-id="note.id" :note-author-id="note.author.id" :awards="note.award_emoji" :toggle-award-path="note.toggle_award_path" - /> + /> <note-attachment v-if="note.attachment" :attachment="note.attachment" - /> + /> </div> </template> diff --git a/app/assets/javascripts/notes/components/note_edited_text.vue b/app/assets/javascripts/notes/components/note_edited_text.vue index 49e09f0ecc5..ae2e52554d2 100644 --- a/app/assets/javascripts/notes/components/note_edited_text.vue +++ b/app/assets/javascripts/notes/components/note_edited_text.vue @@ -2,7 +2,10 @@ import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; export default { - name: 'editedNoteText', + name: 'EditedNoteText', + components: { + timeAgoTooltip, + }, props: { actionText: { type: String, @@ -15,6 +18,7 @@ editedBy: { type: Object, required: false, + default: () => ({}), }, className: { type: String, @@ -22,25 +26,22 @@ default: 'edited-text', }, }, - components: { - timeAgoTooltip, - }, }; </script> <template> <div :class="className"> - {{actionText}} + {{ actionText }} <time-ago-tooltip :time="editedAt" tooltip-placement="bottom" - /> + /> <template v-if="editedBy"> by <a :href="editedBy.path" class="js-vue-author author_link"> - {{editedBy.name}} + {{ editedBy.name }} </a> </template> </div> diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index 4d527cb6643..d382a9bb642 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -6,7 +6,14 @@ import issuableStateMixin from '../mixins/issuable_state'; export default { - name: 'issueNoteForm', + name: 'IssueNoteForm', + components: { + issueWarning, + markdownField, + }, + mixins: [ + issuableStateMixin, + ], props: { noteBody: { type: String, @@ -16,6 +23,7 @@ noteId: { type: Number, required: false, + default: 0, }, saveButtonTitle: { type: String, @@ -39,10 +47,6 @@ isSubmitting: false, }; }, - components: { - issueWarning, - markdownField, - }, computed: { ...mapGetters([ 'getDiscussionLastNote', @@ -70,6 +74,18 @@ return !this.note.length || this.isSubmitting; }, }, + watch: { + noteBody() { + if (this.note === this.noteBody) { + this.note = this.noteBody; + } else { + this.conflictWhileEditing = true; + } + }, + }, + mounted() { + this.$refs.textarea.focus(); + }, methods: { handleUpdate() { this.isSubmitting = true; @@ -94,26 +110,13 @@ this.$emit('cancelFormEdition', shouldConfirm, this.noteBody !== this.note); }, }, - mixins: [ - issuableStateMixin, - ], - mounted() { - this.$refs.textarea.focus(); - }, - watch: { - noteBody() { - if (this.note === this.noteBody) { - this.note = this.noteBody; - } else { - this.conflictWhileEditing = true; - } - }, - }, }; </script> <template> - <div ref="editNoteForm" class="note-edit-form current-note-edit-form"> + <div + ref="editNoteForm" + class="note-edit-form current-note-edit-form"> <div v-if="conflictWhileEditing" class="js-conflict-edit-warning alert alert-danger"> @@ -121,12 +124,13 @@ <a :href="noteHash" target="_blank" - rel="noopener noreferrer">updated comment</a> - to ensure information is not lost. + rel="noopener noreferrer"> + updated comment + </a> + to ensure information is not lost. </div> <div class="flash-container timeline-content"></div> - <form - class="edit-note common-note-form js-quick-submit gfm-form"> + <form class="edit-note common-note-form js-quick-submit gfm-form"> <issue-warning v-if="hasWarning(getNoteableData)" @@ -142,7 +146,8 @@ <textarea id="note_note" name="note[note]" - class="note-textarea js-gfm-input js-autosize markdown-area js-vue-issue-note-form js-vue-textarea" + class="note-textarea js-gfm-input +js-autosize markdown-area js-vue-issue-note-form js-vue-textarea" :data-supports-quick-actions="!isEditing" aria-label="Description" v-model="note" @@ -150,6 +155,7 @@ slot="textarea" placeholder="Write a comment or drag your files here..." @keydown.meta.enter="handleUpdate()" + @keydown.ctrl.enter="handleUpdate()" @keydown.up="editMyLastNote()" @keydown.esc="cancelHandler(true)"> </textarea> @@ -160,7 +166,7 @@ @click="handleUpdate()" :disabled="isDisabled" class="js-vue-issue-save btn btn-save"> - {{saveButtonTitle}} + {{ saveButtonTitle }} </button> <button @click="cancelHandler()" diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue index 63aa3d777d0..5b255d4a710 100644 --- a/app/assets/javascripts/notes/components/note_header.vue +++ b/app/assets/javascripts/notes/components/note_header.vue @@ -3,6 +3,9 @@ import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; export default { + components: { + timeAgoTooltip, + }, props: { author: { type: Object, @@ -37,9 +40,6 @@ isExpanded: true, }; }, - components: { - timeAgoTooltip, - }, computed: { toggleChevronClass() { return this.isExpanded ? 'fa-chevron-up' : 'fa-chevron-down'; @@ -66,17 +66,15 @@ <template> <div class="note-header-info"> <a :href="author.path"> - <span class="note-header-author-name"> - {{author.name}} - </span> + <span class="note-header-author-name">{{ author.name }}</span> <span class="note-headline-light"> - @{{author.username}} + @{{ author.username }} </span> </a> <span class="note-headline-light"> <span class="note-headline-meta"> <template v-if="actionText"> - {{actionText}} + {{ actionText }} </template> <span v-if="actionTextHtml" @@ -90,12 +88,13 @@ <time-ago-tooltip :time="createdAt" tooltip-placement="bottom" - /> + /> </a> <i class="fa fa-spinner fa-spin editing-spinner" aria-label="Comment is being updated" - aria-hidden="true"> + aria-hidden="true" + > </i> </span> </span> @@ -106,12 +105,12 @@ @click="handleToggle" class="note-action-button discussion-toggle-button js-vue-toggle-button" type="button"> - <i - :class="toggleChevronClass" - class="fa" - aria-hidden="true"> - </i> - Toggle discussion + <i + :class="toggleChevronClass" + class="fa" + aria-hidden="true"> + </i> + Toggle discussion </button> </div> </div> diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index 11e8f805635..98a06c5fc71 100644 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -13,17 +13,6 @@ import autosave from '../mixins/autosave'; export default { - props: { - note: { - type: Object, - required: true, - }, - }, - data() { - return { - isReplying: false, - }; - }, components: { noteableNote, userAvatarLink, @@ -37,6 +26,17 @@ mixins: [ autosave, ], + props: { + note: { + type: Object, + required: true, + }, + }, + data() { + return { + isReplying: false, + }; + }, computed: { ...mapGetters([ 'getNoteableData', @@ -72,6 +72,20 @@ return null; }, }, + mounted() { + if (this.isReplying) { + this.initAutoSave(); + } + }, + updated() { + if (this.isReplying) { + if (!this.autosave) { + this.initAutoSave(); + } else { + this.setAutoSave(); + } + } + }, methods: { ...mapActions([ 'saveNote', @@ -130,7 +144,8 @@ this.removePlaceholderNotes(); this.isReplying = true; this.$nextTick(() => { - const msg = 'Your comment could not be submitted! Please check your network connection and try again.'; + 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); @@ -138,20 +153,6 @@ }); }, }, - mounted() { - if (this.isReplying) { - this.initAutoSave(); - } - }, - updated() { - if (this.isReplying) { - if (!this.autosave) { - this.initAutoSave(); - } else { - this.setAutoSave(); - } - } - }, }; </script> @@ -164,7 +165,7 @@ :img-src="author.avatar_url" :img-alt="author.name" :img-size="40" - /> + /> </div> <div class="timeline-content"> <div class="discussion"> @@ -184,42 +185,43 @@ :edited-by="lastUpdatedBy" action-text="Last updated" class-name="discussion-headline-light js-discussion-headline" - /> - </div> + /> </div> - <div - v-if="note.expanded" - class="discussion-body"> - <div class="panel panel-default"> - <div class="discussion-notes"> - <ul class="notes"> - <component - v-for="note in note.notes" - :is="componentName(note)" - :note="componentData(note)" - :key="note.id" - /> - </ul> - <div - :class="{ 'is-replying': isReplying }" - class="discussion-reply-holder"> - <button - v-if="canReply && !isReplying" - @click="showReplyForm" - type="button" - class="js-vue-discussion-reply btn btn-text-field" - title="Add a reply">Reply...</button> - <note-form - v-if="isReplying" - save-button-title="Comment" - :discussion="note" - :is-editing="false" - @handleFormUpdate="saveReply" - @cancelFormEdition="cancelReplyForm" - ref="noteForm" - /> - <note-signed-out-widget v-if="!canReply" /> - </div> + </div> + <div + v-if="note.expanded" + class="discussion-body"> + <div class="panel panel-default"> + <div class="discussion-notes"> + <ul class="notes"> + <component + v-for="note in note.notes" + :is="componentName(note)" + :note="componentData(note)" + :key="note.id" + /> + </ul> + <div + :class="{ 'is-replying': isReplying }" + class="discussion-reply-holder"> + <button + v-if="canReply && !isReplying" + @click="showReplyForm" + type="button" + class="js-vue-discussion-reply btn btn-text-field" + title="Add a reply"> + Reply... + </button> + <note-form + v-if="isReplying" + save-button-title="Comment" + :discussion="note" + :is-editing="false" + @handleFormUpdate="saveReply" + @cancelFormEdition="cancelReplyForm" + ref="noteForm" + /> + <note-signed-out-widget v-if="!canReply" /> </div> </div> </div> diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue index 9186d6ff64a..30e7ccc8229 100644 --- a/app/assets/javascripts/notes/components/noteable_note.vue +++ b/app/assets/javascripts/notes/components/noteable_note.vue @@ -9,6 +9,12 @@ import eventHub from '../event_hub'; export default { + components: { + userAvatarLink, + noteHeader, + noteActions, + noteBody, + }, props: { note: { type: Object, @@ -22,12 +28,6 @@ isRequesting: false, }; }, - components: { - userAvatarLink, - noteHeader, - noteActions, - noteBody, - }, computed: { ...mapGetters([ 'targetNoteHash', @@ -51,6 +51,16 @@ return `note_${this.note.id}`; }, }, + + created() { + eventHub.$on('enterEditMode', ({ noteId }) => { + if (noteId === this.note.id) { + this.isEditing = true; + this.scrollToNoteIfNeeded($(this.$el)); + } + }); + }, + methods: { ...mapActions([ 'deleteNote', @@ -126,14 +136,6 @@ this.$refs.noteBody.$refs.noteForm.note = noteText; }, }, - created() { - eventHub.$on('enterEditMode', ({ noteId }) => { - if (noteId === this.note.id) { - this.isEditing = true; - this.scrollToNoteIfNeeded($(this.$el)); - } - }); - }, }; </script> @@ -150,7 +152,7 @@ :img-src="author.avatar_url" :img-alt="author.name" :img-size="40" - /> + /> </div> <div class="timeline-content"> <div class="note-header"> @@ -159,7 +161,7 @@ :created-at="note.created_at" :note-id="note.id" action-text="commented" - /> + /> <note-actions :author-id="author.id" :note-id="note.id" @@ -170,7 +172,7 @@ :report-abuse-path="note.report_abuse_path" @handleEdit="editHandler" @handleDelete="deleteHandler" - /> + /> </div> <note-body :note="note" @@ -179,7 +181,7 @@ @handleFormUpdate="formUpdateHandler" @cancelFormEdition="formCancelHandler" ref="noteBody" - /> + /> </div> </div> </li> diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue index c4cae4b3b6f..92db4830704 100644 --- a/app/assets/javascripts/notes/components/notes_app.vue +++ b/app/assets/javascripts/notes/components/notes_app.vue @@ -13,7 +13,16 @@ import loadingIcon from '../../vue_shared/components/loading_icon.vue'; export default { - name: 'notesApp', + name: 'NotesApp', + components: { + noteableNote, + noteableDiscussion, + systemNote, + commentForm, + loadingIcon, + placeholderNote, + placeholderSystemNote, + }, props: { noteableData: { type: Object, @@ -26,7 +35,7 @@ userData: { type: Object, required: false, - default: {}, + default: () => ({}), }, }, store, @@ -35,21 +44,30 @@ isLoading: true, }; }, - components: { - noteableNote, - noteableDiscussion, - systemNote, - commentForm, - loadingIcon, - placeholderNote, - placeholderSystemNote, - }, computed: { ...mapGetters([ 'notes', 'getNotesDataByProp', ]), }, + created() { + this.setNotesData(this.notesData); + this.setNoteableData(this.noteableData); + this.setUserData(this.userData); + }, + mounted() { + this.fetchNotes(); + + 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 }); + }); + } + }, methods: { ...mapActions({ actionFetchNotes: 'fetchNotes', @@ -105,24 +123,6 @@ } }, }, - created() { - this.setNotesData(this.notesData); - this.setNoteableData(this.noteableData); - this.setUserData(this.userData); - }, - mounted() { - this.fetchNotes(); - - 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 }); - }); - } - }, }; </script> @@ -144,7 +144,7 @@ :is="getComponentName(note)" :note="getComponentData(note)" :key="note.id" - /> + /> </ul> <comment-form /> diff --git a/app/assets/javascripts/notifications_form.js b/app/assets/javascripts/notifications_form.js index 4534360d577..4e0afe13590 100644 --- a/app/assets/javascripts/notifications_form.js +++ b/app/assets/javascripts/notifications_form.js @@ -1,3 +1,7 @@ +import { __ } from './locale'; +import axios from './lib/utils/axios_utils'; +import flash from './flash'; + export default class NotificationsForm { constructor() { this.toggleCheckbox = this.toggleCheckbox.bind(this); @@ -27,24 +31,20 @@ export default class NotificationsForm { saveEvent($checkbox, $parent) { const form = $parent.parents('form:first'); - return $.ajax({ - url: form.attr('action'), - method: form.attr('method'), - dataType: 'json', - data: form.serialize(), - beforeSend: () => { - this.showCheckboxLoadingSpinner($parent); - }, - }).done((data) => { - $checkbox.enable(); - if (data.saved) { - $parent.find('.custom-notification-event-loading').toggleClass('fa-spin fa-spinner fa-check is-done'); - setTimeout(() => { - $parent.removeClass('is-loading') - .find('.custom-notification-event-loading') - .toggleClass('fa-spin fa-spinner fa-check is-done'); - }, 2000); - } - }); + this.showCheckboxLoadingSpinner($parent); + + axios[form.attr('method')](form.attr('action'), form.serialize()) + .then(({ data }) => { + $checkbox.enable(); + if (data.saved) { + $parent.find('.custom-notification-event-loading').toggleClass('fa-spin fa-spinner fa-check is-done'); + setTimeout(() => { + $parent.removeClass('is-loading') + .find('.custom-notification-event-loading') + .toggleClass('fa-spin fa-spinner fa-check is-done'); + }, 2000); + } + }) + .catch(() => flash(__('There was an error saving your notification settings.'))); } } diff --git a/app/assets/javascripts/pager.js b/app/assets/javascripts/pager.js index 6552a88b606..fd3105b1960 100644 --- a/app/assets/javascripts/pager.js +++ b/app/assets/javascripts/pager.js @@ -1,4 +1,5 @@ import { getParameterByName } from '~/lib/utils/common_utils'; +import axios from './lib/utils/axios_utils'; import { removeParams } from './lib/utils/url_utility'; const ENDLESS_SCROLL_BOTTOM_PX = 400; @@ -22,24 +23,22 @@ export default { getOld() { this.loading.show(); - $.ajax({ - type: 'GET', - url: this.url, - data: `limit=${this.limit}&offset=${this.offset}`, - dataType: 'json', - error: () => this.loading.hide(), - success: (data) => { - this.append(data.count, this.prepareData(data.html)); - this.callback(); - - // keep loading until we've filled the viewport height - if (!this.disable && !this.isScrollable()) { - this.getOld(); - } else { - this.loading.hide(); - } + axios.get(this.url, { + params: { + limit: this.limit, + offset: this.offset, }, - }); + }).then(({ data }) => { + this.append(data.count, this.prepareData(data.html)); + this.callback(); + + // keep loading until we've filled the viewport height + if (!this.disable && !this.isScrollable()) { + this.getOld(); + } else { + this.loading.hide(); + } + }).catch(() => this.loading.hide()); }, append(count, html) { diff --git a/app/assets/javascripts/abuse_reports.js b/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js index d2d3a257c0d..d87e6304a24 100644 --- a/app/assets/javascripts/abuse_reports.js +++ b/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js @@ -1,4 +1,4 @@ -import { truncate } from './lib/utils/text_utility'; +import { truncate } from '../../../lib/utils/text_utility'; const MAX_MESSAGE_LENGTH = 500; const MESSAGE_CELL_SELECTOR = '.abuse-reports .message'; diff --git a/app/assets/javascripts/pages/admin/abuse_reports/index.js b/app/assets/javascripts/pages/admin/abuse_reports/index.js new file mode 100644 index 00000000000..c0b6e8d4095 --- /dev/null +++ b/app/assets/javascripts/pages/admin/abuse_reports/index.js @@ -0,0 +1,3 @@ +import AbuseReports from './abuse_reports'; + +export default () => new AbuseReports(); diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/pages/admin/admin.js index c1f7fa2aced..135c15c346b 100644 --- a/app/assets/javascripts/admin.js +++ b/app/assets/javascripts/pages/admin/admin.js @@ -1,4 +1,4 @@ -import { refreshCurrentPage } from './lib/utils/url_utility'; +import { refreshCurrentPage } from '../../lib/utils/url_utility'; function showBlacklistType() { if ($('input[name="blacklist_type"]:checked').val() === 'file') { diff --git a/app/assets/javascripts/broadcast_message.js b/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js index ff88083a4b4..857a6793fe3 100644 --- a/app/assets/javascripts/broadcast_message.js +++ b/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js @@ -1,3 +1,5 @@ +import _ from 'underscore'; + export default function initBroadcastMessagesForm() { $('input#broadcast_message_color').on('input', function onMessageColorInput() { const previewColor = $(this).val(); diff --git a/app/assets/javascripts/pages/admin/broadcast_messages/index.js b/app/assets/javascripts/pages/admin/broadcast_messages/index.js new file mode 100644 index 00000000000..b548c48282a --- /dev/null +++ b/app/assets/javascripts/pages/admin/broadcast_messages/index.js @@ -0,0 +1,3 @@ +import initBroadcastMessagesForm from './broadcast_message'; + +export default () => initBroadcastMessagesForm(); diff --git a/app/assets/javascripts/pages/admin/cohorts/index.js b/app/assets/javascripts/pages/admin/cohorts/index.js new file mode 100644 index 00000000000..42ef9d38ef7 --- /dev/null +++ b/app/assets/javascripts/pages/admin/cohorts/index.js @@ -0,0 +1,3 @@ +import initUsagePing from './usage_ping'; + +export default () => initUsagePing(); diff --git a/app/assets/javascripts/pages/admin/cohorts/usage_ping.js b/app/assets/javascripts/pages/admin/cohorts/usage_ping.js new file mode 100644 index 00000000000..914a9661c27 --- /dev/null +++ b/app/assets/javascripts/pages/admin/cohorts/usage_ping.js @@ -0,0 +1,13 @@ +import axios from '../../../lib/utils/axios_utils'; +import { __ } from '../../../locale'; +import flash from '../../../flash'; + +export default function UsagePing() { + const el = document.querySelector('.usage-data'); + + axios.get(el.dataset.endpoint, { + responseType: 'text', + }).then(({ data }) => { + el.innerHTML = data; + }).catch(() => flash(__('Error fetching usage ping data.'))); +} diff --git a/app/assets/javascripts/pages/admin/conversational_development_index/show/index.js b/app/assets/javascripts/pages/admin/conversational_development_index/show/index.js new file mode 100644 index 00000000000..6e66ef69fe1 --- /dev/null +++ b/app/assets/javascripts/pages/admin/conversational_development_index/show/index.js @@ -0,0 +1,3 @@ +import UserCallout from '../../../../user_callout'; + +export default () => new UserCallout(); diff --git a/app/assets/javascripts/pages/admin/groups/edit/index.js b/app/assets/javascripts/pages/admin/groups/edit/index.js new file mode 100644 index 00000000000..ff9ef8d2449 --- /dev/null +++ b/app/assets/javascripts/pages/admin/groups/edit/index.js @@ -0,0 +1,3 @@ +import groupAvatar from '../../../../group_avatar'; + +export default () => groupAvatar(); diff --git a/app/assets/javascripts/pages/admin/groups/new/index.js b/app/assets/javascripts/pages/admin/groups/new/index.js new file mode 100644 index 00000000000..fb5c46e4729 --- /dev/null +++ b/app/assets/javascripts/pages/admin/groups/new/index.js @@ -0,0 +1,9 @@ +import BindInOut from '../../../../behaviors/bind_in_out'; +import Group from '../../../../group'; +import groupAvatar from '../../../../group_avatar'; + +export default () => { + BindInOut.initAll(); + new Group(); // eslint-disable-line no-new + groupAvatar(); +}; diff --git a/app/assets/javascripts/pages/admin/groups/show/index.js b/app/assets/javascripts/pages/admin/groups/show/index.js new file mode 100644 index 00000000000..5defea104d4 --- /dev/null +++ b/app/assets/javascripts/pages/admin/groups/show/index.js @@ -0,0 +1,3 @@ +import UsersSelect from '../../../../users_select'; + +export default () => new UsersSelect(); diff --git a/app/assets/javascripts/pages/admin/impersonation_tokens/index.js b/app/assets/javascripts/pages/admin/impersonation_tokens/index.js new file mode 100644 index 00000000000..030328a1363 --- /dev/null +++ b/app/assets/javascripts/pages/admin/impersonation_tokens/index.js @@ -0,0 +1,3 @@ +import DueDateSelectors from '../../../due_date_select'; + +export default () => new DueDateSelectors(); diff --git a/app/assets/javascripts/pages/admin/index.js b/app/assets/javascripts/pages/admin/index.js new file mode 100644 index 00000000000..8b843037d85 --- /dev/null +++ b/app/assets/javascripts/pages/admin/index.js @@ -0,0 +1,3 @@ +import initAdmin from './admin'; + +export default () => initAdmin(); diff --git a/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue b/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue new file mode 100644 index 00000000000..555725cbe12 --- /dev/null +++ b/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue @@ -0,0 +1,47 @@ +<script> + import axios from '~/lib/utils/axios_utils'; + import Flash from '~/flash'; + import modal from '~/vue_shared/components/modal.vue'; + import { s__ } from '~/locale'; + import { redirectTo } from '~/lib/utils/url_utility'; + + export default { + components: { + modal, + }, + props: { + url: { + type: String, + required: true, + }, + }, + computed: { + text() { + return s__('AdminArea|You’re about to stop all jobs. This will halt all current jobs that are running.'); + }, + }, + methods: { + onSubmit() { + return axios.post(this.url) + .then((response) => { + // follow the rediect to refresh the page + redirectTo(response.request.responseURL); + }) + .catch((error) => { + Flash(s__('AdminArea|Stopping jobs failed')); + throw error; + }); + }, + }, + }; +</script> + +<template> + <modal + id="stop-jobs-modal" + :title="s__('AdminArea|Stop all jobs?')" + :text="text" + kind="danger" + :primary-button-label="s__('AdminArea|Stop jobs')" + @submit="onSubmit" /> +</template> diff --git a/app/assets/javascripts/pages/admin/jobs/index/index.js b/app/assets/javascripts/pages/admin/jobs/index/index.js new file mode 100644 index 00000000000..0e004bd9174 --- /dev/null +++ b/app/assets/javascripts/pages/admin/jobs/index/index.js @@ -0,0 +1,29 @@ +import Vue from 'vue'; + +import Translate from '~/vue_shared/translate'; + +import stopJobsModal from './components/stop_jobs_modal.vue'; + +Vue.use(Translate); + +export default () => { + const stopJobsButton = document.getElementById('stop-jobs-button'); + + // eslint-disable-next-line no-new + new Vue({ + el: '#stop-jobs-modal', + components: { + stopJobsModal, + }, + mounted() { + stopJobsButton.classList.remove('disabled'); + }, + render(createElement) { + return createElement('stop-jobs-modal', { + props: { + url: stopJobsButton.dataset.url, + }, + }); + }, + }); +}; diff --git a/app/assets/javascripts/pages/admin/labels/edit/index.js b/app/assets/javascripts/pages/admin/labels/edit/index.js new file mode 100644 index 00000000000..d7ec6e47f67 --- /dev/null +++ b/app/assets/javascripts/pages/admin/labels/edit/index.js @@ -0,0 +1,3 @@ +import Labels from '../../../../labels'; + +export default () => new Labels(); diff --git a/app/assets/javascripts/pages/admin/labels/new/index.js b/app/assets/javascripts/pages/admin/labels/new/index.js new file mode 100644 index 00000000000..d7ec6e47f67 --- /dev/null +++ b/app/assets/javascripts/pages/admin/labels/new/index.js @@ -0,0 +1,3 @@ +import Labels from '../../../../labels'; + +export default () => new Labels(); diff --git a/app/assets/javascripts/pages/admin/projects/index.js b/app/assets/javascripts/pages/admin/projects/index.js new file mode 100644 index 00000000000..71e0ddcd7b6 --- /dev/null +++ b/app/assets/javascripts/pages/admin/projects/index.js @@ -0,0 +1,9 @@ +import ProjectsList from '../../../projects_list'; +import NamespaceSelect from '../../../namespace_select'; + +export default () => { + new ProjectsList(); // eslint-disable-line no-new + + document.querySelectorAll('.js-namespace-select') + .forEach(dropdown => new NamespaceSelect({ dropdown })); +}; diff --git a/app/assets/javascripts/ci_lint_editor.js b/app/assets/javascripts/pages/ci/lints/ci_lint_editor.js index b9469e5b7cb..b9469e5b7cb 100644 --- a/app/assets/javascripts/ci_lint_editor.js +++ b/app/assets/javascripts/pages/ci/lints/ci_lint_editor.js diff --git a/app/assets/javascripts/pages/ci/lints/index.js b/app/assets/javascripts/pages/ci/lints/index.js new file mode 100644 index 00000000000..5cc66546109 --- /dev/null +++ b/app/assets/javascripts/pages/ci/lints/index.js @@ -0,0 +1,3 @@ +import CILintEditor from './ci_lint_editor'; + +export default () => new CILintEditor(); diff --git a/app/assets/javascripts/pages/constants.js b/app/assets/javascripts/pages/constants.js new file mode 100644 index 00000000000..328b6541636 --- /dev/null +++ b/app/assets/javascripts/pages/constants.js @@ -0,0 +1,6 @@ +/* eslint-disable import/prefer-default-export */ + +export const FILTERED_SEARCH = { + MERGE_REQUESTS: 'merge_requests', + ISSUES: 'issues', +}; diff --git a/app/assets/javascripts/pages/dashboard/activity/index.js b/app/assets/javascripts/pages/dashboard/activity/index.js new file mode 100644 index 00000000000..95faf1f1e98 --- /dev/null +++ b/app/assets/javascripts/pages/dashboard/activity/index.js @@ -0,0 +1,3 @@ +import Activities from '~/activities'; + +export default () => new Activities(); diff --git a/app/assets/javascripts/pages/dashboard/groups/index/index.js b/app/assets/javascripts/pages/dashboard/groups/index/index.js new file mode 100644 index 00000000000..8a2aae706c0 --- /dev/null +++ b/app/assets/javascripts/pages/dashboard/groups/index/index.js @@ -0,0 +1,5 @@ +import initGroupsList from '../../../../groups'; + +export default () => { + initGroupsList(); +}; diff --git a/app/assets/javascripts/pages/dashboard/issues/index.js b/app/assets/javascripts/pages/dashboard/issues/index.js new file mode 100644 index 00000000000..b7353669e65 --- /dev/null +++ b/app/assets/javascripts/pages/dashboard/issues/index.js @@ -0,0 +1,7 @@ +import projectSelect from '~/project_select'; +import initLegacyFilters from '~/init_legacy_filters'; + +export default () => { + projectSelect(); + initLegacyFilters(); +}; diff --git a/app/assets/javascripts/pages/dashboard/merge_requests/index.js b/app/assets/javascripts/pages/dashboard/merge_requests/index.js new file mode 100644 index 00000000000..b7353669e65 --- /dev/null +++ b/app/assets/javascripts/pages/dashboard/merge_requests/index.js @@ -0,0 +1,7 @@ +import projectSelect from '~/project_select'; +import initLegacyFilters from '~/init_legacy_filters'; + +export default () => { + projectSelect(); + initLegacyFilters(); +}; diff --git a/app/assets/javascripts/pages/dashboard/milestones/index/index.js b/app/assets/javascripts/pages/dashboard/milestones/index/index.js new file mode 100644 index 00000000000..0f2f1bd4a25 --- /dev/null +++ b/app/assets/javascripts/pages/dashboard/milestones/index/index.js @@ -0,0 +1,3 @@ +import projectSelect from '~/project_select'; + +export default projectSelect; diff --git a/app/assets/javascripts/pages/dashboard/milestones/show/index.js b/app/assets/javascripts/pages/dashboard/milestones/show/index.js new file mode 100644 index 00000000000..2e7a08a369c --- /dev/null +++ b/app/assets/javascripts/pages/dashboard/milestones/show/index.js @@ -0,0 +1,7 @@ +import Milestone from '~/milestone'; +import Sidebar from '~/right_sidebar'; + +export default () => { + new Milestone(); // eslint-disable-line no-new + new Sidebar(); // eslint-disable-line no-new +}; diff --git a/app/assets/javascripts/pages/dashboard/projects/index.js b/app/assets/javascripts/pages/dashboard/projects/index.js new file mode 100644 index 00000000000..c88cbf1a6ba --- /dev/null +++ b/app/assets/javascripts/pages/dashboard/projects/index.js @@ -0,0 +1,3 @@ +import ProjectsList from '~/projects_list'; + +export default () => new ProjectsList(); diff --git a/app/assets/javascripts/pages/dashboard/todos/index/index.js b/app/assets/javascripts/pages/dashboard/todos/index/index.js new file mode 100644 index 00000000000..77c23685943 --- /dev/null +++ b/app/assets/javascripts/pages/dashboard/todos/index/index.js @@ -0,0 +1,3 @@ +import Todos from './todos'; + +export default () => new Todos(); diff --git a/app/assets/javascripts/todos.js b/app/assets/javascripts/pages/dashboard/todos/index/todos.js index 748caecf153..b3f6a72fdcb 100644 --- a/app/assets/javascripts/todos.js +++ b/app/assets/javascripts/pages/dashboard/todos/index/todos.js @@ -1,7 +1,10 @@ /* eslint-disable class-methods-use-this, no-unneeded-ternary, quote-props */ -import { visitUrl } from './lib/utils/url_utility'; -import UsersSelect from './users_select'; -import { isMetaClick } from './lib/utils/common_utils'; +import { visitUrl } from '~/lib/utils/url_utility'; +import UsersSelect from '~/users_select'; +import { isMetaClick } from '~/lib/utils/common_utils'; +import { __ } from '../../../../locale'; +import flash from '../../../../flash'; +import axios from '../../../../lib/utils/axios_utils'; export default class Todos { constructor() { @@ -59,18 +62,12 @@ export default class Todos { const target = e.target; target.setAttribute('disabled', true); target.classList.add('disabled'); - $.ajax({ - type: 'POST', - url: target.dataset.href, - dataType: 'json', - data: { - '_method': target.dataset.method, - }, - success: (data) => { + + axios[target.dataset.method](target.dataset.href) + .then(({ data }) => { this.updateRowState(target); - return this.updateBadges(data); - }, - }); + this.updateBadges(data); + }).catch(() => flash(__('Error updating todo status.'))); } updateRowState(target) { @@ -98,19 +95,15 @@ export default class Todos { e.preventDefault(); const target = e.currentTarget; - const requestData = { '_method': target.dataset.method, ids: this.todo_ids }; target.setAttribute('disabled', true); target.classList.add('disabled'); - $.ajax({ - type: 'POST', - url: target.dataset.href, - dataType: 'json', - data: requestData, - success: (data) => { - this.updateAllState(target, data); - return this.updateBadges(data); - }, - }); + + axios[target.dataset.method](target.dataset.href, { + ids: this.todo_ids, + }).then(({ data }) => { + this.updateAllState(target, data); + this.updateBadges(data); + }).catch(() => flash(__('Error updating status for all todos.'))); } updateAllState(target, data) { diff --git a/app/assets/javascripts/pages/explore/groups/index.js b/app/assets/javascripts/pages/explore/groups/index.js new file mode 100644 index 00000000000..e59c38b8bc4 --- /dev/null +++ b/app/assets/javascripts/pages/explore/groups/index.js @@ -0,0 +1,16 @@ +import GroupsList from '~/groups_list'; +import Landing from '~/landing'; +import initGroupsList from '../../../groups'; + +export default function () { + new GroupsList(); // eslint-disable-line no-new + initGroupsList(); + const landingElement = document.querySelector('.js-explore-groups-landing'); + if (!landingElement) return; + const exploreGroupsLanding = new Landing( + landingElement, + landingElement.querySelector('.dismiss-button'), + 'explore_groups_landing_dismissed', + ); + exploreGroupsLanding.toggle(); +} diff --git a/app/assets/javascripts/pages/explore/projects/index.js b/app/assets/javascripts/pages/explore/projects/index.js new file mode 100644 index 00000000000..c88cbf1a6ba --- /dev/null +++ b/app/assets/javascripts/pages/explore/projects/index.js @@ -0,0 +1,3 @@ +import ProjectsList from '~/projects_list'; + +export default () => new ProjectsList(); diff --git a/app/assets/javascripts/pages/groups/activity/index.js b/app/assets/javascripts/pages/groups/activity/index.js new file mode 100644 index 00000000000..95faf1f1e98 --- /dev/null +++ b/app/assets/javascripts/pages/groups/activity/index.js @@ -0,0 +1,3 @@ +import Activities from '~/activities'; + +export default () => new Activities(); diff --git a/app/assets/javascripts/pages/groups/edit/index.js b/app/assets/javascripts/pages/groups/edit/index.js new file mode 100644 index 00000000000..48e8c9550bf --- /dev/null +++ b/app/assets/javascripts/pages/groups/edit/index.js @@ -0,0 +1,3 @@ +import groupAvatar from '~/group_avatar'; + +export default groupAvatar; diff --git a/app/assets/javascripts/pages/groups/group_members/index/index.js b/app/assets/javascripts/pages/groups/group_members/index/index.js new file mode 100644 index 00000000000..29319b97ae2 --- /dev/null +++ b/app/assets/javascripts/pages/groups/group_members/index/index.js @@ -0,0 +1,11 @@ +/* eslint-disable no-new */ + +import memberExpirationDate from '~/member_expiration_date'; +import Members from '~/members'; +import UsersSelect from '~/users_select'; + +export default () => { + memberExpirationDate(); + new Members(); + new UsersSelect(); +}; diff --git a/app/assets/javascripts/pages/groups/issues/index.js b/app/assets/javascripts/pages/groups/issues/index.js new file mode 100644 index 00000000000..78db543a64d --- /dev/null +++ b/app/assets/javascripts/pages/groups/issues/index.js @@ -0,0 +1,8 @@ +import projectSelect from '~/project_select'; +import initFilteredSearch from '~/pages/search/init_filtered_search'; +import { FILTERED_SEARCH } from '~/pages/constants'; + +export default () => { + initFilteredSearch(FILTERED_SEARCH.ISSUES); + projectSelect(); +}; diff --git a/app/assets/javascripts/pages/groups/labels/edit/index.js b/app/assets/javascripts/pages/groups/labels/edit/index.js new file mode 100644 index 00000000000..72c5e4744ac --- /dev/null +++ b/app/assets/javascripts/pages/groups/labels/edit/index.js @@ -0,0 +1,3 @@ +import Labels from '~/labels'; + +export default () => new Labels(); diff --git a/app/assets/javascripts/pages/groups/labels/index/index.js b/app/assets/javascripts/pages/groups/labels/index/index.js new file mode 100644 index 00000000000..018345fa112 --- /dev/null +++ b/app/assets/javascripts/pages/groups/labels/index/index.js @@ -0,0 +1,3 @@ +import initLabels from '~/init_labels'; + +export default initLabels; diff --git a/app/assets/javascripts/pages/groups/labels/new/index.js b/app/assets/javascripts/pages/groups/labels/new/index.js new file mode 100644 index 00000000000..72c5e4744ac --- /dev/null +++ b/app/assets/javascripts/pages/groups/labels/new/index.js @@ -0,0 +1,3 @@ +import Labels from '~/labels'; + +export default () => new Labels(); diff --git a/app/assets/javascripts/pages/groups/merge_requests/index.js b/app/assets/javascripts/pages/groups/merge_requests/index.js new file mode 100644 index 00000000000..9b3af4537e7 --- /dev/null +++ b/app/assets/javascripts/pages/groups/merge_requests/index.js @@ -0,0 +1,8 @@ +import projectSelect from '~/project_select'; +import initFilteredSearch from '~/pages/search/init_filtered_search'; +import { FILTERED_SEARCH } from '~/pages/constants'; + +export default () => { + initFilteredSearch(FILTERED_SEARCH.MERGE_REQUESTS); + projectSelect(); +}; diff --git a/app/assets/javascripts/pages/groups/milestones/edit/index.js b/app/assets/javascripts/pages/groups/milestones/edit/index.js new file mode 100644 index 00000000000..5c99c90e24d --- /dev/null +++ b/app/assets/javascripts/pages/groups/milestones/edit/index.js @@ -0,0 +1,3 @@ +import initForm from '../../../../shared/milestones/form'; + +export default () => initForm(false); diff --git a/app/assets/javascripts/pages/groups/milestones/new/index.js b/app/assets/javascripts/pages/groups/milestones/new/index.js new file mode 100644 index 00000000000..5c99c90e24d --- /dev/null +++ b/app/assets/javascripts/pages/groups/milestones/new/index.js @@ -0,0 +1,3 @@ +import initForm from '../../../../shared/milestones/form'; + +export default () => initForm(false); diff --git a/app/assets/javascripts/pages/groups/milestones/show/index.js b/app/assets/javascripts/pages/groups/milestones/show/index.js new file mode 100644 index 00000000000..c9a18353f2e --- /dev/null +++ b/app/assets/javascripts/pages/groups/milestones/show/index.js @@ -0,0 +1,3 @@ +import initMilestonesShow from '~/pages/milestones/shared/init_milestones_show'; + +export default initMilestonesShow; diff --git a/app/assets/javascripts/pages/groups/new/index.js b/app/assets/javascripts/pages/groups/new/index.js new file mode 100644 index 00000000000..7850b90d3d2 --- /dev/null +++ b/app/assets/javascripts/pages/groups/new/index.js @@ -0,0 +1,9 @@ +import BindInOut from '~/behaviors/bind_in_out'; +import Group from '~/group'; +import groupAvatar from '~/group_avatar'; + +export default () => { + BindInOut.initAll(); + new Group(); // eslint-disable-line no-new + groupAvatar(); +}; diff --git a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js new file mode 100644 index 00000000000..f26c7360fbe --- /dev/null +++ b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js @@ -0,0 +1,11 @@ +import SecretValues from '~/behaviors/secret_values'; + +export default () => { + const secretVariableTable = document.querySelector('.js-secret-variable-table'); + if (secretVariableTable) { + const secretVariableTableValues = new SecretValues({ + container: secretVariableTable, + }); + secretVariableTableValues.init(); + } +}; diff --git a/app/assets/javascripts/pages/groups/show/index.js b/app/assets/javascripts/pages/groups/show/index.js new file mode 100644 index 00000000000..6ed0f010f15 --- /dev/null +++ b/app/assets/javascripts/pages/groups/show/index.js @@ -0,0 +1,22 @@ +/* eslint-disable no-new */ + +import NewGroupChild from '~/groups/new_group_child'; +import notificationsDropdown from '~/notifications_dropdown'; +import NotificationsForm from '~/notifications_form'; +import ProjectsList from '~/projects_list'; +import ShortcutsNavigation from '~/shortcuts_navigation'; +import initGroupsList from '../../../groups'; + +export default () => { + const newGroupChildWrapper = document.querySelector('.js-new-project-subgroup'); + new ShortcutsNavigation(); + new NotificationsForm(); + notificationsDropdown(); + new ProjectsList(); + + if (newGroupChildWrapper) { + new NewGroupChild(newGroupChildWrapper); + } + + initGroupsList(); +}; diff --git a/app/assets/javascripts/pages/help/index.js b/app/assets/javascripts/pages/help/index.js new file mode 100644 index 00000000000..4cf8afc4b7e --- /dev/null +++ b/app/assets/javascripts/pages/help/index.js @@ -0,0 +1,3 @@ +import VersionCheckImage from '../../version_check_image'; + +export default () => VersionCheckImage.bindErrorEvent($('img.js-version-status-badge')); diff --git a/app/assets/javascripts/pages/import/fogbugz/new_user_map/index.js b/app/assets/javascripts/pages/import/fogbugz/new_user_map/index.js new file mode 100644 index 00000000000..5defea104d4 --- /dev/null +++ b/app/assets/javascripts/pages/import/fogbugz/new_user_map/index.js @@ -0,0 +1,3 @@ +import UsersSelect from '../../../../users_select'; + +export default () => new UsersSelect(); diff --git a/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue new file mode 100644 index 00000000000..c43e0a0490f --- /dev/null +++ b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue @@ -0,0 +1,110 @@ +<script> + import axios from '~/lib/utils/axios_utils'; + + import Flash from '~/flash'; + import modal from '~/vue_shared/components/modal.vue'; + import { n__, s__, sprintf } from '~/locale'; + import { redirectTo } from '~/lib/utils/url_utility'; + import eventHub from '../event_hub'; + + export default { + components: { + modal, + }, + props: { + issueCount: { + type: Number, + required: true, + }, + mergeRequestCount: { + type: Number, + required: true, + }, + milestoneId: { + type: Number, + required: true, + }, + milestoneTitle: { + type: String, + required: true, + }, + milestoneUrl: { + type: String, + required: true, + }, + }, + computed: { + text() { + const milestoneTitle = sprintf('<strong>%{milestoneTitle}</strong>', { milestoneTitle: this.milestoneTitle }); + + if (this.issueCount === 0 && this.mergeRequestCount === 0) { + return sprintf( + s__(`Milestones| +You’re about to permanently delete the milestone %{milestoneTitle} from this project. +%{milestoneTitle} is not currently used in any issues or merge requests.`), + { + milestoneTitle, + }, + false, + ); + } + + return sprintf( + s__(`Milestones| +You’re about to permanently delete the milestone %{milestoneTitle} from this project and remove it from %{issuesWithCount} and %{mergeRequestsWithCount}. +Once deleted, it cannot be undone or recovered.`), + { + milestoneTitle, + issuesWithCount: n__('%d issue', '%d issues', this.issueCount), + mergeRequestsWithCount: n__('%d merge request', '%d merge requests', this.mergeRequestCount), + }, + false, + ); + }, + title() { + return sprintf(s__('Milestones|Delete milestone %{milestoneTitle}?'), { milestoneTitle: this.milestoneTitle }); + }, + }, + methods: { + onSubmit() { + eventHub.$emit('deleteMilestoneModal.requestStarted', this.milestoneUrl); + + return axios.delete(this.milestoneUrl) + .then((response) => { + eventHub.$emit('deleteMilestoneModal.requestFinished', { milestoneUrl: this.milestoneUrl, successful: true }); + + // follow the rediect to milestones overview page + redirectTo(response.request.responseURL); + }) + .catch((error) => { + eventHub.$emit('deleteMilestoneModal.requestFinished', { milestoneUrl: this.milestoneUrl, successful: false }); + + if (error.response && error.response.status === 404) { + Flash(sprintf(s__('Milestones|Milestone %{milestoneTitle} was not found'), { milestoneTitle: this.milestoneTitle })); + } else { + Flash(sprintf(s__('Milestones|Failed to delete milestone %{milestoneTitle}'), { milestoneTitle: this.milestoneTitle })); + } + throw error; + }); + }, + }, + }; +</script> + +<template> + <modal + id="delete-milestone-modal" + :title="title" + :text="text" + kind="danger" + :primary-button-label="s__('Milestones|Delete milestone')" + @submit="onSubmit"> + + <template + slot="body" + slot-scope="props"> + <p v-html="props.text"></p> + </template> + + </modal> +</template> diff --git a/app/assets/javascripts/pages/milestones/shared/event_hub.js b/app/assets/javascripts/pages/milestones/shared/event_hub.js new file mode 100644 index 00000000000..0948c2e5352 --- /dev/null +++ b/app/assets/javascripts/pages/milestones/shared/event_hub.js @@ -0,0 +1,3 @@ +import Vue from 'vue'; + +export default new Vue(); diff --git a/app/assets/javascripts/pages/milestones/shared/index.js b/app/assets/javascripts/pages/milestones/shared/index.js new file mode 100644 index 00000000000..327e2cf569c --- /dev/null +++ b/app/assets/javascripts/pages/milestones/shared/index.js @@ -0,0 +1,88 @@ +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'); + 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, + }); + }, + }); +}; diff --git a/app/assets/javascripts/pages/milestones/shared/init_milestones_show.js b/app/assets/javascripts/pages/milestones/shared/init_milestones_show.js new file mode 100644 index 00000000000..7aa5be0d5b9 --- /dev/null +++ b/app/assets/javascripts/pages/milestones/shared/init_milestones_show.js @@ -0,0 +1,9 @@ +/* eslint-disable no-new */ + +import Milestone from '~/milestone'; +import Sidebar from '~/right_sidebar'; + +export default () => { + new Milestone(); + new Sidebar(); +}; diff --git a/app/assets/javascripts/pages/omniauth_callbacks/index.js b/app/assets/javascripts/pages/omniauth_callbacks/index.js new file mode 100644 index 00000000000..54f4e56359a --- /dev/null +++ b/app/assets/javascripts/pages/omniauth_callbacks/index.js @@ -0,0 +1,5 @@ +import initU2F from '../../shared/sessions/u2f'; + +export default () => { + initU2F(); +}; diff --git a/app/assets/javascripts/pages/profiles/index/index.js b/app/assets/javascripts/pages/profiles/index/index.js new file mode 100644 index 00000000000..90eed38777a --- /dev/null +++ b/app/assets/javascripts/pages/profiles/index/index.js @@ -0,0 +1,7 @@ +import NotificationsForm from '../../../notifications_form'; +import notificationsDropdown from '../../../notifications_dropdown'; + +export default () => { + new NotificationsForm(); // eslint-disable-line no-new + notificationsDropdown(); +}; diff --git a/app/assets/javascripts/pages/profiles/personal_access_tokens/index.js b/app/assets/javascripts/pages/profiles/personal_access_tokens/index.js new file mode 100644 index 00000000000..030328a1363 --- /dev/null +++ b/app/assets/javascripts/pages/profiles/personal_access_tokens/index.js @@ -0,0 +1,3 @@ +import DueDateSelectors from '../../../due_date_select'; + +export default () => new DueDateSelectors(); diff --git a/app/assets/javascripts/pages/projects/activity/index.js b/app/assets/javascripts/pages/projects/activity/index.js new file mode 100644 index 00000000000..7af95127fd5 --- /dev/null +++ b/app/assets/javascripts/pages/projects/activity/index.js @@ -0,0 +1,7 @@ +import Activities from '~/activities'; +import ShortcutsNavigation from '~/shortcuts_navigation'; + +export default function () { + new Activities(); // eslint-disable-line no-new + new ShortcutsNavigation(); // eslint-disable-line no-new +} diff --git a/app/assets/javascripts/pages/projects/artifacts/browse/index.js b/app/assets/javascripts/pages/projects/artifacts/browse/index.js new file mode 100644 index 00000000000..02456071086 --- /dev/null +++ b/app/assets/javascripts/pages/projects/artifacts/browse/index.js @@ -0,0 +1,7 @@ +import BuildArtifacts from '~/build_artifacts'; +import ShortcutsNavigation from '~/shortcuts_navigation'; + +export default function () { + new ShortcutsNavigation(); // eslint-disable-line no-new + new BuildArtifacts(); // eslint-disable-line no-new +} diff --git a/app/assets/javascripts/pages/projects/artifacts/file/index.js b/app/assets/javascripts/pages/projects/artifacts/file/index.js new file mode 100644 index 00000000000..4cd67ac76e3 --- /dev/null +++ b/app/assets/javascripts/pages/projects/artifacts/file/index.js @@ -0,0 +1,7 @@ +import BlobViewer from '~/blob/viewer/index'; +import ShortcutsNavigation from '~/shortcuts_navigation'; + +export default function () { + new ShortcutsNavigation(); // eslint-disable-line no-new + new BlobViewer(); // eslint-disable-line no-new +} diff --git a/app/assets/javascripts/pages/projects/blame/show/index.js b/app/assets/javascripts/pages/projects/blame/show/index.js new file mode 100644 index 00000000000..480357a309c --- /dev/null +++ b/app/assets/javascripts/pages/projects/blame/show/index.js @@ -0,0 +1,3 @@ +import initBlob from '~/pages/projects/init_blob'; + +export default initBlob; diff --git a/app/assets/javascripts/pages/projects/blob/show/index.js b/app/assets/javascripts/pages/projects/blob/show/index.js new file mode 100644 index 00000000000..a3eeb1cefb6 --- /dev/null +++ b/app/assets/javascripts/pages/projects/blob/show/index.js @@ -0,0 +1,7 @@ +import BlobViewer from '~/blob/viewer/index'; +import initBlob from '~/pages/projects/init_blob'; + +export default () => { + new BlobViewer(); // eslint-disable-line no-new + initBlob(); +}; diff --git a/app/assets/javascripts/pages/projects/boards/index.js b/app/assets/javascripts/pages/projects/boards/index.js new file mode 100644 index 00000000000..42c9bb5ec99 --- /dev/null +++ b/app/assets/javascripts/pages/projects/boards/index.js @@ -0,0 +1,7 @@ +import UsersSelect from '~/users_select'; +import ShortcutsNavigation from '~/shortcuts_navigation'; + +export default () => { + new UsersSelect(); // eslint-disable-line no-new + new ShortcutsNavigation(); // eslint-disable-line no-new +}; diff --git a/app/assets/javascripts/pages/projects/branches/index/index.js b/app/assets/javascripts/pages/projects/branches/index/index.js new file mode 100644 index 00000000000..cee0f19bf2a --- /dev/null +++ b/app/assets/javascripts/pages/projects/branches/index/index.js @@ -0,0 +1,7 @@ +import AjaxLoadingSpinner from '~/ajax_loading_spinner'; +import DeleteModal from '~/branches/branches_delete_modal'; + +export default () => { + AjaxLoadingSpinner.init(); + new DeleteModal(); // eslint-disable-line no-new +}; diff --git a/app/assets/javascripts/pages/projects/branches/new/index.js b/app/assets/javascripts/pages/projects/branches/new/index.js new file mode 100644 index 00000000000..ae5e033e97e --- /dev/null +++ b/app/assets/javascripts/pages/projects/branches/new/index.js @@ -0,0 +1,3 @@ +import NewBranchForm from '~/new_branch_form'; + +export default () => new NewBranchForm($('.js-create-branch-form'), JSON.parse(document.getElementById('availableRefs').innerHTML)); diff --git a/app/assets/javascripts/pages/projects/clusters/index/index.js b/app/assets/javascripts/pages/projects/clusters/index/index.js new file mode 100644 index 00000000000..d531ab81dc7 --- /dev/null +++ b/app/assets/javascripts/pages/projects/clusters/index/index.js @@ -0,0 +1,5 @@ +import ClustersIndex from '~/clusters/clusters_index'; + +export default () => { + new ClustersIndex(); // eslint-disable-line no-new +}; diff --git a/app/assets/javascripts/pages/projects/clusters/show/index.js b/app/assets/javascripts/pages/projects/clusters/show/index.js new file mode 100644 index 00000000000..0458c02a66f --- /dev/null +++ b/app/assets/javascripts/pages/projects/clusters/show/index.js @@ -0,0 +1,5 @@ +import ClustersBundle from '~/clusters/clusters_bundle'; + +export default () => { + new ClustersBundle(); // eslint-disable-line no-new +}; diff --git a/app/assets/javascripts/pages/projects/commit/pipelines/index.js b/app/assets/javascripts/pages/projects/commit/pipelines/index.js new file mode 100644 index 00000000000..523ad567021 --- /dev/null +++ b/app/assets/javascripts/pages/projects/commit/pipelines/index.js @@ -0,0 +1,8 @@ +import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown'; + +export default () => { + new MiniPipelineGraph({ + container: '.js-commit-pipeline-graph', + }).bindEvents(); + $('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath); +}; diff --git a/app/assets/javascripts/pages/projects/commit/show/index.js b/app/assets/javascripts/pages/projects/commit/show/index.js new file mode 100644 index 00000000000..5ac38e6f278 --- /dev/null +++ b/app/assets/javascripts/pages/projects/commit/show/index.js @@ -0,0 +1,22 @@ +/* eslint-disable no-new */ +import Diff from '~/diff'; +import ZenMode from '~/zen_mode'; +import ShortcutsNavigation from '~/shortcuts_navigation'; +import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown'; +import initNotes from '~/init_notes'; +import initChangesDropdown from '~/init_changes_dropdown'; +import { fetchCommitMergeRequests } from '~/commit_merge_requests'; + +export default () => { + new Diff(); + new ZenMode(); + new ShortcutsNavigation(); + new MiniPipelineGraph({ + container: '.js-commit-pipeline-graph', + }).bindEvents(); + initNotes(); + const stickyBarPaddingTop = 16; + initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - stickyBarPaddingTop); + $('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath); + fetchCommitMergeRequests(); +}; diff --git a/app/assets/javascripts/pages/projects/commits/show/index.js b/app/assets/javascripts/pages/projects/commits/show/index.js new file mode 100644 index 00000000000..90b5882a24f --- /dev/null +++ b/app/assets/javascripts/pages/projects/commits/show/index.js @@ -0,0 +1,9 @@ +import CommitsList from '~/commits'; +import GpgBadges from '~/gpg_badges'; +import ShortcutsNavigation from '~/shortcuts_navigation'; + +export default () => { + CommitsList.init(document.querySelector('.js-project-commits-show').dataset.commitsLimit); + new ShortcutsNavigation(); // eslint-disable-line no-new + GpgBadges.fetch(); +}; diff --git a/app/assets/javascripts/pages/projects/compare/index.js b/app/assets/javascripts/pages/projects/compare/index.js new file mode 100644 index 00000000000..890062eeee6 --- /dev/null +++ b/app/assets/javascripts/pages/projects/compare/index.js @@ -0,0 +1,5 @@ +import initCompareAutocomplete from '~/compare_autocomplete'; + +export default () => { + initCompareAutocomplete(); +}; diff --git a/app/assets/javascripts/pages/projects/compare/show/index.js b/app/assets/javascripts/pages/projects/compare/show/index.js new file mode 100644 index 00000000000..6b8d4503568 --- /dev/null +++ b/app/assets/javascripts/pages/projects/compare/show/index.js @@ -0,0 +1,8 @@ +import Diff from '~/diff'; +import initChangesDropdown from '~/init_changes_dropdown'; + +export default () => { + new Diff(); // eslint-disable-line no-new + const paddingTop = 16; + initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - paddingTop); +}; diff --git a/app/assets/javascripts/pages/projects/constants.js b/app/assets/javascripts/pages/projects/constants.js new file mode 100644 index 00000000000..9efbf7cd36e --- /dev/null +++ b/app/assets/javascripts/pages/projects/constants.js @@ -0,0 +1,6 @@ +/* eslint-disable import/prefer-default-export */ + +export const ISSUABLE_INDEX = { + MERGE_REQUEST: 'merge_request_', + ISSUE: 'issue_', +}; diff --git a/app/assets/javascripts/pages/projects/edit/index.js b/app/assets/javascripts/pages/projects/edit/index.js new file mode 100644 index 00000000000..9edf36d66b1 --- /dev/null +++ b/app/assets/javascripts/pages/projects/edit/index.js @@ -0,0 +1,14 @@ +import initSettingsPanels from '~/settings_panels'; +import setupProjectEdit from '~/project_edit'; +import ProjectNew from '../shared/project_new'; +import projectAvatar from '../shared/project_avatar'; +import initProjectPermissionsSettings from '../shared/permissions'; + +export default () => { + new ProjectNew(); // eslint-disable-line no-new + setupProjectEdit(); + // Initialize expandable settings panels + initSettingsPanels(); + projectAvatar(); + initProjectPermissionsSettings(); +}; diff --git a/app/assets/javascripts/pages/projects/environments/metrics/index.js b/app/assets/javascripts/pages/projects/environments/metrics/index.js new file mode 100644 index 00000000000..f4760cb2720 --- /dev/null +++ b/app/assets/javascripts/pages/projects/environments/metrics/index.js @@ -0,0 +1,3 @@ +import monitoringBundle from '~/monitoring/monitoring_bundle'; + +export default monitoringBundle; diff --git a/app/assets/javascripts/pages/projects/find_file/show/index.js b/app/assets/javascripts/pages/projects/find_file/show/index.js new file mode 100644 index 00000000000..42bde0ff779 --- /dev/null +++ b/app/assets/javascripts/pages/projects/find_file/show/index.js @@ -0,0 +1,12 @@ +import ProjectFindFile from '~/project_find_file'; +import ShortcutsFindFile from '~/shortcuts_find_file'; + +export default () => { + const findElement = document.querySelector('.js-file-finder'); + const projectFindFile = new ProjectFindFile($('.file-finder-holder'), { + url: findElement.dataset.fileFindUrl, + treeUrl: findElement.dataset.findTreeUrl, + blobUrlTemplate: findElement.dataset.blobUrlTemplate, + }); + new ShortcutsFindFile(projectFindFile); // eslint-disable-line no-new +}; diff --git a/app/assets/javascripts/pages/projects/forks/new/index.js b/app/assets/javascripts/pages/projects/forks/new/index.js new file mode 100644 index 00000000000..7825eb01949 --- /dev/null +++ b/app/assets/javascripts/pages/projects/forks/new/index.js @@ -0,0 +1,5 @@ +import ProjectFork from '~/project_fork'; + +export default () => { + new ProjectFork(); // eslint-disable-line no-new +}; diff --git a/app/assets/javascripts/pages/projects/imports/show/index.js b/app/assets/javascripts/pages/projects/imports/show/index.js new file mode 100644 index 00000000000..378f7b3f38b --- /dev/null +++ b/app/assets/javascripts/pages/projects/imports/show/index.js @@ -0,0 +1,5 @@ +import ProjectImport from '~/project_import'; + +export default () => { + new ProjectImport(); // eslint-disable-line no-new +}; diff --git a/app/assets/javascripts/pages/projects/index.js b/app/assets/javascripts/pages/projects/index.js new file mode 100644 index 00000000000..9b1d52692a3 --- /dev/null +++ b/app/assets/javascripts/pages/projects/index.js @@ -0,0 +1,7 @@ +import Project from './project'; +import ShortcutsNavigation from '../../shortcuts_navigation'; + +export default () => { + new Project(); // eslint-disable-line no-new + new ShortcutsNavigation(); // eslint-disable-line no-new +}; diff --git a/app/assets/javascripts/pages/projects/init_blob.js b/app/assets/javascripts/pages/projects/init_blob.js new file mode 100644 index 00000000000..26f0ad46114 --- /dev/null +++ b/app/assets/javascripts/pages/projects/init_blob.js @@ -0,0 +1,33 @@ +import LineHighlighter from '~/line_highlighter'; +import BlobLinePermalinkUpdater from '~/blob/blob_line_permalink_updater'; +import ShortcutsNavigation from '~/shortcuts_navigation'; +import ShortcutsBlob from '~/shortcuts_blob'; +import BlobForkSuggestion from '~/blob/blob_fork_suggestion'; + +export default () => { + new LineHighlighter(); // eslint-disable-line no-new + + new BlobLinePermalinkUpdater( // eslint-disable-line no-new + document.querySelector('#blob-content-holder'), + '.diff-line-num[data-line-number]', + document.querySelectorAll('.js-data-file-blob-permalink-url, .js-blob-blame-link'), + ); + + const fileBlobPermalinkUrlElement = document.querySelector('.js-data-file-blob-permalink-url'); + const fileBlobPermalinkUrl = fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href'); + + new ShortcutsNavigation(); // eslint-disable-line no-new + + new ShortcutsBlob({ // eslint-disable-line no-new + skipResetBindings: true, + fileBlobPermalinkUrl, + }); + + new BlobForkSuggestion({ // eslint-disable-line no-new + openButtons: document.querySelectorAll('.js-edit-blob-link-fork-toggler'), + forkButtons: document.querySelectorAll('.js-fork-suggestion-button'), + cancelButtons: document.querySelectorAll('.js-cancel-fork-suggestion-button'), + suggestionSections: document.querySelectorAll('.js-file-fork-suggestion-section'), + actionTextPieces: document.querySelectorAll('.js-file-fork-suggestion-section-action'), + }).init(); +}; diff --git a/app/assets/javascripts/pages/projects/init_form.js b/app/assets/javascripts/pages/projects/init_form.js new file mode 100644 index 00000000000..0b6c5c1d30b --- /dev/null +++ b/app/assets/javascripts/pages/projects/init_form.js @@ -0,0 +1,7 @@ +import ZenMode from '~/zen_mode'; +import GLForm from '~/gl_form'; + +export default function ($formEl) { + new ZenMode(); // eslint-disable-line no-new + new GLForm($formEl, true); // eslint-disable-line no-new +} diff --git a/app/assets/javascripts/pages/projects/issues/edit/index.js b/app/assets/javascripts/pages/projects/issues/edit/index.js new file mode 100644 index 00000000000..7f27f379d8c --- /dev/null +++ b/app/assets/javascripts/pages/projects/issues/edit/index.js @@ -0,0 +1,5 @@ +import initForm from '../form'; + +export default () => { + initForm(); +}; diff --git a/app/assets/javascripts/pages/projects/issues/form.js b/app/assets/javascripts/pages/projects/issues/form.js new file mode 100644 index 00000000000..5c7daf84738 --- /dev/null +++ b/app/assets/javascripts/pages/projects/issues/form.js @@ -0,0 +1,16 @@ +/* eslint-disable no-new */ +import GLForm from '~/gl_form'; +import IssuableForm from '~/issuable_form'; +import LabelsSelect from '~/labels_select'; +import MilestoneSelect from '~/milestone_select'; +import ShortcutsNavigation from '~/shortcuts_navigation'; +import IssuableTemplateSelectors from '~/templates/issuable_template_selectors'; + +export default () => { + new ShortcutsNavigation(); + new GLForm($('.issue-form'), true); + new IssuableForm($('.issue-form')); + new LabelsSelect(); + new MilestoneSelect(); + new IssuableTemplateSelectors(); +}; diff --git a/app/assets/javascripts/pages/projects/issues/index/index.js b/app/assets/javascripts/pages/projects/issues/index/index.js new file mode 100644 index 00000000000..0d3f35f044d --- /dev/null +++ b/app/assets/javascripts/pages/projects/issues/index/index.js @@ -0,0 +1,16 @@ +/* eslint-disable no-new */ + +import IssuableIndex from '~/issuable_index'; +import ShortcutsNavigation from '~/shortcuts_navigation'; +import UsersSelect from '~/users_select'; +import initFilteredSearch from '~/pages/search/init_filtered_search'; +import { FILTERED_SEARCH } from '~/pages/constants'; +import { ISSUABLE_INDEX } from '~/pages/projects/constants'; + +export default () => { + initFilteredSearch(FILTERED_SEARCH.ISSUES); + new IssuableIndex(ISSUABLE_INDEX.ISSUE); + + new ShortcutsNavigation(); + new UsersSelect(); +}; diff --git a/app/assets/javascripts/pages/projects/issues/new/index.js b/app/assets/javascripts/pages/projects/issues/new/index.js new file mode 100644 index 00000000000..7f27f379d8c --- /dev/null +++ b/app/assets/javascripts/pages/projects/issues/new/index.js @@ -0,0 +1,5 @@ +import initForm from '../form'; + +export default () => { + initForm(); +}; diff --git a/app/assets/javascripts/pages/projects/issues/show/index.js b/app/assets/javascripts/pages/projects/issues/show/index.js new file mode 100644 index 00000000000..48ed8fb2243 --- /dev/null +++ b/app/assets/javascripts/pages/projects/issues/show/index.js @@ -0,0 +1,13 @@ + +/* eslint-disable no-new */ +import initIssuableSidebar from '~/init_issuable_sidebar'; +import Issue from '~/issue'; +import ShortcutsIssuable from '~/shortcuts_issuable'; +import ZenMode from '~/zen_mode'; + +export default () => { + new Issue(); + new ShortcutsIssuable(); + new ZenMode(); + initIssuableSidebar(); +}; diff --git a/app/assets/javascripts/pages/projects/labels/edit/index.js b/app/assets/javascripts/pages/projects/labels/edit/index.js new file mode 100644 index 00000000000..72c5e4744ac --- /dev/null +++ b/app/assets/javascripts/pages/projects/labels/edit/index.js @@ -0,0 +1,3 @@ +import Labels from '~/labels'; + +export default () => new Labels(); diff --git a/app/assets/javascripts/pages/projects/labels/index/index.js b/app/assets/javascripts/pages/projects/labels/index/index.js new file mode 100644 index 00000000000..018345fa112 --- /dev/null +++ b/app/assets/javascripts/pages/projects/labels/index/index.js @@ -0,0 +1,3 @@ +import initLabels from '~/init_labels'; + +export default initLabels; diff --git a/app/assets/javascripts/pages/projects/labels/new/index.js b/app/assets/javascripts/pages/projects/labels/new/index.js new file mode 100644 index 00000000000..72c5e4744ac --- /dev/null +++ b/app/assets/javascripts/pages/projects/labels/new/index.js @@ -0,0 +1,3 @@ +import Labels from '~/labels'; + +export default () => new Labels(); diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/diffs/index.js b/app/assets/javascripts/pages/projects/merge_requests/creations/diffs/index.js new file mode 100644 index 00000000000..734d01ae6f2 --- /dev/null +++ b/app/assets/javascripts/pages/projects/merge_requests/creations/diffs/index.js @@ -0,0 +1,3 @@ +import initMergeRequest from '~/pages/projects/merge_requests/init_merge_request'; + +export default initMergeRequest; diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js new file mode 100644 index 00000000000..ccd0b54c5ed --- /dev/null +++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js @@ -0,0 +1,18 @@ +import Compare from '~/compare'; +import MergeRequest from '~/merge_request'; + +export default () => { + const mrNewCompareNode = document.querySelector('.js-merge-request-new-compare'); + if (mrNewCompareNode) { + new Compare({ // eslint-disable-line no-new + targetProjectUrl: mrNewCompareNode.dataset.targetProjectUrl, + sourceBranchUrl: mrNewCompareNode.dataset.sourceBranchUrl, + targetBranchUrl: mrNewCompareNode.dataset.targetBranchUrl, + }); + } else { + const mrNewSubmitNode = document.querySelector('.js-merge-request-new-submit'); + new MergeRequest({ // eslint-disable-line no-new + action: mrNewSubmitNode.dataset.mrSubmitAction, + }); + } +}; diff --git a/app/assets/javascripts/pages/projects/merge_requests/edit/index.js b/app/assets/javascripts/pages/projects/merge_requests/edit/index.js new file mode 100644 index 00000000000..734d01ae6f2 --- /dev/null +++ b/app/assets/javascripts/pages/projects/merge_requests/edit/index.js @@ -0,0 +1,3 @@ +import initMergeRequest from '~/pages/projects/merge_requests/init_merge_request'; + +export default initMergeRequest; diff --git a/app/assets/javascripts/pages/projects/merge_requests/index/index.js b/app/assets/javascripts/pages/projects/merge_requests/index/index.js new file mode 100644 index 00000000000..b386e8fb48d --- /dev/null +++ b/app/assets/javascripts/pages/projects/merge_requests/index/index.js @@ -0,0 +1,13 @@ +import IssuableIndex from '~/issuable_index'; +import ShortcutsNavigation from '~/shortcuts_navigation'; +import UsersSelect from '~/users_select'; +import initFilteredSearch from '~/pages/search/init_filtered_search'; +import { FILTERED_SEARCH } from '~/pages/constants'; +import { ISSUABLE_INDEX } from '~/pages/projects/constants'; + +export default () => { + initFilteredSearch(FILTERED_SEARCH.MERGE_REQUESTS); + new IssuableIndex(ISSUABLE_INDEX.MERGE_REQUEST); // eslint-disable-line no-new + new ShortcutsNavigation(); // eslint-disable-line no-new + new UsersSelect(); // eslint-disable-line no-new +}; 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 new file mode 100644 index 00000000000..8bfac606aab --- /dev/null +++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js @@ -0,0 +1,19 @@ +/* eslint-disable no-new */ + +import Diff from '~/diff'; +import ShortcutsNavigation from '~/shortcuts_navigation'; +import GLForm from '~/gl_form'; +import IssuableForm from '~/issuable_form'; +import LabelsSelect from '~/labels_select'; +import MilestoneSelect from '~/milestone_select'; +import IssuableTemplateSelectors from '~/templates/issuable_template_selectors'; + +export default () => { + new Diff(); + new ShortcutsNavigation(); + new GLForm($('.merge-request-form'), true); + new IssuableForm($('.merge-request-form')); + new LabelsSelect(); + new MilestoneSelect(); + new IssuableTemplateSelectors(); +}; diff --git a/app/assets/javascripts/pages/projects/milestones/edit/index.js b/app/assets/javascripts/pages/projects/milestones/edit/index.js new file mode 100644 index 00000000000..10e3979a36e --- /dev/null +++ b/app/assets/javascripts/pages/projects/milestones/edit/index.js @@ -0,0 +1,3 @@ +import initForm from '../../../../shared/milestones/form'; + +export default () => initForm(); diff --git a/app/assets/javascripts/pages/projects/milestones/index/index.js b/app/assets/javascripts/pages/projects/milestones/index/index.js new file mode 100644 index 00000000000..8fb4d83d8a3 --- /dev/null +++ b/app/assets/javascripts/pages/projects/milestones/index/index.js @@ -0,0 +1,3 @@ +import milestones from '~/pages/milestones/shared'; + +export default milestones; diff --git a/app/assets/javascripts/pages/projects/milestones/new/index.js b/app/assets/javascripts/pages/projects/milestones/new/index.js new file mode 100644 index 00000000000..10e3979a36e --- /dev/null +++ b/app/assets/javascripts/pages/projects/milestones/new/index.js @@ -0,0 +1,3 @@ +import initForm from '../../../../shared/milestones/form'; + +export default () => initForm(); diff --git a/app/assets/javascripts/pages/projects/milestones/show/index.js b/app/assets/javascripts/pages/projects/milestones/show/index.js new file mode 100644 index 00000000000..35b5c9c2ced --- /dev/null +++ b/app/assets/javascripts/pages/projects/milestones/show/index.js @@ -0,0 +1,7 @@ +import initMilestonesShow from '~/pages/milestones/shared/init_milestones_show'; +import milestones from '~/pages/milestones/shared'; + +export default () => { + initMilestonesShow(); + milestones(); +}; diff --git a/app/assets/javascripts/pages/projects/new/index.js b/app/assets/javascripts/pages/projects/new/index.js new file mode 100644 index 00000000000..71c49deb9d0 --- /dev/null +++ b/app/assets/javascripts/pages/projects/new/index.js @@ -0,0 +1,9 @@ +import ProjectNew from '../shared/project_new'; +import initProjectVisibilitySelector from '../../../project_visibility'; +import initProjectNew from '../../../projects/project_new'; + +export default () => { + new ProjectNew(); // eslint-disable-line no-new + initProjectVisibilitySelector(); + initProjectNew.bindEvents(); +}; diff --git a/app/assets/javascripts/pages/projects/pipelines/builds/index.js b/app/assets/javascripts/pages/projects/pipelines/builds/index.js new file mode 100644 index 00000000000..060a78b427e --- /dev/null +++ b/app/assets/javascripts/pages/projects/pipelines/builds/index.js @@ -0,0 +1,16 @@ +import Pipelines from '../../../../pipelines'; + +export default () => { + const { controllerAction } = document.querySelector('.js-pipeline-container').dataset; + const pipelineStatusUrl = `${document.querySelector('.js-pipeline-tab-link a').getAttribute('href')}/status.json`; + + new Pipelines({ // eslint-disable-line no-new + initTabs: true, + pipelineStatusUrl, + tabsOptions: { + action: controllerAction, + defaultAction: 'pipelines', + parentEl: '.pipelines-tabs', + }, + }); +}; diff --git a/app/assets/javascripts/pages/projects/pipelines/new/index.js b/app/assets/javascripts/pages/projects/pipelines/new/index.js new file mode 100644 index 00000000000..c54cc62bf05 --- /dev/null +++ b/app/assets/javascripts/pages/projects/pipelines/new/index.js @@ -0,0 +1,5 @@ +import NewBranchForm from '../../../../new_branch_form'; + +export default () => { + new NewBranchForm($('.js-new-pipeline-form')); // eslint-disable-line no-new +}; diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/pages/projects/project.js index d4f26b81f30..e30d558726b 100644 --- a/app/assets/javascripts/project.js +++ b/app/assets/javascripts/pages/projects/project.js @@ -1,8 +1,8 @@ /* 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 Cookies from 'js-cookie'; -import { visitUrl } from './lib/utils/url_utility'; -import projectSelect from './project_select'; +import { visitUrl } from '../../lib/utils/url_utility'; +import projectSelect from '../../project_select'; export default class Project { constructor() { diff --git a/app/assets/javascripts/pages/projects/project_members/index.js b/app/assets/javascripts/pages/projects/project_members/index.js new file mode 100644 index 00000000000..f4643e7dba0 --- /dev/null +++ b/app/assets/javascripts/pages/projects/project_members/index.js @@ -0,0 +1,12 @@ +import memberExpirationDate from '../../../member_expiration_date'; +import UsersSelect from '../../../users_select'; +import groupsSelect from '../../../groups_select'; +import Members from '../../../members'; + +export default () => { + memberExpirationDate('.js-access-expiration-date-groups'); + groupsSelect(); + memberExpirationDate(); + new Members(); // eslint-disable-line no-new + new UsersSelect(); // eslint-disable-line no-new +}; diff --git a/app/assets/javascripts/pages/projects/releases/edit/index.js b/app/assets/javascripts/pages/projects/releases/edit/index.js new file mode 100644 index 00000000000..3d997cdfff0 --- /dev/null +++ b/app/assets/javascripts/pages/projects/releases/edit/index.js @@ -0,0 +1,3 @@ +import initForm from '~/pages/projects/init_form'; + +export default initForm($('.release-form')); diff --git a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js new file mode 100644 index 00000000000..18dc1dc03a5 --- /dev/null +++ b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js @@ -0,0 +1,22 @@ +import initSettingsPanels from '~/settings_panels'; +import SecretValues from '~/behaviors/secret_values'; + +export default function () { + // Initialize expandable settings panels + initSettingsPanels(); + const runnerToken = document.querySelector('.js-secret-runner-token'); + if (runnerToken) { + const runnerTokenSecretValue = new SecretValues({ + container: runnerToken, + }); + runnerTokenSecretValue.init(); + } + + const secretVariableTable = document.querySelector('.js-secret-variable-table'); + if (secretVariableTable) { + const secretVariableTableValues = new SecretValues({ + container: secretVariableTable, + }); + secretVariableTableValues.init(); + } +} diff --git a/app/assets/javascripts/pages/projects/settings/repository/show/index.js b/app/assets/javascripts/pages/projects/settings/repository/show/index.js new file mode 100644 index 00000000000..83b5467fbc0 --- /dev/null +++ b/app/assets/javascripts/pages/projects/settings/repository/show/index.js @@ -0,0 +1,3 @@ +import initSettingsPanels from '~/settings_panels'; + +export default initSettingsPanels; diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue new file mode 100644 index 00000000000..9b13b2a524f --- /dev/null +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue @@ -0,0 +1,111 @@ +<script> + import projectFeatureToggle from '../../../../../vue_shared/components/toggle_button.vue'; + + export default { + components: { + projectFeatureToggle, + }, + + model: { + prop: 'value', + event: 'change', + }, + + props: { + name: { + type: String, + required: false, + default: '', + }, + options: { + type: Array, + required: false, + default: () => [], + }, + value: { + type: Number, + required: false, + default: 0, + }, + disabledInput: { + type: Boolean, + required: false, + default: false, + }, + }, + + computed: { + featureEnabled() { + return this.value !== 0; + }, + + displayOptions() { + if (this.featureEnabled) { + return this.options; + } + return [ + [0, 'Enable feature to choose access level'], + ]; + }, + + displaySelectInput() { + return this.disabledInput || !this.featureEnabled || this.displayOptions.length < 2; + }, + }, + + methods: { + toggleFeature(featureEnabled) { + if (featureEnabled === false || this.options.length < 1) { + this.$emit('change', 0); + } else { + const [firstOptionValue] = this.options[this.options.length - 1]; + this.$emit('change', firstOptionValue); + } + }, + + selectOption(e) { + this.$emit('change', Number(e.target.value)); + }, + }, + }; +</script> + +<template> + <div + class="project-feature-controls" + :data-for="name" + > + <input + v-if="name" + type="hidden" + :name="name" + :value="value" + /> + <project-feature-toggle + :value="featureEnabled" + @change="toggleFeature" + :disabled-input="disabledInput" + /> + <div class="select-wrapper"> + <select + class="form-control project-repo-select select-control" + @change="selectOption" + :disabled="displaySelectInput" + > + <option + v-for="[optionValue, optionName] in displayOptions" + :key="optionValue" + :value="optionValue" + :selected="optionValue === value" + > + {{ optionName }} + </option> + </select> + <i + aria-hidden="true" + class="fa fa-chevron-down" + > + </i> + </div> + </div> +</template> diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue new file mode 100644 index 00000000000..25a88f846eb --- /dev/null +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue @@ -0,0 +1,51 @@ +<script> + export default { + props: { + label: { + type: String, + required: false, + default: null, + }, + helpPath: { + type: String, + required: false, + default: null, + }, + helpText: { + type: String, + required: false, + default: null, + }, + }, + }; +</script> + +<template> + <div class="project-feature-row"> + <label + v-if="label" + class="label-light" + > + {{ label }} + <a + v-if="helpPath" + :href="helpPath" + target="_blank" + > + <i + aria-hidden="true" + data-hidden="true" + class="fa fa-question-circle" + > + </i> + </a> + </label> + <span + v-if="helpText" + class="help-block" + > + {{ helpText }} + </span> + <slot></slot> + </div> +</template> diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue new file mode 100644 index 00000000000..755a34b7348 --- /dev/null +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue @@ -0,0 +1,328 @@ +<script> + import projectFeatureSetting from './project_feature_setting.vue'; + import projectFeatureToggle from '../../../../../vue_shared/components/toggle_button.vue'; + import projectSettingRow from './project_setting_row.vue'; + import { visibilityOptions, visibilityLevelDescriptions } from '../constants'; + import { toggleHiddenClassBySelector } from '../external'; + + export default { + components: { + projectFeatureSetting, + projectFeatureToggle, + projectSettingRow, + }, + + props: { + currentSettings: { + type: Object, + required: true, + }, + canChangeVisibilityLevel: { + type: Boolean, + required: false, + default: false, + }, + allowedVisibilityOptions: { + type: Array, + required: false, + default: () => [0, 10, 20], + }, + lfsAvailable: { + type: Boolean, + required: false, + default: false, + }, + registryAvailable: { + type: Boolean, + required: false, + default: false, + }, + visibilityHelpPath: { + type: String, + required: false, + default: '', + }, + lfsHelpPath: { + type: String, + required: false, + default: '', + }, + registryHelpPath: { + type: String, + required: false, + default: '', + }, + }, + + data() { + const defaults = { + visibilityOptions, + visibilityLevel: visibilityOptions.PUBLIC, + issuesAccessLevel: 20, + repositoryAccessLevel: 20, + mergeRequestsAccessLevel: 20, + buildsAccessLevel: 20, + wikiAccessLevel: 20, + snippetsAccessLevel: 20, + containerRegistryEnabled: true, + lfsEnabled: true, + requestAccessEnabled: true, + highlightChangesClass: false, + }; + + return { ...defaults, ...this.currentSettings }; + }, + + computed: { + featureAccessLevelOptions() { + const options = [ + [10, 'Only Project Members'], + ]; + if (this.visibilityLevel !== visibilityOptions.PRIVATE) { + options.push([20, 'Everyone With Access']); + } + return options; + }, + + repoFeatureAccessLevelOptions() { + return this.featureAccessLevelOptions.filter( + ([value]) => value <= this.repositoryAccessLevel, + ); + }, + + repositoryEnabled() { + return this.repositoryAccessLevel > 0; + }, + + visibilityLevelDescription() { + return visibilityLevelDescriptions[this.visibilityLevel]; + }, + }, + + watch: { + visibilityLevel(value, oldValue) { + if (value === visibilityOptions.PRIVATE) { + // when private, features are restricted to "only team members" + this.issuesAccessLevel = Math.min(10, this.issuesAccessLevel); + this.repositoryAccessLevel = Math.min(10, this.repositoryAccessLevel); + this.mergeRequestsAccessLevel = Math.min(10, this.mergeRequestsAccessLevel); + this.buildsAccessLevel = Math.min(10, this.buildsAccessLevel); + this.wikiAccessLevel = Math.min(10, this.wikiAccessLevel); + this.snippetsAccessLevel = Math.min(10, this.snippetsAccessLevel); + this.highlightChanges(); + } else if (oldValue === visibilityOptions.PRIVATE) { + // if changing away from private, make enabled features more permissive + if (this.issuesAccessLevel > 0) this.issuesAccessLevel = 20; + if (this.repositoryAccessLevel > 0) this.repositoryAccessLevel = 20; + if (this.mergeRequestsAccessLevel > 0) this.mergeRequestsAccessLevel = 20; + if (this.buildsAccessLevel > 0) this.buildsAccessLevel = 20; + if (this.wikiAccessLevel > 0) this.wikiAccessLevel = 20; + if (this.snippetsAccessLevel > 0) this.snippetsAccessLevel = 20; + this.highlightChanges(); + } + }, + + repositoryAccessLevel(value, oldValue) { + if (value < oldValue) { + // sub-features cannot have more premissive access level + this.mergeRequestsAccessLevel = Math.min(this.mergeRequestsAccessLevel, value); + this.buildsAccessLevel = Math.min(this.buildsAccessLevel, value); + + if (value === 0) { + this.containerRegistryEnabled = false; + this.lfsEnabled = false; + } + } else if (oldValue === 0) { + this.mergeRequestsAccessLevel = value; + this.buildsAccessLevel = value; + this.containerRegistryEnabled = true; + this.lfsEnabled = true; + } + }, + + issuesAccessLevel(value, oldValue) { + if (value === 0) toggleHiddenClassBySelector('.issues-feature', true); + else if (oldValue === 0) toggleHiddenClassBySelector('.issues-feature', false); + }, + + mergeRequestsAccessLevel(value, oldValue) { + if (value === 0) toggleHiddenClassBySelector('.merge-requests-feature', true); + else if (oldValue === 0) toggleHiddenClassBySelector('.merge-requests-feature', false); + }, + + buildsAccessLevel(value, oldValue) { + if (value === 0) toggleHiddenClassBySelector('.builds-feature', true); + else if (oldValue === 0) toggleHiddenClassBySelector('.builds-feature', false); + }, + }, + + methods: { + highlightChanges() { + this.highlightChangesClass = true; + this.$nextTick(() => { + this.highlightChangesClass = false; + }); + }, + + visibilityAllowed(option) { + return this.allowedVisibilityOptions.includes(option); + }, + }, + }; +</script> + +<template> + <div> + <div class="project-visibility-setting"> + <project-setting-row + label="Project visibility" + :help-path="visibilityHelpPath" + > + <div class="project-feature-controls"> + <div class="select-wrapper"> + <select + name="project[visibility_level]" + v-model="visibilityLevel" + class="form-control select-control" + :disabled="!canChangeVisibilityLevel" + > + <option + :value="visibilityOptions.PRIVATE" + :disabled="!visibilityAllowed(visibilityOptions.PRIVATE)" + > + Private + </option> + <option + :value="visibilityOptions.INTERNAL" + :disabled="!visibilityAllowed(visibilityOptions.INTERNAL)" + > + Internal + </option> + <option + :value="visibilityOptions.PUBLIC" + :disabled="!visibilityAllowed(visibilityOptions.PUBLIC)" + > + Public + </option> + </select> + <i + aria-hidden="true" + data-hidden="true" + class="fa fa-chevron-down" + > + </i> + </div> + </div> + <span class="help-block">{{ visibilityLevelDescription }}</span> + <label + v-if="visibilityLevel !== visibilityOptions.PRIVATE" + class="request-access" + > + <input + type="hidden" + name="project[request_access_enabled]" + :value="requestAccessEnabled" + /> + <input + type="checkbox" + v-model="requestAccessEnabled" + /> + Allow users to request access + </label> + </project-setting-row> + </div> + <div + class="project-feature-settings" + :class="{ 'highlight-changes': highlightChangesClass }" + > + <project-setting-row + label="Issues" + help-text="Lightweight issue tracking system for this project" + > + <project-feature-setting + name="project[project_feature_attributes][issues_access_level]" + :options="featureAccessLevelOptions" + v-model="issuesAccessLevel" + /> + </project-setting-row> + <project-setting-row + label="Repository" + help-text="View and edit files in this project" + > + <project-feature-setting + name="project[project_feature_attributes][repository_access_level]" + :options="featureAccessLevelOptions" + v-model="repositoryAccessLevel" + /> + </project-setting-row> + <div class="project-feature-setting-group"> + <project-setting-row + label="Merge requests" + help-text="Submit changes to be merged upstream" + > + <project-feature-setting + name="project[project_feature_attributes][merge_requests_access_level]" + :options="repoFeatureAccessLevelOptions" + v-model="mergeRequestsAccessLevel" + :disabled-input="!repositoryEnabled" + /> + </project-setting-row> + <project-setting-row + label="Pipelines" + help-text="Build, test, and deploy your changes" + > + <project-feature-setting + name="project[project_feature_attributes][builds_access_level]" + :options="repoFeatureAccessLevelOptions" + v-model="buildsAccessLevel" + :disabled-input="!repositoryEnabled" + /> + </project-setting-row> + <project-setting-row + v-if="registryAvailable" + label="Container registry" + :help-path="registryHelpPath" + help-text="Every project can have its own space to store its Docker images" + > + <project-feature-toggle + name="project[container_registry_enabled]" + v-model="containerRegistryEnabled" + :disabled-input="!repositoryEnabled" + /> + </project-setting-row> + <project-setting-row + v-if="lfsAvailable" + label="Git Large File Storage" + :help-path="lfsHelpPath" + help-text="Manages large files such as audio, video, and graphics files" + > + <project-feature-toggle + name="project[lfs_enabled]" + v-model="lfsEnabled" + :disabled-input="!repositoryEnabled" + /> + </project-setting-row> + </div> + <project-setting-row + label="Wiki" + help-text="Pages for project documentation" + > + <project-feature-setting + name="project[project_feature_attributes][wiki_access_level]" + :options="featureAccessLevelOptions" + v-model="wikiAccessLevel" + /> + </project-setting-row> + <project-setting-row + label="Snippets" + help-text="Share code pastes with others out of Git repository" + > + <project-feature-setting + name="project[project_feature_attributes][snippets_access_level]" + :options="featureAccessLevelOptions" + v-model="snippetsAccessLevel" + /> + </project-setting-row> + </div> + </div> +</template> diff --git a/app/assets/javascripts/projects/permissions/constants.js b/app/assets/javascripts/pages/projects/shared/permissions/constants.js index ce47562f259..ce47562f259 100644 --- a/app/assets/javascripts/projects/permissions/constants.js +++ b/app/assets/javascripts/pages/projects/shared/permissions/constants.js diff --git a/app/assets/javascripts/projects/permissions/external.js b/app/assets/javascripts/pages/projects/shared/permissions/external.js index 460af4a2111..460af4a2111 100644 --- a/app/assets/javascripts/projects/permissions/external.js +++ b/app/assets/javascripts/pages/projects/shared/permissions/external.js diff --git a/app/assets/javascripts/projects/permissions/index.js b/app/assets/javascripts/pages/projects/shared/permissions/index.js index dbde8dda634..dbde8dda634 100644 --- a/app/assets/javascripts/projects/permissions/index.js +++ b/app/assets/javascripts/pages/projects/shared/permissions/index.js diff --git a/app/assets/javascripts/project_avatar.js b/app/assets/javascripts/pages/projects/shared/project_avatar.js index 56627aa155c..56627aa155c 100644 --- a/app/assets/javascripts/project_avatar.js +++ b/app/assets/javascripts/pages/projects/shared/project_avatar.js diff --git a/app/assets/javascripts/project_new.js b/app/assets/javascripts/pages/projects/shared/project_new.js index ca548d011b6..86faba0b910 100644 --- a/app/assets/javascripts/project_new.js +++ b/app/assets/javascripts/pages/projects/shared/project_new.js @@ -1,6 +1,6 @@ /* eslint-disable func-names, no-var, no-underscore-dangle, prefer-template, prefer-arrow-callback*/ -import VisibilitySelect from './visibility_select'; +import VisibilitySelect from '../../../visibility_select'; function highlightChanges($elm) { $elm.addClass('highlight-changes'); diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js new file mode 100644 index 00000000000..55154cdddcb --- /dev/null +++ b/app/assets/javascripts/pages/projects/show/index.js @@ -0,0 +1,27 @@ +import ShortcutsNavigation from '~/shortcuts_navigation'; +import NotificationsForm from '~/notifications_form'; +import UserCallout from '~/user_callout'; +import TreeView from '~/tree'; +import BlobViewer from '~/blob/viewer/index'; +import Activities from '~/activities'; +import { ajaxGet } from '~/lib/utils/common_utils'; +import Star from '../../../star'; +import notificationsDropdown from '../../../notifications_dropdown'; + +export default () => { + new Star(); // eslint-disable-line no-new + notificationsDropdown(); + new ShortcutsNavigation(); // eslint-disable-line no-new + new NotificationsForm(); // eslint-disable-line no-new + new UserCallout({ // eslint-disable-line no-new + setCalloutPerProject: true, + 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(() => { + 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 new file mode 100644 index 00000000000..9edb16dc73b --- /dev/null +++ b/app/assets/javascripts/pages/projects/snippets/edit/index.js @@ -0,0 +1,3 @@ +import initForm from '~/pages/projects/init_form'; + +export default initForm($('.snippet-form')); diff --git a/app/assets/javascripts/pages/projects/snippets/new/index.js b/app/assets/javascripts/pages/projects/snippets/new/index.js new file mode 100644 index 00000000000..9edb16dc73b --- /dev/null +++ b/app/assets/javascripts/pages/projects/snippets/new/index.js @@ -0,0 +1,3 @@ +import initForm from '~/pages/projects/init_form'; + +export default initForm($('.snippet-form')); diff --git a/app/assets/javascripts/pages/projects/snippets/show/index.js b/app/assets/javascripts/pages/projects/snippets/show/index.js new file mode 100644 index 00000000000..a3cf75c385b --- /dev/null +++ b/app/assets/javascripts/pages/projects/snippets/show/index.js @@ -0,0 +1,11 @@ +import initNotes from '~/init_notes'; +import ZenMode from '~/zen_mode'; +import LineHighlighter from '../../../../line_highlighter'; +import BlobViewer from '../../../../blob/viewer'; + +export default function () { + new LineHighlighter(); // eslint-disable-line no-new + new BlobViewer(); // eslint-disable-line no-new + initNotes(); + new ZenMode(); // eslint-disable-line no-new +} diff --git a/app/assets/javascripts/pages/projects/tags/new/index.js b/app/assets/javascripts/pages/projects/tags/new/index.js new file mode 100644 index 00000000000..dacc2875c8c --- /dev/null +++ b/app/assets/javascripts/pages/projects/tags/new/index.js @@ -0,0 +1,9 @@ +import RefSelectDropdown from '../../../../ref_select_dropdown'; +import ZenMode from '../../../../zen_mode'; +import GLForm from '../../../../gl_form'; + +export default () => { + new ZenMode(); // eslint-disable-line no-new + new GLForm($('.tag-form'), true); // eslint-disable-line no-new + new RefSelectDropdown($('.js-branch-select')); // eslint-disable-line no-new +}; diff --git a/app/assets/javascripts/pages/projects/tree/show/index.js b/app/assets/javascripts/pages/projects/tree/show/index.js new file mode 100644 index 00000000000..c4b3356e478 --- /dev/null +++ b/app/assets/javascripts/pages/projects/tree/show/index.js @@ -0,0 +1,37 @@ +import Vue from 'vue'; +import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue'; +import TreeView from '../../../../tree'; +import ShortcutsNavigation from '../../../../shortcuts_navigation'; +import BlobViewer from '../../../../blob/viewer'; +import NewCommitForm from '../../../../new_commit_form'; +import { ajaxGet } from '../../../../lib/utils/common_utils'; + +export default () => { + new ShortcutsNavigation(); // eslint-disable-line no-new + new TreeView(); // eslint-disable-line no-new + new BlobViewer(); // eslint-disable-line no-new + new NewCommitForm($('.js-create-dir-form')); // eslint-disable-line no-new + $('#tree-slider').waitForImages(() => + ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath)); + + const commitPipelineStatusEl = document.getElementById('commit-pipeline-status'); + const statusLink = document.querySelector('.commit-actions .ci-status-link'); + if (statusLink != null) { + 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/wikis/index.js b/app/assets/javascripts/pages/projects/wikis/index.js new file mode 100644 index 00000000000..eb14c7a0e78 --- /dev/null +++ b/app/assets/javascripts/pages/projects/wikis/index.js @@ -0,0 +1,11 @@ +import Wikis from './wikis'; +import ShortcutsWiki from '../../../shortcuts_wiki'; +import ZenMode from '../../../zen_mode'; +import GLForm from '../../../gl_form'; + +export default () => { + new Wikis(); // eslint-disable-line no-new + new ShortcutsWiki(); // eslint-disable-line no-new + new ZenMode(); // eslint-disable-line no-new + new GLForm($('.wiki-form'), true); // eslint-disable-line no-new +}; diff --git a/app/assets/javascripts/wikis.js b/app/assets/javascripts/pages/projects/wikis/wikis.js index 7a865587444..34a12ef76a1 100644 --- a/app/assets/javascripts/wikis.js +++ b/app/assets/javascripts/pages/projects/wikis/wikis.js @@ -1,5 +1,5 @@ -import bp from './breakpoints'; -import { slugify } from './lib/utils/text_utility'; +import bp from '../../../breakpoints'; +import { slugify } from '../../../lib/utils/text_utility'; export default class Wikis { constructor() { diff --git a/app/assets/javascripts/pages/search/init_filtered_search.js b/app/assets/javascripts/pages/search/init_filtered_search.js new file mode 100644 index 00000000000..44853636aea --- /dev/null +++ b/app/assets/javascripts/pages/search/init_filtered_search.js @@ -0,0 +1,7 @@ +export default (page) => { + const filteredSearchEnabled = gl.FilteredSearchManager && document.querySelector('.filtered-search'); + if (filteredSearchEnabled) { + const filteredSearchManager = new gl.FilteredSearchManager(page); + filteredSearchManager.setup(); + } +}; diff --git a/app/assets/javascripts/pages/search/show/index.js b/app/assets/javascripts/pages/search/show/index.js new file mode 100644 index 00000000000..4264c5c9dbe --- /dev/null +++ b/app/assets/javascripts/pages/search/show/index.js @@ -0,0 +1,3 @@ +import Search from './search'; + +export default () => new Search(); diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/pages/search/show/search.js index 363322af47a..dc621bc87c0 100644 --- a/app/assets/javascripts/search.js +++ b/app/assets/javascripts/pages/search/show/search.js @@ -1,5 +1,5 @@ -import Flash from './flash'; -import Api from './api'; +import Flash from '~/flash'; +import Api from '~/api'; export default class Search { constructor() { @@ -15,6 +15,7 @@ export default class Search { $groupDropdown.glDropdown({ selectable: true, filterable: true, + filterRemote: true, fieldName: 'group_id', search: { fields: ['full_name'], @@ -43,6 +44,7 @@ export default class Search { $projectDropdown.glDropdown({ selectable: true, filterable: true, + filterRemote: true, fieldName: 'project_id', search: { fields: ['name'], diff --git a/app/assets/javascripts/pages/sessions/index.js b/app/assets/javascripts/pages/sessions/index.js new file mode 100644 index 00000000000..54f4e56359a --- /dev/null +++ b/app/assets/javascripts/pages/sessions/index.js @@ -0,0 +1,5 @@ +import initU2F from '../../shared/sessions/u2f'; + +export default () => { + initU2F(); +}; diff --git a/app/assets/javascripts/pages/sessions/new/index.js b/app/assets/javascripts/pages/sessions/new/index.js new file mode 100644 index 00000000000..f163557babc --- /dev/null +++ b/app/assets/javascripts/pages/sessions/new/index.js @@ -0,0 +1,11 @@ +import UsernameValidator from './username_validator'; +import SigninTabsMemoizer from './signin_tabs_memoizer'; +import OAuthRememberMe from './oauth_remember_me'; + +export default () => { + new UsernameValidator(); // eslint-disable-line no-new + new SigninTabsMemoizer(); // eslint-disable-line no-new + new OAuthRememberMe({ // eslint-disable-line no-new + container: $('.omniauth-container'), + }).bindEvents(); +}; diff --git a/app/assets/javascripts/oauth_remember_me.js b/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js index ffc2dd6bbca..ffc2dd6bbca 100644 --- a/app/assets/javascripts/oauth_remember_me.js +++ b/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js diff --git a/app/assets/javascripts/signin_tabs_memoizer.js b/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js index 20255398047..08f0afdcce3 100644 --- a/app/assets/javascripts/signin_tabs_memoizer.js +++ b/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js @@ -1,6 +1,4 @@ -/* eslint no-param-reassign: ["error", { "props": false }]*/ -/* eslint no-new: "off" */ -import AccessorUtilities from './lib/utils/accessor'; +import AccessorUtilities from '~/lib/utils/accessor'; /** * Memorize the last selected tab after reloading a page. @@ -11,6 +9,10 @@ export default class SigninTabsMemoizer { this.currentTabKey = currentTabKey; this.tabSelector = tabSelector; this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe(); + // sets selected tab if given as hash tag + if (window.location.hash) { + this.saveData(window.location.hash); + } this.bootstrap(); } diff --git a/app/assets/javascripts/username_validator.js b/app/assets/javascripts/pages/sessions/new/username_validator.js index bb34d5d2008..bb34d5d2008 100644 --- a/app/assets/javascripts/username_validator.js +++ b/app/assets/javascripts/pages/sessions/new/username_validator.js diff --git a/app/assets/javascripts/pages/snippets/edit/index.js b/app/assets/javascripts/pages/snippets/edit/index.js new file mode 100644 index 00000000000..9c664b5f1ff --- /dev/null +++ b/app/assets/javascripts/pages/snippets/edit/index.js @@ -0,0 +1,3 @@ +import form from '../form'; + +export default form; diff --git a/app/assets/javascripts/pages/snippets/form.js b/app/assets/javascripts/pages/snippets/form.js new file mode 100644 index 00000000000..f996d3cd74e --- /dev/null +++ b/app/assets/javascripts/pages/snippets/form.js @@ -0,0 +1,7 @@ +import GLForm from '~/gl_form'; +import ZenMode from '~/zen_mode'; + +export default () => { + new GLForm($('.snippet-form'), false); // eslint-disable-line no-new + new ZenMode(); // eslint-disable-line no-new +}; diff --git a/app/assets/javascripts/pages/snippets/new/index.js b/app/assets/javascripts/pages/snippets/new/index.js new file mode 100644 index 00000000000..9c664b5f1ff --- /dev/null +++ b/app/assets/javascripts/pages/snippets/new/index.js @@ -0,0 +1,3 @@ +import form from '../form'; + +export default form; diff --git a/app/assets/javascripts/pages/snippets/show/index.js b/app/assets/javascripts/pages/snippets/show/index.js new file mode 100644 index 00000000000..04c9562bfbb --- /dev/null +++ b/app/assets/javascripts/pages/snippets/show/index.js @@ -0,0 +1,12 @@ +/* eslint-disable no-new */ +import LineHighlighter from '../../../line_highlighter'; +import BlobViewer from '../../../blob/viewer'; +import ZenMode from '../../../zen_mode'; +import initNotes from '../../../init_notes'; + +export default () => { + new LineHighlighter(); + new BlobViewer(); + initNotes(); + new ZenMode(); +}; diff --git a/app/assets/javascripts/pdf/index.vue b/app/assets/javascripts/pdf/index.vue index c8a2f778ee8..00f32d9de78 100644 --- a/app/assets/javascripts/pdf/index.vue +++ b/app/assets/javascripts/pdf/index.vue @@ -5,6 +5,7 @@ import page from './page/index.vue'; export default { + components: { page }, props: { pdf: { type: [String, Uint8Array], @@ -17,8 +18,6 @@ pages: [], }; }, - components: { page }, - watch: { pdf: 'load' }, computed: { document() { return typeof this.pdf === 'string' ? this.pdf : { data: this.pdf }; @@ -27,6 +26,11 @@ return this.pdf && this.pdf.length > 0; }, }, + watch: { pdf: 'load' }, + mounted() { + pdfjsLib.PDFJS.workerSrc = workerSrc; + if (this.hasPDF) this.load(); + }, methods: { load() { this.pages = []; @@ -47,20 +51,20 @@ return Promise.all(pagePromises); }, }, - mounted() { - pdfjsLib.PDFJS.workerSrc = workerSrc; - if (this.hasPDF) this.load(); - }, }; </script> <template> - <div class="pdf-viewer" v-if="hasPDF"> - <page v-for="(page, index) in pages" + <div + class="pdf-viewer" + v-if="hasPDF"> + <page + v-for="(page, index) in pages" :key="index" :v-if="!loading" :page="page" - :number="index + 1" /> + :number="index + 1" + /> </div> </template> diff --git a/app/assets/javascripts/pdf/page/index.vue b/app/assets/javascripts/pdf/page/index.vue index be38f7cc129..fcba819beba 100644 --- a/app/assets/javascripts/pdf/page/index.vue +++ b/app/assets/javascripts/pdf/page/index.vue @@ -45,24 +45,26 @@ <canvas class="pdf-page" ref="canvas" - :data-page="number" /> + :data-page="number" + > + </canvas> </template> <style> -.pdf-page { - margin: 8px auto 0 auto; - border-top: 1px #ddd solid; - border-bottom: 1px #ddd solid; - width: 100%; -} + .pdf-page { + margin: 8px auto 0 auto; + border-top: 1px #ddd solid; + border-bottom: 1px #ddd solid; + width: 100%; + } -.pdf-page:first-child { - margin-top: 0px; - border-top: 0px; -} + .pdf-page:first-child { + margin-top: 0px; + border-top: 0px; + } -.pdf-page:last-child { - margin-bottom: 0px; - border-bottom: 0px; -} + .pdf-page:last-child { + margin-bottom: 0px; + border-bottom: 0px; + } </style> diff --git a/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.vue b/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.vue index b5d85299cf8..2d18fa2044b 100644 --- a/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.vue +++ b/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.vue @@ -32,6 +32,20 @@ return !!(this.customInputEnabled || !this.intervalIsPreset); }, }, + watch: { + cronInterval() { + // updates field validation state when model changes, as + // glFieldError only updates on input. + this.$nextTick(() => { + gl.pipelineScheduleFieldErrors.updateFormValidityState(); + }); + }, + }, + created() { + if (this.intervalIsPreset) { + this.enableCustomInput = false; + } + }, methods: { toggleCustomInput(shouldEnable) { this.customInputEnabled = shouldEnable; @@ -43,20 +57,6 @@ } }, }, - created() { - if (this.intervalIsPreset) { - this.enableCustomInput = false; - } - }, - watch: { - cronInterval() { - // updates field validation state when model changes, as - // glFieldError only updates on input. - this.$nextTick(() => { - gl.pipelineScheduleFieldErrors.updateFormValidityState(); - }); - }, - }, }; </script> @@ -78,7 +78,12 @@ </label> <span class="cron-syntax-link-wrap"> - (<a :href="cronSyntaxUrl" target="_blank">{{ __('Cron syntax') }}</a>) + (<a + :href="cronSyntaxUrl" + target="_blank" + > + {{ __('Cron syntax') }} + </a>) </span> </div> @@ -93,7 +98,10 @@ @click="toggleCustomInput(false)" /> - <label class="label-light" for="every-day"> + <label + class="label-light" + for="every-day" + > {{ __('Every day (at 4:00am)') }} </label> </div> @@ -109,7 +117,10 @@ @click="toggleCustomInput(false)" /> - <label class="label-light" for="every-week"> + <label + class="label-light" + for="every-week" + > {{ __('Every week (Sundays at 4:00am)') }} </label> </div> @@ -125,7 +136,10 @@ @click="toggleCustomInput(false)" /> - <label class="label-light" for="every-month"> + <label + class="label-light" + for="every-month" + > {{ __('Every month (on the 1st at 4:00am)') }} </label> </div> diff --git a/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.vue b/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.vue index 6e0bc2d697a..aa04a0ac47a 100644 --- a/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.vue +++ b/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.vue @@ -16,15 +16,15 @@ calloutDismissed: Cookies.get(cookieKey) === 'true', }; }, + created() { + this.illustrationSvg = illustrationSvg; + }, methods: { dismissCallout() { this.calloutDismissed = true; Cookies.set(cookieKey, this.calloutDismissed, { expires: 365 }); }, }, - created() { - this.illustrationSvg = illustrationSvg; - }, }; </script> <template> @@ -41,17 +41,25 @@ class="fa fa-times"> </i> </button> - <div class="svg-container" v-html="illustrationSvg"></div> + <div + class="svg-container" + v-html="illustrationSvg"> + </div> <div class="user-callout-copy"> <h4>{{ __('Scheduling Pipelines') }}</h4> <p> - {{ __('The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user.') }} + {{ __(`The pipelines schedule runs pipelines in the future, +repeatedly, for specific branches or tags. +Those scheduled pipelines will inherit limited project access based on their associated user.`) }} </p> <p> {{ __('Learn more in the') }} <a :href="docsUrl" target="_blank" - rel="nofollow">{{ s__('Learn more in the|pipeline schedules documentation') }}</a>. <!-- oneline to prevent extra space before period --> + rel="nofollow" + > + {{ s__('Learn more in the|pipeline schedules documentation') }}</a>. + <!-- oneline to prevent extra space before period --> </p> </div> </div> diff --git a/app/assets/javascripts/pipelines/components/async_button.vue b/app/assets/javascripts/pipelines/components/async_button.vue index 16cc0761fc1..77553ca67cc 100644 --- a/app/assets/javascripts/pipelines/components/async_button.vue +++ b/app/assets/javascripts/pipelines/components/async_button.vue @@ -1,67 +1,67 @@ <script> -/* eslint-disable no-new, no-alert */ + /* eslint-disable no-alert */ -import eventHub from '../event_hub'; -import loadingIcon from '../../vue_shared/components/loading_icon.vue'; -import tooltip from '../../vue_shared/directives/tooltip'; + import eventHub from '../event_hub'; + import loadingIcon from '../../vue_shared/components/loading_icon.vue'; + import icon from '../../vue_shared/components/icon.vue'; + import tooltip from '../../vue_shared/directives/tooltip'; -export default { - props: { - endpoint: { - type: String, - required: true, + export default { + directives: { + tooltip, }, - title: { - type: String, - required: true, + components: { + loadingIcon, + icon, }, - icon: { - type: String, - required: true, + props: { + endpoint: { + type: String, + required: true, + }, + title: { + type: String, + required: true, + }, + icon: { + type: String, + required: true, + }, + cssClass: { + type: String, + required: true, + }, + confirmActionMessage: { + type: String, + required: false, + default: '', + }, }, - cssClass: { - type: String, - required: true, + data() { + return { + isLoading: false, + }; }, - confirmActionMessage: { - type: String, - required: false, + computed: { + buttonClass() { + return `btn ${this.cssClass}`; + }, }, - }, - directives: { - tooltip, - }, - components: { - loadingIcon, - }, - data() { - return { - isLoading: false, - }; - }, - computed: { - iconClass() { - return `fa fa-${this.icon}`; - }, - buttonClass() { - return `btn ${this.cssClass}`; - }, - }, - methods: { - onClick() { - if (this.confirmActionMessage && confirm(this.confirmActionMessage)) { - this.makeRequest(); - } else if (!this.confirmActionMessage) { - this.makeRequest(); - } - }, - makeRequest() { - this.isLoading = true; + methods: { + onClick() { + if (this.confirmActionMessage !== '' && confirm(this.confirmActionMessage)) { + this.makeRequest(); + } else if (this.confirmActionMessage === '') { + this.makeRequest(); + } + }, + makeRequest() { + this.isLoading = true; - eventHub.$emit('postAction', this.endpoint); + eventHub.$emit('postAction', this.endpoint); + }, }, - }, -}; + }; </script> <template> @@ -75,10 +75,9 @@ export default { data-container="body" data-placement="top" :disabled="isLoading"> - <i - :class="iconClass" - aria-hidden="true"> - </i> + <icon + :name="icon" + /> <loading-icon v-if="isLoading" /> </button> </template> diff --git a/app/assets/javascripts/pipelines/components/empty_state.vue b/app/assets/javascripts/pipelines/components/empty_state.vue index 78322f30685..dfaa2574091 100644 --- a/app/assets/javascripts/pipelines/components/empty_state.vue +++ b/app/assets/javascripts/pipelines/components/empty_state.vue @@ -26,13 +26,15 @@ {{ 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.") }} + {{ 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-info" - > + > {{ s__("Pipelines|Get started with Pipelines") }} </a> </div> diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue index 19d8e1f49cf..d7effb27bff 100644 --- a/app/assets/javascripts/pipelines/components/graph/action_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue @@ -7,6 +7,14 @@ * TODO: Remove UJS from here and use an async request instead. */ export default { + components: { + icon, + }, + + directives: { + tooltip, + }, + props: { tooltipText: { type: String, @@ -29,14 +37,6 @@ }, }, - components: { - icon, - }, - - directives: { - tooltip, - }, - computed: { cssClass() { const actionIconDash = dasherize(this.actionIcon); @@ -53,7 +53,8 @@ :href="link" class="ci-action-icon-container ci-action-icon-wrapper" :class="cssClass" - data-container="body"> - <icon :name="actionIcon"/> + data-container="body" + > + <icon :name="actionIcon" /> </a> </template> diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue index 1c0944d45fc..7c4fd65e36f 100644 --- a/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue @@ -7,6 +7,13 @@ * TODO: Remove UJS from here and use an async request instead. */ export default { + components: { + icon, + }, + + directives: { + tooltip, + }, props: { tooltipText: { type: String, @@ -28,14 +35,6 @@ required: true, }, }, - - components: { - icon, - }, - - directives: { - tooltip, - }, }; </script> <template> @@ -47,7 +46,8 @@ rel="nofollow" class="ci-action-icon-wrapper js-ci-status-icon" data-container="body" - aria-label="Job's action"> - <icon :name="actionIcon"/> + aria-label="Job's action" + > + <icon :name="actionIcon" /> </a> </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 7006d05e7b2..b86e95f0b4a 100644 --- a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue @@ -27,13 +27,6 @@ * } */ export default { - props: { - job: { - type: Object, - required: true, - }, - }, - directives: { tooltip, }, @@ -43,12 +36,23 @@ jobNameComponent, }, + props: { + job: { + type: Object, + required: true, + }, + }, + computed: { tooltipText() { return `${this.job.name} - ${this.job.status.label}`; }, }, + mounted() { + this.stopDropdownClickPropagation(); + }, + methods: { /** * When the user right clicks or cmd/ctrl + click in the job name @@ -59,16 +63,13 @@ * target the click event of this component. */ stopDropdownClickPropagation() { - $(this.$el.querySelectorAll('.js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item')) + $(this.$el + .querySelectorAll('.js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item')) .on('click', (e) => { e.stopPropagation(); }); }, }, - - mounted() { - this.stopDropdownClickPropagation(); - }, }; </script> <template> @@ -83,22 +84,25 @@ <job-name-component :name="job.name" - :status="job.status" /> + :status="job.status" + /> <span class="dropdown-counter-badge"> - {{job.size}} + {{ job.size }} </span> </button> <ul class="dropdown-menu big-pipeline-graph-dropdown-menu js-grouped-pipeline-dropdown"> <li class="scrollable-menu"> <ul> - <li v-for="item in job.jobs"> + <li + v-for="(item, i) in job.jobs" + :key="i"> <job-component :job="item" :is-dropdown="true" css-class-job-name="mini-pipeline-graph-dropdown-item" - /> + /> </li> </ul> </li> diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue index 66bc1d1979c..a1f58580318 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue @@ -1,9 +1,13 @@ <script> import loadingIcon from '~/vue_shared/components/loading_icon.vue'; - import '~/flash'; import stageColumnComponent from './stage_column_component.vue'; export default { + components: { + stageColumnComponent, + loadingIcon, + }, + props: { isLoading: { type: Boolean, @@ -15,11 +19,6 @@ }, }, - components: { - stageColumnComponent, - loadingIcon, - }, - computed: { graph() { return this.pipeline.details && this.pipeline.details.stages; @@ -58,7 +57,7 @@ <loading-icon v-if="isLoading" size="3" - /> + /> </div> <ul @@ -70,7 +69,8 @@ :jobs="stage.groups" :key="stage.name" :stage-connector-class="stageConnectorClass(index, stage)" - :is-first-column="isFirstColumn(index)"/> + :is-first-column="isFirstColumn(index)" + /> </ul> </div> </div> diff --git a/app/assets/javascripts/pipelines/components/graph/job_component.vue b/app/assets/javascripts/pipelines/components/graph/job_component.vue index b01c799643c..9b136573135 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_component.vue @@ -29,6 +29,15 @@ */ export default { + components: { + actionComponent, + dropdownActionComponent, + jobNameComponent, + }, + + directives: { + tooltip, + }, props: { job: { type: Object, @@ -48,16 +57,6 @@ }, }, - components: { - actionComponent, - dropdownActionComponent, - jobNameComponent, - }, - - directives: { - tooltip, - }, - computed: { status() { return this.job && this.job.status ? this.job.status : {}; @@ -102,12 +101,12 @@ :class="cssClassJobName" data-container="body" class="js-pipeline-graph-job-link" - > + > <job-name-component :name="job.name" :status="job.status" - /> + /> </a> <div @@ -117,12 +116,12 @@ :title="tooltipText" :class="cssClassJobName" data-container="body" - > + > <job-name-component :name="job.name" :status="job.status" - /> + /> </div> <action-component @@ -131,7 +130,7 @@ :link="status.action.path" :action-icon="status.action.icon" :action-method="status.action.method" - /> + /> <dropdown-action-component v-if="hasAction && isDropdown" @@ -139,6 +138,6 @@ :link="status.action.path" :action-icon="status.action.icon" :action-method="status.action.method" - /> + /> </div> </template> diff --git a/app/assets/javascripts/pipelines/components/graph/job_name_component.vue b/app/assets/javascripts/pipelines/components/graph/job_name_component.vue index f46d21bd6d7..14f4964a406 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_name_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_name_component.vue @@ -8,6 +8,9 @@ * - Dropdown badge components */ export default { + components: { + ciIcon, + }, props: { name: { type: String, @@ -19,19 +22,14 @@ required: true, }, }, - - components: { - ciIcon, - }, }; </script> <template> <span class="ci-job-name-component"> - <ci-icon - :status="status" /> + <ci-icon :status="status" /> <span class="ci-status-text"> - {{name}} + {{ name }} </span> </span> </template> diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue index 9b1bbb0906f..e027f08ff5c 100644 --- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue @@ -1,58 +1,58 @@ <script> -import jobComponent from './job_component.vue'; -import dropdownJobComponent from './dropdown_job_component.vue'; + import jobComponent from './job_component.vue'; + import dropdownJobComponent from './dropdown_job_component.vue'; -export default { - props: { - title: { - type: String, - required: true, + export default { + components: { + jobComponent, + dropdownJobComponent, }, - jobs: { - type: Array, - required: true, - }, - - isFirstColumn: { - type: Boolean, - required: false, - default: false, - }, + props: { + title: { + type: String, + required: true, + }, - stageConnectorClass: { - type: String, - required: false, - default: '', - }, - }, + jobs: { + type: Array, + required: true, + }, - components: { - jobComponent, - dropdownJobComponent, - }, + isFirstColumn: { + type: Boolean, + required: false, + default: false, + }, - methods: { - firstJob(list) { - return list[0]; + stageConnectorClass: { + type: String, + required: false, + default: '', + }, }, - jobId(job) { - return `ci-badge-${job.name}`; - }, + methods: { + firstJob(list) { + return list[0]; + }, + + jobId(job) { + return `ci-badge-${job.name}`; + }, - buildConnnectorClass(index) { - return index === 0 && !this.isFirstColumn ? 'left-connector' : ''; + buildConnnectorClass(index) { + return index === 0 && !this.isFirstColumn ? 'left-connector' : ''; + }, }, - }, -}; + }; </script> <template> <li class="stage-column" :class="stageConnectorClass"> <div class="stage-name"> - {{title}} + {{ title }} </div> <div class="builds-container"> <ul> @@ -61,7 +61,8 @@ export default { :key="job.id" class="build" :class="buildConnnectorClass(index)" - :id="jobId(job)"> + :id="jobId(job)" + > <div class="curve"></div> @@ -69,12 +70,12 @@ export default { v-if="job.size === 1" :job="job" css-class-job-name="build-content" - /> + /> <dropdown-job-component v-if="job.size > 1" :job="job" - /> + /> </li> </ul> diff --git a/app/assets/javascripts/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue index 2a1ecac3707..e08c2092680 100644 --- a/app/assets/javascripts/pipelines/components/header_component.vue +++ b/app/assets/javascripts/pipelines/components/header_component.vue @@ -1,82 +1,81 @@ <script> -import ciHeader from '../../vue_shared/components/header_ci_component.vue'; -import eventHub from '../event_hub'; -import loadingIcon from '../../vue_shared/components/loading_icon.vue'; + import ciHeader from '../../vue_shared/components/header_ci_component.vue'; + import eventHub from '../event_hub'; + import loadingIcon from '../../vue_shared/components/loading_icon.vue'; -export default { - name: 'PipelineHeaderSection', - props: { - pipeline: { - type: Object, - required: true, + export default { + name: 'PipelineHeaderSection', + components: { + ciHeader, + loadingIcon, }, - isLoading: { - type: Boolean, - required: true, + props: { + pipeline: { + type: Object, + required: true, + }, + isLoading: { + type: Boolean, + required: true, + }, }, - }, - components: { - ciHeader, - loadingIcon, - }, - - data() { - return { - actions: this.getActions(), - }; - }, - - computed: { - status() { - return this.pipeline.details && this.pipeline.details.status; + data() { + return { + actions: this.getActions(), + }; }, - shouldRenderContent() { - return !this.isLoading && Object.keys(this.pipeline).length; + + computed: { + status() { + return this.pipeline.details && this.pipeline.details.status; + }, + shouldRenderContent() { + return !this.isLoading && Object.keys(this.pipeline).length; + }, }, - }, - methods: { - postAction(action) { - const index = this.actions.indexOf(action); + watch: { + pipeline() { + this.actions = this.getActions(); + }, + }, - this.$set(this.actions[index], 'isLoading', true); + methods: { + postAction(action) { + const index = this.actions.indexOf(action); - eventHub.$emit('headerPostAction', action); - }, + this.$set(this.actions[index], 'isLoading', true); - getActions() { - const actions = []; + eventHub.$emit('headerPostAction', action); + }, - if (this.pipeline.retry_path) { - actions.push({ - label: 'Retry', - path: this.pipeline.retry_path, - cssClass: 'js-retry-button btn btn-inverted-secondary', - type: 'button', - isLoading: false, - }); - } + getActions() { + const actions = []; - if (this.pipeline.cancel_path) { - actions.push({ - label: 'Cancel running', - path: this.pipeline.cancel_path, - cssClass: 'js-btn-cancel-pipeline btn btn-danger', - type: 'button', - isLoading: false, - }); - } + if (this.pipeline.retry_path) { + actions.push({ + label: 'Retry', + path: this.pipeline.retry_path, + cssClass: 'js-retry-button btn btn-inverted-secondary', + type: 'button', + isLoading: false, + }); + } - return actions; - }, - }, + if (this.pipeline.cancel_path) { + actions.push({ + label: 'Cancel running', + path: this.pipeline.cancel_path, + cssClass: 'js-btn-cancel-pipeline btn btn-danger', + type: 'button', + isLoading: false, + }); + } - watch: { - pipeline() { - this.actions = this.getActions(); + return actions; + }, }, - }, -}; + }; </script> <template> <div class="pipeline-header-container"> @@ -89,9 +88,11 @@ export default { :user="pipeline.user" :actions="actions" @actionClicked="postAction" - /> + /> <loading-icon v-if="isLoading" - size="2"/> + size="2" + class="prepend-top-default append-bottom-default" + /> </div> </template> diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue index 9da0aac50a1..ceb4d9ca604 100644 --- a/app/assets/javascripts/pipelines/components/pipeline_url.vue +++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue @@ -4,6 +4,13 @@ import popover from '../../vue_shared/directives/popover'; export default { + components: { + userAvatarLink, + }, + directives: { + tooltip, + popover, + }, props: { pipeline: { type: Object, @@ -14,13 +21,6 @@ required: true, }, }, - components: { - userAvatarLink, - }, - directives: { - tooltip, - popover, - }, computed: { user() { return this.pipeline.user; @@ -30,8 +30,16 @@ html: true, trigger: 'focus', placement: 'top', - title: '<div class="autodevops-title">This pipeline makes use of a predefined CI/CD configuration enabled by <b>Auto DevOps.</b></div>', - content: `<a class="autodevops-link" href="${this.autoDevopsHelpPath}" target="_blank" rel="noopener noreferrer nofollow">Learn more about Auto DevOps</a>`, + title: `<div class="autodevops-title"> + This pipeline makes use of a predefined CI/CD configuration enabled by <b>Auto DevOps.</b> + </div>`, + content: `<a + class="autodevops-link" + href="${this.autoDevopsHelpPath}" + target="_blank" + rel="noopener noreferrer nofollow"> + Learn more about Auto DevOps + </a>`, }; }, }, @@ -42,7 +50,7 @@ <a :href="pipeline.path" class="js-pipeline-url-link"> - <span class="pipeline-id">#{{pipeline.id}}</span> + <span class="pipeline-id">#{{ pipeline.id }}</span> </a> <span>by</span> <user-avatar-link diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue index 8fa416168e7..90930d5ff44 100644 --- a/app/assets/javascripts/pipelines/components/pipelines.vue +++ b/app/assets/javascripts/pipelines/components/pipelines.vue @@ -13,6 +13,15 @@ import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin'; export default { + components: { + tablePagination, + navigationTabs, + navigationControls, + }, + mixins: [ + pipelinesMixin, + CIPaginationMixin, + ], props: { store: { type: Object, @@ -28,15 +37,6 @@ default: 'root', }, }, - components: { - tablePagination, - navigationTabs, - navigationControls, - }, - mixins: [ - pipelinesMixin, - CIPaginationMixin, - ], data() { const pipelinesData = document.querySelector('#pipelines-list-vue').dataset; @@ -197,7 +197,8 @@ <div class="pipelines-container"> <div class="top-area scrolling-tabs-container inner-page-scroll-tabs" - v-if="!shouldRenderEmptyState"> + v-if="!shouldRenderEmptyState" + > <div class="fade-left"> <i class="fa fa-angle-left" @@ -215,16 +216,16 @@ :tabs="tabs" @onChangeTab="onChangeTab" scope="pipelines" - /> + /> <navigation-controls :new-pipeline-path="newPipelinePath" :has-ci-enabled="hasCiEnabled" :help-page-path="helpPagePath" - :resetCachePath="resetCachePath" + :reset-cache-path="resetCachePath" :ci-lint-path="ciLintPath" :can-create-pipeline="canCreatePipelineParsed " - /> + /> </div> <div class="content-list pipelines"> @@ -234,22 +235,23 @@ 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" - /> + /> <div class="blank-state-row" - v-if="shouldRenderNoPipelinesMessage"> + v-if="shouldRenderNoPipelinesMessage" + > <div class="blank-state-center"> <h2 class="blank-state-title js-blank-state-title">No pipelines to show.</h2> </div> @@ -257,21 +259,22 @@ <div class="table-holder" - v-if="shouldRenderTable"> + v-if="shouldRenderTable" + > <pipelines-table-component :pipelines="state.pipelines" :update-graph-dropdown="updateGraphDropdown" :auto-devops-help-path="autoDevopsPath" :view-type="viewType" - /> + /> </div> <table-pagination v-if="shouldRenderPagination" :change="onChangePage" :page-info="state.pageInfo" - /> + /> </div> </div> </template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_actions.vue index f3c0aca17ba..3297af7bde4 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_actions.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue @@ -1,25 +1,25 @@ <script> - import playIconSvg from 'icons/_icon_play.svg'; import eventHub from '../event_hub'; import loadingIcon from '../../vue_shared/components/loading_icon.vue'; + import icon from '../../vue_shared/components/icon.vue'; import tooltip from '../../vue_shared/directives/tooltip'; export default { - props: { - actions: { - type: Array, - required: true, - }, - }, directives: { tooltip, }, components: { loadingIcon, + icon, + }, + props: { + actions: { + type: Array, + required: true, + }, }, data() { return { - playIconSvg, isLoading: false, }; }, @@ -50,8 +50,12 @@ data-toggle="dropdown" data-placement="top" aria-label="Manual job" - :disabled="isLoading"> - <span v-html="playIconSvg"></span> + :disabled="isLoading" + > + <icon + name="play" + class="icon-play" + /> <i class="fa fa-caret-down" aria-hidden="true"> @@ -60,14 +64,18 @@ </button> <ul class="dropdown-menu dropdown-menu-align-right"> - <li v-for="action in actions"> + <li + v-for="(action, i) in actions" + :key="i" + > <button type="button" class="js-pipeline-action-link no-btn btn" @click="onClickAction(action.path)" :class="{ disabled: isActionDisabled(action) }" - :disabled="isActionDisabled(action)"> - {{action.name}} + :disabled="isActionDisabled(action)" + > + {{ action.name }} </button> </li> </ul> diff --git a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue index 831aa92ac4f..1b9e0f917a4 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue @@ -3,46 +3,50 @@ import icon from '../../vue_shared/components/icon.vue'; export default { - props: { - artifacts: { - type: Array, - required: true, - }, - }, directives: { tooltip, }, components: { icon, }, + props: { + artifacts: { + type: Array, + required: true, + }, + }, }; </script> <template> <div class="btn-group" - role="group"> + role="group" + > <button v-tooltip class="dropdown-toggle btn btn-default build-artifacts js-pipeline-dropdown-download" title="Artifacts" data-placement="top" data-toggle="dropdown" - aria-label="Artifacts"> - <icon - name="download"> - </icon> + aria-label="Artifacts" + > + <icon name="download" /> <i class="fa fa-caret-down" - aria-hidden="true"> + aria-hidden="true" + > </i> </button> <ul class="dropdown-menu dropdown-menu-align-right"> - <li v-for="artifact in artifacts"> + <li + v-for="(artifact, i) in artifacts" + :key="i"> <a rel="nofollow" download - :href="artifact.path"> - Download {{artifact.name}} artifacts + :href="artifact.path" + > + Download {{ artifact.name }} artifacts </a> </li> </ul> diff --git a/app/assets/javascripts/pipelines/components/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_table.vue index 16a705cbaff..6681b89e629 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table.vue @@ -7,6 +7,9 @@ * Given an array of objects, renders a table. */ export default { + components: { + pipelinesTableRowComponent, + }, props: { pipelines: { type: Array, @@ -26,34 +29,36 @@ required: true, }, }, - components: { - pipelinesTableRowComponent, - }, }; </script> <template> <div class="ci-table"> <div class="gl-responsive-table-row table-row-header" - role="row"> + role="row" + > <div class="table-section section-10 js-pipeline-status pipeline-status" - role="rowheader"> + role="rowheader" + > Status </div> <div class="table-section section-15 js-pipeline-info pipeline-info" - role="rowheader"> + role="rowheader" + > Pipeline </div> <div - class="table-section section-25 js-pipeline-commit pipeline-commit" - role="rowheader"> + class="table-section section-20 js-pipeline-commit pipeline-commit" + role="rowheader" + > Commit </div> <div - class="table-section section-15 js-pipeline-stages pipeline-stages" - role="rowheader"> + class="table-section section-20 js-pipeline-stages pipeline-stages" + role="rowheader" + > Stages </div> </div> diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue index 33fbce993b2..d0e4cf7ff40 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue @@ -1,227 +1,228 @@ <script> -/* eslint-disable no-param-reassign */ -import asyncButtonComponent from './async_button.vue'; -import pipelinesActionsComponent from './pipelines_actions.vue'; -import pipelinesArtifactsComponent from './pipelines_artifacts.vue'; -import ciBadge from '../../vue_shared/components/ci_badge_link.vue'; -import pipelineStage from './stage.vue'; -import pipelineUrl from './pipeline_url.vue'; -import pipelinesTimeago from './time_ago.vue'; -import commitComponent from '../../vue_shared/components/commit.vue'; + /* eslint-disable no-param-reassign */ + import asyncButtonComponent from './async_button.vue'; + import pipelinesActionsComponent from './pipelines_actions.vue'; + import pipelinesArtifactsComponent from './pipelines_artifacts.vue'; + import ciBadge from '../../vue_shared/components/ci_badge_link.vue'; + import pipelineStage from './stage.vue'; + import pipelineUrl from './pipeline_url.vue'; + import pipelinesTimeago from './time_ago.vue'; + import commitComponent from '../../vue_shared/components/commit.vue'; -/** - * Pipeline table row. - * - * Given the received object renders a table row in the pipelines' table. - */ -export default { - props: { - pipeline: { - type: Object, - required: true, + /** + * Pipeline table row. + * + * Given the received object renders a table row in the pipelines' table. + */ + export default { + components: { + asyncButtonComponent, + pipelinesActionsComponent, + pipelinesArtifactsComponent, + commitComponent, + pipelineStage, + pipelineUrl, + ciBadge, + pipelinesTimeago, }, - updateGraphDropdown: { - type: Boolean, - required: false, - default: false, + props: { + pipeline: { + type: Object, + required: true, + }, + updateGraphDropdown: { + type: Boolean, + required: false, + default: false, + }, + autoDevopsHelpPath: { + type: String, + required: true, + }, + viewType: { + type: String, + required: true, + }, }, - autoDevopsHelpPath: { - type: String, - required: true, - }, - viewType: { - type: String, - required: true, - }, - }, - components: { - asyncButtonComponent, - pipelinesActionsComponent, - pipelinesArtifactsComponent, - commitComponent, - pipelineStage, - pipelineUrl, - ciBadge, - pipelinesTimeago, - }, - computed: { - /** - * If provided, returns the commit tag. - * Needed to render the commit component column. - * - * This field needs a lot of verification, because of different possible cases: - * - * 1. person who is an author of a commit might be a GitLab user - * 2. if person who is an author of a commit is a GitLab user he/she can have a GitLab avatar - * 3. If GitLab user does not have avatar he/she might have a Gravatar - * 4. If committer is not a GitLab User he/she can have a Gravatar - * 5. We do not have consistent API object in this case - * 6. We should improve API and the code - * - * @returns {Object|Undefined} - */ - commitAuthor() { - let commitAuthorInformation; + computed: { + /** + * If provided, returns the commit tag. + * Needed to render the commit component column. + * + * This field needs a lot of verification, because of different possible cases: + * + * 1. person who is an author of a commit might be a GitLab user + * 2. if person who is an author of a commit is a GitLab user he/she can have a GitLab avatar + * 3. If GitLab user does not have avatar he/she might have a Gravatar + * 4. If committer is not a GitLab User he/she can have a Gravatar + * 5. We do not have consistent API object in this case + * 6. We should improve API and the code + * + * @returns {Object|Undefined} + */ + commitAuthor() { + let commitAuthorInformation; - if (!this.pipeline || !this.pipeline.commit) { - return null; - } + if (!this.pipeline || !this.pipeline.commit) { + return null; + } - // 1. person who is an author of a commit might be a GitLab user - if (this.pipeline.commit.author) { - // 2. if person who is an author of a commit is a GitLab user - // he/she can have a GitLab avatar - if (this.pipeline.commit.author.avatar_url) { - commitAuthorInformation = this.pipeline.commit.author; + // 1. person who is an author of a commit might be a GitLab user + if (this.pipeline.commit.author) { + // 2. if person who is an author of a commit is a GitLab user + // he/she can have a GitLab avatar + if (this.pipeline.commit.author.avatar_url) { + commitAuthorInformation = this.pipeline.commit.author; - // 3. If GitLab user does not have avatar he/she might have a Gravatar - } else if (this.pipeline.commit.author_gravatar_url) { - commitAuthorInformation = Object.assign({}, this.pipeline.commit.author, { + // 3. If GitLab user does not have avatar he/she might have a Gravatar + } else if (this.pipeline.commit.author_gravatar_url) { + commitAuthorInformation = Object.assign({}, this.pipeline.commit.author, { + avatar_url: this.pipeline.commit.author_gravatar_url, + }); + } + // 4. If committer is not a GitLab User he/she can have a Gravatar + } else { + commitAuthorInformation = { avatar_url: this.pipeline.commit.author_gravatar_url, - }); + path: `mailto:${this.pipeline.commit.author_email}`, + username: this.pipeline.commit.author_name, + }; } - // 4. If committer is not a GitLab User he/she can have a Gravatar - } else { - commitAuthorInformation = { - avatar_url: this.pipeline.commit.author_gravatar_url, - path: `mailto:${this.pipeline.commit.author_email}`, - username: this.pipeline.commit.author_name, - }; - } - return commitAuthorInformation; - }, + return commitAuthorInformation; + }, - /** - * If provided, returns the commit tag. - * Needed to render the commit component column. - * - * @returns {String|Undefined} - */ - commitTag() { - if (this.pipeline.ref && - this.pipeline.ref.tag) { - return this.pipeline.ref.tag; - } - return undefined; - }, + /** + * If provided, returns the commit tag. + * Needed to render the commit component column. + * + * @returns {String|Undefined} + */ + commitTag() { + if (this.pipeline.ref && + this.pipeline.ref.tag) { + return this.pipeline.ref.tag; + } + return undefined; + }, - /** - * If provided, returns the commit ref. - * Needed to render the commit component column. - * - * Matches `path` prop sent in the API to `ref_url` prop needed - * in the commit component. - * - * @returns {Object|Undefined} - */ - commitRef() { - if (this.pipeline.ref) { - return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => { - if (prop === 'path') { - accumulator.ref_url = this.pipeline.ref[prop]; - } else { - accumulator[prop] = this.pipeline.ref[prop]; - } - return accumulator; - }, {}); - } + /** + * If provided, returns the commit ref. + * Needed to render the commit component column. + * + * Matches `path` prop sent in the API to `ref_url` prop needed + * in the commit component. + * + * @returns {Object|Undefined} + */ + commitRef() { + if (this.pipeline.ref) { + return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => { + if (prop === 'path') { + accumulator.ref_url = this.pipeline.ref[prop]; + } else { + accumulator[prop] = this.pipeline.ref[prop]; + } + return accumulator; + }, {}); + } - return undefined; - }, + return undefined; + }, - /** - * If provided, returns the commit url. - * Needed to render the commit component column. - * - * @returns {String|Undefined} - */ - commitUrl() { - if (this.pipeline.commit && - this.pipeline.commit.commit_path) { - return this.pipeline.commit.commit_path; - } - return undefined; - }, + /** + * If provided, returns the commit url. + * Needed to render the commit component column. + * + * @returns {String|Undefined} + */ + commitUrl() { + if (this.pipeline.commit && + this.pipeline.commit.commit_path) { + return this.pipeline.commit.commit_path; + } + return undefined; + }, - /** - * If provided, returns the commit short sha. - * Needed to render the commit component column. - * - * @returns {String|Undefined} - */ - commitShortSha() { - if (this.pipeline.commit && - this.pipeline.commit.short_id) { - return this.pipeline.commit.short_id; - } - return undefined; - }, + /** + * If provided, returns the commit short sha. + * Needed to render the commit component column. + * + * @returns {String|Undefined} + */ + commitShortSha() { + if (this.pipeline.commit && + this.pipeline.commit.short_id) { + return this.pipeline.commit.short_id; + } + return undefined; + }, - /** - * If provided, returns the commit title. - * Needed to render the commit component column. - * - * @returns {String|Undefined} - */ - commitTitle() { - if (this.pipeline.commit && - this.pipeline.commit.title) { - return this.pipeline.commit.title; - } - return undefined; - }, + /** + * If provided, returns the commit title. + * Needed to render the commit component column. + * + * @returns {String|Undefined} + */ + commitTitle() { + if (this.pipeline.commit && + this.pipeline.commit.title) { + return this.pipeline.commit.title; + } + return undefined; + }, - /** - * Timeago components expects a number - * - * @return {type} description - */ - pipelineDuration() { - if (this.pipeline.details && this.pipeline.details.duration) { - return this.pipeline.details.duration; - } + /** + * Timeago components expects a number + * + * @return {type} description + */ + pipelineDuration() { + if (this.pipeline.details && this.pipeline.details.duration) { + return this.pipeline.details.duration; + } - return 0; - }, + return 0; + }, - /** - * Timeago component expects a String. - * - * @return {String} - */ - pipelineFinishedAt() { - if (this.pipeline.details && this.pipeline.details.finished_at) { - return this.pipeline.details.finished_at; - } + /** + * Timeago component expects a String. + * + * @return {String} + */ + pipelineFinishedAt() { + if (this.pipeline.details && this.pipeline.details.finished_at) { + return this.pipeline.details.finished_at; + } - return ''; - }, + return ''; + }, - pipelineStatus() { - if (this.pipeline.details && this.pipeline.details.status) { - return this.pipeline.details.status; - } - return {}; - }, + pipelineStatus() { + if (this.pipeline.details && this.pipeline.details.status) { + return this.pipeline.details.status; + } + return {}; + }, - displayPipelineActions() { - return this.pipeline.flags.retryable || - this.pipeline.flags.cancelable || - this.pipeline.details.manual_actions.length || - this.pipeline.details.artifacts.length; - }, + displayPipelineActions() { + return this.pipeline.flags.retryable || + this.pipeline.flags.cancelable || + this.pipeline.details.manual_actions.length || + this.pipeline.details.artifacts.length; + }, - isChildView() { - return this.viewType === 'child'; + isChildView() { + return this.viewType === 'child'; + }, }, - }, -}; + }; </script> <template> <div class="commit gl-responsive-table-row"> <div class="table-section section-10 commit-link"> - <div class="table-mobile-header" + <div + class="table-mobile-header" role="rowheader"> Status </div> @@ -229,16 +230,16 @@ export default { <ci-badge :status="pipelineStatus" :show-text="!isChildView" - /> + /> </div> </div> <pipeline-url :pipeline="pipeline" :auto-devops-help-path="autoDevopsHelpPath" - /> + /> - <div class="table-section section-25"> + <div class="table-section section-20"> <div class="table-mobile-header" role="rowheader"> @@ -253,32 +254,35 @@ export default { :title="commitTitle" :author="commitAuthor" :show-branch="!isChildView" - /> + /> </div> </div> - <div class="table-section section-wrap section-15 stage-cell"> + <div class="table-section section-wrap section-20 stage-cell"> <div class="table-mobile-header" role="rowheader"> Stages </div> <div class="table-mobile-content"> - <div class="stage-container dropdown js-mini-pipeline-graph" - v-if="pipeline.details.stages.length > 0" - v-for="stage in pipeline.details.stages"> - <pipeline-stage - :stage="stage" - :update-dropdown="updateGraphDropdown" + <template v-if="pipeline.details.stages.length > 0"> + <div + class="stage-container dropdown js-mini-pipeline-graph" + v-for="(stage, index) in pipeline.details.stages" + :key="index"> + <pipeline-stage + :stage="stage" + :update-dropdown="updateGraphDropdown" /> - </div> + </div> + </template> </div> </div> <pipelines-timeago :duration="pipelineDuration" :finished-time="pipelineFinishedAt" - /> + /> <div v-if="displayPipelineActions" @@ -287,13 +291,13 @@ export default { <pipelines-actions-component v-if="pipeline.details.manual_actions.length" :actions="pipeline.details.manual_actions" - /> + /> <pipelines-artifacts-component v-if="pipeline.details.artifacts.length" class="hidden-xs hidden-sm" :artifacts="pipeline.details.artifacts" - /> + /> <async-button-component v-if="pipeline.flags.retryable" @@ -301,16 +305,16 @@ export default { css-class="js-pipelines-retry-button btn-default btn-retry" title="Retry" icon="repeat" - /> + /> <async-button-component v-if="pipeline.flags.cancelable" :endpoint="pipeline.cancel_path" css-class="js-pipelines-cancel-button btn-remove" title="Cancel" - icon="remove" + icon="close" confirm-action-message="Are you sure you want to cancel this pipeline?" - /> + /> </div> </div> </div> diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue index ac9d9c901ca..58806aa114a 100644 --- a/app/assets/javascripts/pipelines/components/stage.vue +++ b/app/assets/javascripts/pipelines/components/stage.vue @@ -1,133 +1,135 @@ <script> -/** - * Renders each stage of the pipeline mini graph. - * - * Given the provided endpoint will make a request to - * fetch the dropdown data when the stage is clicked. - * - * Request is made inside this component to make it reusable between: - * 1. Pipelines main table - * 2. Pipelines table in commit and Merge request views - * 3. Merge request widget - * 4. Commit widget - */ - -import Flash from '../../flash'; -import icon from '../../vue_shared/components/icon.vue'; -import loadingIcon from '../../vue_shared/components/loading_icon.vue'; -import tooltip from '../../vue_shared/directives/tooltip'; - -export default { - props: { - stage: { - type: Object, - required: true, + /** + * Renders each stage of the pipeline mini graph. + * + * Given the provided endpoint will make a request to + * fetch the dropdown data when the stage is clicked. + * + * Request is made inside this component to make it reusable between: + * 1. Pipelines main table + * 2. Pipelines table in commit and Merge request views + * 3. Merge request widget + * 4. Commit widget + */ + + import Flash from '../../flash'; + import icon from '../../vue_shared/components/icon.vue'; + import loadingIcon from '../../vue_shared/components/loading_icon.vue'; + import tooltip from '../../vue_shared/directives/tooltip'; + + export default { + components: { + loadingIcon, + icon, }, - updateDropdown: { - type: Boolean, - required: false, - default: false, + directives: { + tooltip, }, - }, - - directives: { - tooltip, - }, - - data() { - return { - isLoading: false, - dropdownContent: '', - }; - }, - - components: { - loadingIcon, - icon, - }, - - updated() { - if (this.dropdownContent.length > 0) { - this.stopDropdownClickPropagation(); - } - }, - - watch: { - updateDropdown() { - if (this.updateDropdown && - this.isDropdownOpen() && - !this.isLoading) { - this.fetchJobs(); - } - }, - }, - methods: { - onClickStage() { - if (!this.isDropdownOpen()) { - this.isLoading = true; - this.fetchJobs(); - } + props: { + stage: { + type: Object, + required: true, + }, + + updateDropdown: { + type: Boolean, + required: false, + default: false, + }, }, - fetchJobs() { - this.$http.get(this.stage.dropdown_path) - .then(response => response.json()) - .then((data) => { - this.dropdownContent = data.html; - this.isLoading = false; - }) - .catch(() => { - this.closeDropdown(); - this.isLoading = false; - - const flash = new Flash('Something went wrong on our end.'); - return flash; - }); + data() { + return { + isLoading: false, + dropdownContent: '', + }; }, - /** - * When the user right clicks or cmd/ctrl + click in the job name - * the dropdown should not be closed and the link should open in another tab, - * so we stop propagation of the click event inside the dropdown. - * - * Since this component is rendered multiple times per page we need to guarantee we only - * target the click event of this component. - */ - stopDropdownClickPropagation() { - $(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item')) - .on('click', (e) => { - e.stopPropagation(); - }); - }, + computed: { + dropdownClass() { + return this.dropdownContent.length > 0 ? + 'js-builds-dropdown-container' : + 'js-builds-dropdown-loading'; + }, - closeDropdown() { - if (this.isDropdownOpen()) { - $(this.$refs.dropdown).dropdown('toggle'); - } - }, + triggerButtonClass() { + return `ci-status-icon-${this.stage.status.group}`; + }, - isDropdownOpen() { - return this.$el.classList.contains('open'); + borderlessIcon() { + return `${this.stage.status.icon}_borderless`; + }, }, - }, - computed: { - dropdownClass() { - return this.dropdownContent.length > 0 ? 'js-builds-dropdown-container' : 'js-builds-dropdown-loading'; + watch: { + updateDropdown() { + if (this.updateDropdown && + this.isDropdownOpen() && + !this.isLoading) { + this.fetchJobs(); + } + }, }, - triggerButtonClass() { - return `ci-status-icon-${this.stage.status.group}`; + updated() { + if (this.dropdownContent.length > 0) { + this.stopDropdownClickPropagation(); + } }, - borderlessIcon() { - return `${this.stage.status.icon}_borderless`; + methods: { + onClickStage() { + if (!this.isDropdownOpen()) { + this.isLoading = true; + this.fetchJobs(); + } + }, + + fetchJobs() { + this.$http.get(this.stage.dropdown_path) + .then(response => response.json()) + .then((data) => { + this.dropdownContent = data.html; + this.isLoading = false; + }) + .catch(() => { + this.closeDropdown(); + this.isLoading = false; + + const flash = new Flash('Something went wrong on our end.'); + return flash; + }); + }, + + /** + * When the user right clicks or cmd/ctrl + click in the job name + * the dropdown should not be closed and the link should open in another tab, + * so we stop propagation of the click event inside the dropdown. + * + * Since this component is rendered multiple times per page we need to guarantee we only + * target the click event of this component. + */ + stopDropdownClickPropagation() { + $(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item')) + .on('click', (e) => { + e.stopPropagation(); + }); + }, + + closeDropdown() { + if (this.isDropdownOpen()) { + $(this.$refs.dropdown).dropdown('toggle'); + } + }, + + isDropdownOpen() { + return this.$el.classList.contains('open'); + }, }, - }, -}; + }; </script> <template> @@ -143,36 +145,41 @@ export default { type="button" id="stageDropdown" aria-haspopup="true" - aria-expanded="false"> + aria-expanded="false" + > <span aria-hidden="true" - :aria-label="stage.title"> - <icon - :name="borderlessIcon"/> + :aria-label="stage.title" + > + <icon :name="borderlessIcon" /> </span> <i class="fa fa-caret-down" - aria-hidden="true"> + aria-hidden="true" + > </i> </button> <ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container" - aria-labelledby="stageDropdown"> + aria-labelledby="stageDropdown" + > <li :class="dropdownClass" - class="js-builds-dropdown-list scrollable-menu"> + class="js-builds-dropdown-list scrollable-menu" + > <loading-icon v-if="isLoading"/> <ul v-else - v-html="dropdownContent"> + v-html="dropdownContent" + > </ul> </li> </ul> </div> -</script> +</template> diff --git a/app/assets/javascripts/pipelines/components/time_ago.vue b/app/assets/javascripts/pipelines/components/time_ago.vue index 037684b4e72..cd54d26c9d3 100644 --- a/app/assets/javascripts/pipelines/components/time_ago.vue +++ b/app/assets/javascripts/pipelines/components/time_ago.vue @@ -5,6 +5,12 @@ import timeagoMixin from '../../vue_shared/mixins/timeago'; export default { + directives: { + tooltip, + }, + mixins: [ + timeagoMixin, + ], props: { finishedTime: { type: String, @@ -15,12 +21,6 @@ required: true, }, }, - mixins: [ - timeagoMixin, - ], - directives: { - tooltip, - }, data() { return { iconTimerSvg, @@ -60,26 +60,29 @@ <div class="table-section section-15 pipelines-time-ago"> <div class="table-mobile-header" - role="rowheader"> + role="rowheader" + > Duration </div> <div class="table-mobile-content"> <p class="duration" - v-if="hasDuration"> - <span - v-html="iconTimerSvg"> + v-if="hasDuration" + > + <span v-html="iconTimerSvg"> </span> - {{durationFormated}} + {{ durationFormated }} </p> <p class="finished-at hidden-xs hidden-sm" - v-if="hasFinishedTime"> + v-if="hasFinishedTime" + > <i class="fa fa-calendar" - aria-hidden="true"> + aria-hidden="true" + > </i> <time @@ -87,9 +90,9 @@ data-placement="top" data-container="body" :title="tooltipTitle(finishedTime)"> - {{timeFormated(finishedTime)}} + {{ timeFormated(finishedTime) }} </time> </p> </div> </div> -</script> +</template> diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js index 206023d4ddb..d88d280cb3f 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js +++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js @@ -15,14 +15,14 @@ document.addEventListener('DOMContentLoaded', () => { // eslint-disable-next-line new Vue({ el: '#js-pipeline-graph-vue', + components: { + pipelineGraph, + }, data() { return { mediator, }; }, - components: { - pipelineGraph, - }, render(createElement) { return createElement('pipeline-graph', { props: { @@ -36,14 +36,14 @@ document.addEventListener('DOMContentLoaded', () => { // eslint-disable-next-line new Vue({ el: '#js-pipeline-header-vue', + components: { + pipelineHeader, + }, data() { return { mediator, }; }, - components: { - pipelineHeader, - }, created() { eventHub.$on('headerPostAction', this.postAction); }, diff --git a/app/assets/javascripts/pipelines/pipelines_bundle.js b/app/assets/javascripts/pipelines/pipelines_bundle.js index 3e4b6eeb5bf..ab5596e70f0 100644 --- a/app/assets/javascripts/pipelines/pipelines_bundle.js +++ b/app/assets/javascripts/pipelines/pipelines_bundle.js @@ -7,6 +7,9 @@ Vue.use(Translate); document.addEventListener('DOMContentLoaded', () => new Vue({ el: '#pipelines-list-vue', + components: { + pipelinesComponent, + }, data() { const store = new PipelinesStore(); @@ -14,9 +17,6 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ store, }; }, - components: { - pipelinesComponent, - }, render(createElement) { return createElement('pipelines-component', { props: { diff --git a/app/assets/javascripts/pipelines/pipelines_charts.js b/app/assets/javascripts/pipelines/pipelines_charts.js index 001faf4be33..821aa7e229f 100644 --- a/app/assets/javascripts/pipelines/pipelines_charts.js +++ b/app/assets/javascripts/pipelines/pipelines_charts.js @@ -6,16 +6,16 @@ document.addEventListener('DOMContentLoaded', () => { const data = { labels: chartScope.labels, datasets: [{ - fillColor: '#7f8fa4', - strokeColor: '#7f8fa4', - pointColor: '#7f8fa4', + fillColor: '#707070', + strokeColor: '#707070', + pointColor: '#707070', pointStrokeColor: '#EEE', data: chartScope.totalValues, }, { - fillColor: '#44aa22', - strokeColor: '#44aa22', - pointColor: '#44aa22', + fillColor: '#1aaa55', + strokeColor: '#1aaa55', + pointColor: '#1aaa55', pointStrokeColor: '#fff', data: chartScope.successValues, }, diff --git a/app/assets/javascripts/preview_markdown.js b/app/assets/javascripts/preview_markdown.js index ffaafb3ee9e..86c7b56198d 100644 --- a/app/assets/javascripts/preview_markdown.js +++ b/app/assets/javascripts/preview_markdown.js @@ -6,195 +6,193 @@ // (including the explanation of quick actions), and showing a warning when // more than `x` users are referenced. // -(function () { - var lastTextareaPreviewed; - var lastTextareaHeight = null; - var markdownPreview; - var previewButtonSelector; - var writeButtonSelector; - - window.MarkdownPreview = (function () { - function MarkdownPreview() {} - - // Minimum number of users referenced before triggering a warning - MarkdownPreview.prototype.referenceThreshold = 10; - MarkdownPreview.prototype.emptyMessage = 'Nothing to preview.'; - - MarkdownPreview.prototype.ajaxCache = {}; - - MarkdownPreview.prototype.showPreview = function ($form) { - var mdText; - var preview = $form.find('.js-md-preview'); - var url = preview.data('url'); - if (preview.hasClass('md-preview-loading')) { - return; - } - mdText = $form.find('textarea.markdown-area').val(); - - if (mdText.trim().length === 0) { - preview.text(this.emptyMessage); - this.hideReferencedUsers($form); - } else { - preview.addClass('md-preview-loading').text('Loading...'); - this.fetchMarkdownPreview(mdText, url, (function (response) { - var body; - if (response.body.length > 0) { - body = response.body; - } else { - body = this.emptyMessage; - } - - preview.removeClass('md-preview-loading').html(body); - preview.renderGFM(); - this.renderReferencedUsers(response.references.users, $form); - - if (response.references.commands) { - this.renderReferencedCommands(response.references.commands, $form); - } - }).bind(this)); - } - }; - MarkdownPreview.prototype.fetchMarkdownPreview = function (text, url, success) { - if (!url) { - return; - } - if (text === this.ajaxCache.text) { - success(this.ajaxCache.response); - return; - } - $.ajax({ - type: 'POST', - url: url, - data: { - text: text - }, - dataType: 'json', - success: (function (response) { - this.ajaxCache = { - text: text, - response: response - }; - success(response); - }).bind(this) - }); - }; - - MarkdownPreview.prototype.hideReferencedUsers = function ($form) { - $form.find('.referenced-users').hide(); - }; - - MarkdownPreview.prototype.renderReferencedUsers = function (users, $form) { - var referencedUsers; - referencedUsers = $form.find('.referenced-users'); - if (referencedUsers.length) { - if (users.length >= this.referenceThreshold) { - referencedUsers.show(); - referencedUsers.find('.js-referenced-users-count').text(users.length); - } else { - referencedUsers.hide(); - } - } - }; - - MarkdownPreview.prototype.hideReferencedCommands = function ($form) { - $form.find('.referenced-commands').hide(); - }; - - MarkdownPreview.prototype.renderReferencedCommands = function (commands, $form) { - var referencedCommands; - referencedCommands = $form.find('.referenced-commands'); - if (commands.length > 0) { - referencedCommands.html(commands); - referencedCommands.show(); +var lastTextareaPreviewed; +var lastTextareaHeight = null; +var markdownPreview; +var previewButtonSelector; +var writeButtonSelector; + +function MarkdownPreview() {} + +// Minimum number of users referenced before triggering a warning +MarkdownPreview.prototype.referenceThreshold = 10; +MarkdownPreview.prototype.emptyMessage = 'Nothing to preview.'; + +MarkdownPreview.prototype.ajaxCache = {}; + +MarkdownPreview.prototype.showPreview = function ($form) { + var mdText; + var preview = $form.find('.js-md-preview'); + var url = preview.data('url'); + if (preview.hasClass('md-preview-loading')) { + return; + } + mdText = $form.find('textarea.markdown-area').val(); + + if (mdText.trim().length === 0) { + preview.text(this.emptyMessage); + this.hideReferencedUsers($form); + } else { + preview.addClass('md-preview-loading').text('Loading...'); + this.fetchMarkdownPreview(mdText, url, (function (response) { + var body; + if (response.body.length > 0) { + body = response.body; } else { - referencedCommands.html(''); - referencedCommands.hide(); + body = this.emptyMessage; } - }; - - return MarkdownPreview; - }()); - - markdownPreview = new window.MarkdownPreview(); - previewButtonSelector = '.js-md-preview-button'; - writeButtonSelector = '.js-md-write-button'; - lastTextareaPreviewed = null; - const markdownToolbar = $('.md-header-toolbar'); - - $.fn.setupMarkdownPreview = function () { - var $form = $(this); - $form.find('textarea.markdown-area').on('input', function () { - markdownPreview.hideReferencedUsers($form); - }); - }; - - $(document).on('markdown-preview:show', function (e, $form) { - if (!$form) { - return; - } - - lastTextareaPreviewed = $form.find('textarea.markdown-area'); - lastTextareaHeight = lastTextareaPreviewed.height(); - - // toggle tabs - $form.find(writeButtonSelector).parent().removeClass('active'); - $form.find(previewButtonSelector).parent().addClass('active'); - // toggle content - $form.find('.md-write-holder').hide(); - $form.find('.md-preview-holder').show(); - markdownToolbar.removeClass('active'); - markdownPreview.showPreview($form); - }); - - $(document).on('markdown-preview:hide', function (e, $form) { - if (!$form) { - return; - } - lastTextareaPreviewed = null; - - if (lastTextareaHeight) { - $form.find('textarea.markdown-area').height(lastTextareaHeight); - } - - // toggle tabs - $form.find(writeButtonSelector).parent().addClass('active'); - $form.find(previewButtonSelector).parent().removeClass('active'); - - // toggle content - $form.find('.md-write-holder').show(); - $form.find('textarea.markdown-area').focus(); - $form.find('.md-preview-holder').hide(); - markdownToolbar.addClass('active'); + preview.removeClass('md-preview-loading').html(body); + preview.renderGFM(); + this.renderReferencedUsers(response.references.users, $form); - markdownPreview.hideReferencedCommands($form); + if (response.references.commands) { + this.renderReferencedCommands(response.references.commands, $form); + } + }).bind(this)); + } +}; + +MarkdownPreview.prototype.fetchMarkdownPreview = function (text, url, success) { + if (!url) { + return; + } + if (text === this.ajaxCache.text) { + success(this.ajaxCache.response); + return; + } + $.ajax({ + type: 'POST', + url: url, + data: { + text: text + }, + dataType: 'json', + success: (function (response) { + this.ajaxCache = { + text: text, + response: response + }; + success(response); + }).bind(this) }); - - $(document).on('markdown-preview:toggle', function (e, keyboardEvent) { - var $target; - $target = $(keyboardEvent.target); - if ($target.is('textarea.markdown-area')) { - $(document).triggerHandler('markdown-preview:show', [$target.closest('form')]); - keyboardEvent.preventDefault(); - } else if (lastTextareaPreviewed) { - $target = lastTextareaPreviewed; - $(document).triggerHandler('markdown-preview:hide', [$target.closest('form')]); - keyboardEvent.preventDefault(); +}; + +MarkdownPreview.prototype.hideReferencedUsers = function ($form) { + $form.find('.referenced-users').hide(); +}; + +MarkdownPreview.prototype.renderReferencedUsers = function (users, $form) { + var referencedUsers; + referencedUsers = $form.find('.referenced-users'); + if (referencedUsers.length) { + if (users.length >= this.referenceThreshold) { + referencedUsers.show(); + referencedUsers.find('.js-referenced-users-count').text(users.length); + } else { + referencedUsers.hide(); } + } +}; + +MarkdownPreview.prototype.hideReferencedCommands = function ($form) { + $form.find('.referenced-commands').hide(); +}; + +MarkdownPreview.prototype.renderReferencedCommands = function (commands, $form) { + var referencedCommands; + referencedCommands = $form.find('.referenced-commands'); + if (commands.length > 0) { + referencedCommands.html(commands); + referencedCommands.show(); + } else { + referencedCommands.html(''); + referencedCommands.hide(); + } +}; + +markdownPreview = new MarkdownPreview(); + +previewButtonSelector = '.js-md-preview-button'; +writeButtonSelector = '.js-md-write-button'; +lastTextareaPreviewed = null; +const markdownToolbar = $('.md-header-toolbar'); + +$.fn.setupMarkdownPreview = function () { + var $form = $(this); + $form.find('textarea.markdown-area').on('input', function () { + markdownPreview.hideReferencedUsers($form); }); +}; + +$(document).on('markdown-preview:show', function (e, $form) { + if (!$form) { + return; + } + + lastTextareaPreviewed = $form.find('textarea.markdown-area'); + lastTextareaHeight = lastTextareaPreviewed.height(); + + // toggle tabs + $form.find(writeButtonSelector).parent().removeClass('active'); + $form.find(previewButtonSelector).parent().addClass('active'); + + // toggle content + $form.find('.md-write-holder').hide(); + $form.find('.md-preview-holder').show(); + markdownToolbar.removeClass('active'); + markdownPreview.showPreview($form); +}); + +$(document).on('markdown-preview:hide', function (e, $form) { + if (!$form) { + return; + } + lastTextareaPreviewed = null; - $(document).on('click', previewButtonSelector, function (e) { - var $form; - e.preventDefault(); - $form = $(this).closest('form'); - $(document).triggerHandler('markdown-preview:show', [$form]); - }); - - $(document).on('click', writeButtonSelector, function (e) { - var $form; - e.preventDefault(); - $form = $(this).closest('form'); - $(document).triggerHandler('markdown-preview:hide', [$form]); - }); -}()); + if (lastTextareaHeight) { + $form.find('textarea.markdown-area').height(lastTextareaHeight); + } + + // toggle tabs + $form.find(writeButtonSelector).parent().addClass('active'); + $form.find(previewButtonSelector).parent().removeClass('active'); + + // toggle content + $form.find('.md-write-holder').show(); + $form.find('textarea.markdown-area').focus(); + $form.find('.md-preview-holder').hide(); + markdownToolbar.addClass('active'); + + markdownPreview.hideReferencedCommands($form); +}); + +$(document).on('markdown-preview:toggle', function (e, keyboardEvent) { + var $target; + $target = $(keyboardEvent.target); + if ($target.is('textarea.markdown-area')) { + $(document).triggerHandler('markdown-preview:show', [$target.closest('form')]); + keyboardEvent.preventDefault(); + } else if (lastTextareaPreviewed) { + $target = lastTextareaPreviewed; + $(document).triggerHandler('markdown-preview:hide', [$target.closest('form')]); + keyboardEvent.preventDefault(); + } +}); + +$(document).on('click', previewButtonSelector, function (e) { + var $form; + e.preventDefault(); + $form = $(this).closest('form'); + $(document).triggerHandler('markdown-preview:show', [$form]); +}); + +$(document).on('click', writeButtonSelector, function (e) { + var $form; + e.preventDefault(); + $form = $(this).closest('form'); + $(document).triggerHandler('markdown-preview:hide', [$form]); +}); + +export default MarkdownPreview; diff --git a/app/assets/javascripts/profile/account/components/delete_account_modal.vue b/app/assets/javascripts/profile/account/components/delete_account_modal.vue index 36ad618aa46..1ffe482d782 100644 --- a/app/assets/javascripts/profile/account/components/delete_account_modal.vue +++ b/app/assets/javascripts/profile/account/components/delete_account_modal.vue @@ -4,6 +4,9 @@ import csrf from '~/lib/utils/csrf'; export default { + components: { + modal, + }, props: { actionUrl: { type: String, @@ -24,9 +27,6 @@ enteredUsername: '', }; }, - components: { - modal, - }, computed: { csrfToken() { return csrf.token; @@ -85,7 +85,9 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`), @submit="onSubmit" :submit-disabled="!canSubmit()"> - <template slot="body" slot-scope="props"> + <template + slot="body" + slot-scope="props"> <p v-html="props.text"></p> <form @@ -96,13 +98,19 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`), <input type="hidden" name="_method" - value="delete" /> + value="delete" + /> <input type="hidden" name="authenticity_token" - :value="csrfToken" /> + :value="csrfToken" + /> - <p id="input-label" v-html="inputLabel"></p> + <p + id="input-label" + v-html="inputLabel" + > + </p> <input v-if="confirmWithPassword" @@ -110,14 +118,16 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`), class="form-control" type="password" v-model="enteredPassword" - aria-labelledby="input-label" /> + aria-labelledby="input-label" + /> <input v-else name="username" class="form-control" type="text" v-model="enteredUsername" - aria-labelledby="input-label" /> + aria-labelledby="input-label" + /> </form> </template> diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js index 0da32b4a3cc..586d188350f 100644 --- a/app/assets/javascripts/project_find_file.js +++ b/app/assets/javascripts/project_find_file.js @@ -1,6 +1,9 @@ /* 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 fuzzaldrinPlus from 'fuzzaldrin-plus'; +import axios from '~/lib/utils/axios_utils'; +import flash from '~/flash'; +import { __ } from '~/locale'; // highlight text(awefwbwgtc -> <b>a</b>wefw<b>b</b>wgt<b>c</b> ) const highlighter = function(element, text, matches) { @@ -72,19 +75,14 @@ export default class ProjectFindFile { // files pathes load load(url) { - return $.ajax({ - url: url, - method: "get", - dataType: "json", - success: (function(_this) { - return function(data) { - _this.element.find(".loading").hide(); - _this.filePaths = data; - _this.findFile(); - return _this.element.find(".files-slider tr.tree-item").eq(0).addClass("selected").focus(); - }; - })(this) - }); + axios.get(url) + .then(({ data }) => { + this.element.find('.loading').hide(); + this.filePaths = data; + this.findFile(); + this.element.find('.files-slider tr.tree-item').eq(0).addClass('selected').focus(); + }) + .catch(() => flash(__('An error occurred while loading filenames'))); } // render result diff --git a/app/assets/javascripts/projects/permissions/components/project_feature_setting.vue b/app/assets/javascripts/projects/permissions/components/project_feature_setting.vue deleted file mode 100644 index 8fce4c63872..00000000000 --- a/app/assets/javascripts/projects/permissions/components/project_feature_setting.vue +++ /dev/null @@ -1,104 +0,0 @@ -<script> -import projectFeatureToggle from '../../../vue_shared/components/toggle_button.vue'; - -export default { - props: { - name: { - type: String, - required: false, - default: '', - }, - options: { - type: Array, - required: false, - default: () => [], - }, - value: { - type: Number, - required: false, - default: 0, - }, - disabledInput: { - type: Boolean, - required: false, - default: false, - }, - }, - - components: { - projectFeatureToggle, - }, - - computed: { - featureEnabled() { - return this.value !== 0; - }, - - displayOptions() { - if (this.featureEnabled) { - return this.options; - } - return [ - [0, 'Enable feature to choose access level'], - ]; - }, - - displaySelectInput() { - return this.disabledInput || !this.featureEnabled || this.displayOptions.length < 2; - }, - }, - - model: { - prop: 'value', - event: 'change', - }, - - methods: { - toggleFeature(featureEnabled) { - if (featureEnabled === false || this.options.length < 1) { - this.$emit('change', 0); - } else { - const [firstOptionValue] = this.options[this.options.length - 1]; - this.$emit('change', firstOptionValue); - } - }, - - selectOption(e) { - this.$emit('change', Number(e.target.value)); - }, - }, -}; -</script> - -<template> - <div class="project-feature-controls" :data-for="name"> - <input - v-if="name" - type="hidden" - :name="name" - :value="value" - /> - <project-feature-toggle - :value="featureEnabled" - @change="toggleFeature" - :disabledInput="disabledInput" - /> - <div class="select-wrapper"> - <select - class="form-control project-repo-select select-control" - @change="selectOption" - :disabled="displaySelectInput" - > - <option - v-for="[optionValue, optionName] in displayOptions" - :key="optionValue" - :value="optionValue" - :selected="optionValue === value" - > - {{optionName}} - </option> - </select> - <i aria-hidden="true" class="fa fa-chevron-down"></i> - </div> - </div> -</template> diff --git a/app/assets/javascripts/projects/permissions/components/project_setting_row.vue b/app/assets/javascripts/projects/permissions/components/project_setting_row.vue deleted file mode 100644 index 6140d74fea8..00000000000 --- a/app/assets/javascripts/projects/permissions/components/project_setting_row.vue +++ /dev/null @@ -1,36 +0,0 @@ -<script> -export default { - props: { - label: { - type: String, - required: false, - default: null, - }, - helpPath: { - type: String, - required: false, - default: null, - }, - helpText: { - type: String, - required: false, - default: null, - }, - }, -}; -</script> - -<template> - <div class="project-feature-row"> - <label v-if="label" class="label-light"> - {{label}} - <a v-if="helpPath" :href="helpPath" target="_blank"> - <i aria-hidden="true" data-hidden="true" class="fa fa-question-circle"></i> - </a> - </label> - <span v-if="helpText" class="help-block"> - {{helpText}} - </span> - <slot /> - </div> -</template> diff --git a/app/assets/javascripts/projects/permissions/components/settings_panel.vue b/app/assets/javascripts/projects/permissions/components/settings_panel.vue deleted file mode 100644 index 639429baf26..00000000000 --- a/app/assets/javascripts/projects/permissions/components/settings_panel.vue +++ /dev/null @@ -1,312 +0,0 @@ -<script> -import projectFeatureSetting from './project_feature_setting.vue'; -import projectFeatureToggle from '../../../vue_shared/components/toggle_button.vue'; -import projectSettingRow from './project_setting_row.vue'; -import { visibilityOptions, visibilityLevelDescriptions } from '../constants'; -import { toggleHiddenClassBySelector } from '../external'; - -export default { - props: { - currentSettings: { - type: Object, - required: true, - }, - canChangeVisibilityLevel: { - type: Boolean, - required: false, - default: false, - }, - allowedVisibilityOptions: { - type: Array, - required: false, - default: () => [0, 10, 20], - }, - lfsAvailable: { - type: Boolean, - required: false, - default: false, - }, - registryAvailable: { - type: Boolean, - required: false, - default: false, - }, - visibilityHelpPath: { - type: String, - required: false, - }, - lfsHelpPath: { - type: String, - required: false, - }, - registryHelpPath: { - type: String, - required: false, - }, - }, - - data() { - const defaults = { - visibilityOptions, - visibilityLevel: visibilityOptions.PUBLIC, - issuesAccessLevel: 20, - repositoryAccessLevel: 20, - mergeRequestsAccessLevel: 20, - buildsAccessLevel: 20, - wikiAccessLevel: 20, - snippetsAccessLevel: 20, - containerRegistryEnabled: true, - lfsEnabled: true, - requestAccessEnabled: true, - highlightChangesClass: false, - }; - - return { ...defaults, ...this.currentSettings }; - }, - - components: { - projectFeatureSetting, - projectFeatureToggle, - projectSettingRow, - }, - - computed: { - featureAccessLevelOptions() { - const options = [ - [10, 'Only Project Members'], - ]; - if (this.visibilityLevel !== visibilityOptions.PRIVATE) { - options.push([20, 'Everyone With Access']); - } - return options; - }, - - repoFeatureAccessLevelOptions() { - return this.featureAccessLevelOptions.filter( - ([value]) => value <= this.repositoryAccessLevel, - ); - }, - - repositoryEnabled() { - return this.repositoryAccessLevel > 0; - }, - - visibilityLevelDescription() { - return visibilityLevelDescriptions[this.visibilityLevel]; - }, - }, - - methods: { - highlightChanges() { - this.highlightChangesClass = true; - this.$nextTick(() => { - this.highlightChangesClass = false; - }); - }, - - visibilityAllowed(option) { - return this.allowedVisibilityOptions.includes(option); - }, - }, - - watch: { - visibilityLevel(value, oldValue) { - if (value === visibilityOptions.PRIVATE) { - // when private, features are restricted to "only team members" - this.issuesAccessLevel = Math.min(10, this.issuesAccessLevel); - this.repositoryAccessLevel = Math.min(10, this.repositoryAccessLevel); - this.mergeRequestsAccessLevel = Math.min(10, this.mergeRequestsAccessLevel); - this.buildsAccessLevel = Math.min(10, this.buildsAccessLevel); - this.wikiAccessLevel = Math.min(10, this.wikiAccessLevel); - this.snippetsAccessLevel = Math.min(10, this.snippetsAccessLevel); - this.highlightChanges(); - } else if (oldValue === visibilityOptions.PRIVATE) { - // if changing away from private, make enabled features more permissive - if (this.issuesAccessLevel > 0) this.issuesAccessLevel = 20; - if (this.repositoryAccessLevel > 0) this.repositoryAccessLevel = 20; - if (this.mergeRequestsAccessLevel > 0) this.mergeRequestsAccessLevel = 20; - if (this.buildsAccessLevel > 0) this.buildsAccessLevel = 20; - if (this.wikiAccessLevel > 0) this.wikiAccessLevel = 20; - if (this.snippetsAccessLevel > 0) this.snippetsAccessLevel = 20; - this.highlightChanges(); - } - }, - - repositoryAccessLevel(value, oldValue) { - if (value < oldValue) { - // sub-features cannot have more premissive access level - this.mergeRequestsAccessLevel = Math.min(this.mergeRequestsAccessLevel, value); - this.buildsAccessLevel = Math.min(this.buildsAccessLevel, value); - - if (value === 0) { - this.containerRegistryEnabled = false; - this.lfsEnabled = false; - } - } else if (oldValue === 0) { - this.mergeRequestsAccessLevel = value; - this.buildsAccessLevel = value; - this.containerRegistryEnabled = true; - this.lfsEnabled = true; - } - }, - - issuesAccessLevel(value, oldValue) { - if (value === 0) toggleHiddenClassBySelector('.issues-feature', true); - else if (oldValue === 0) toggleHiddenClassBySelector('.issues-feature', false); - }, - - mergeRequestsAccessLevel(value, oldValue) { - if (value === 0) toggleHiddenClassBySelector('.merge-requests-feature', true); - else if (oldValue === 0) toggleHiddenClassBySelector('.merge-requests-feature', false); - }, - - buildsAccessLevel(value, oldValue) { - if (value === 0) toggleHiddenClassBySelector('.builds-feature', true); - else if (oldValue === 0) toggleHiddenClassBySelector('.builds-feature', false); - }, - }, -}; - -</script> - -<template> - <div> - <div class="project-visibility-setting"> - <project-setting-row - label="Project visibility" - :help-path="visibilityHelpPath" - > - <div class="project-feature-controls"> - <div class="select-wrapper"> - <select - name="project[visibility_level]" - v-model="visibilityLevel" - class="form-control select-control" - :disabled="!canChangeVisibilityLevel" - > - <option - :value="visibilityOptions.PRIVATE" - :disabled="!visibilityAllowed(visibilityOptions.PRIVATE)" - > - Private - </option> - <option - :value="visibilityOptions.INTERNAL" - :disabled="!visibilityAllowed(visibilityOptions.INTERNAL)" - > - Internal - </option> - <option - :value="visibilityOptions.PUBLIC" - :disabled="!visibilityAllowed(visibilityOptions.PUBLIC)" - > - Public - </option> - </select> - <i aria-hidden="true" data-hidden="true" class="fa fa-chevron-down"></i> - </div> - </div> - <span class="help-block">{{ visibilityLevelDescription }}</span> - <label v-if="visibilityLevel !== visibilityOptions.PUBLIC" class="request-access"> - <input - type="hidden" - name="project[request_access_enabled]" - :value="requestAccessEnabled" - /> - <input type="checkbox" v-model="requestAccessEnabled" /> - Allow users to request access - </label> - </project-setting-row> - </div> - <div class="project-feature-settings" :class="{ 'highlight-changes': highlightChangesClass }"> - <project-setting-row - label="Issues" - help-text="Lightweight issue tracking system for this project" - > - <project-feature-setting - name="project[project_feature_attributes][issues_access_level]" - :options="featureAccessLevelOptions" - v-model="issuesAccessLevel" - /> - </project-setting-row> - <project-setting-row - label="Repository" - help-text="View and edit files in this project" - > - <project-feature-setting - name="project[project_feature_attributes][repository_access_level]" - :options="featureAccessLevelOptions" - v-model="repositoryAccessLevel" - /> - </project-setting-row> - <div class="project-feature-setting-group"> - <project-setting-row - label="Merge requests" - help-text="Submit changes to be merged upstream" - > - <project-feature-setting - name="project[project_feature_attributes][merge_requests_access_level]" - :options="repoFeatureAccessLevelOptions" - v-model="mergeRequestsAccessLevel" - :disabledInput="!repositoryEnabled" - /> - </project-setting-row> - <project-setting-row - label="Pipelines" - help-text="Build, test, and deploy your changes" - > - <project-feature-setting - name="project[project_feature_attributes][builds_access_level]" - :options="repoFeatureAccessLevelOptions" - v-model="buildsAccessLevel" - :disabledInput="!repositoryEnabled" - /> - </project-setting-row> - <project-setting-row - v-if="registryAvailable" - label="Container registry" - :help-path="registryHelpPath" - help-text="Every project can have its own space to store its Docker images" - > - <project-feature-toggle - name="project[container_registry_enabled]" - v-model="containerRegistryEnabled" - :disabledInput="!repositoryEnabled" - /> - </project-setting-row> - <project-setting-row - v-if="lfsAvailable" - label="Git Large File Storage" - :help-path="lfsHelpPath" - help-text="Manages large files such as audio, video, and graphics files" - > - <project-feature-toggle - name="project[lfs_enabled]" - v-model="lfsEnabled" - :disabledInput="!repositoryEnabled" - /> - </project-setting-row> - </div> - <project-setting-row - label="Wiki" - help-text="Pages for project documentation" - > - <project-feature-setting - name="project[project_feature_attributes][wiki_access_level]" - :options="featureAccessLevelOptions" - v-model="wikiAccessLevel" - /> - </project-setting-row> - <project-setting-row - label="Snippets" - help-text="Share code pastes with others out of Git repository" - > - <project-feature-setting - name="project[project_feature_attributes][snippets_access_level]" - :options="featureAccessLevelOptions" - v-model="snippetsAccessLevel" - /> - </project-setting-row> - </div> - </div> -</template> diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js index 4710e70d619..f5133111d04 100644 --- a/app/assets/javascripts/projects/project_new.js +++ b/app/assets/javascripts/projects/project_new.js @@ -100,8 +100,6 @@ const bindEvents = () => { $projectImportUrl.keyup(() => deriveProjectPathFromUrl($projectImportUrl)); }; -document.addEventListener('DOMContentLoaded', bindEvents); - export default { bindEvents, deriveProjectPathFromUrl, diff --git a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue new file mode 100644 index 00000000000..63f20a0041d --- /dev/null +++ b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue @@ -0,0 +1,120 @@ +<script> + import Visibility from 'visibilityjs'; + import ciIcon from '~/vue_shared/components/ci_icon.vue'; + import loadingIcon from '~/vue_shared/components/loading_icon.vue'; + import Poll from '~/lib/utils/poll'; + import Flash from '~/flash'; + import { s__, sprintf } from '~/locale'; + import tooltip from '~/vue_shared/directives/tooltip'; + import CommitPipelineService from '../services/commit_pipeline_service'; + + export default { + directives: { + tooltip, + }, + components: { + ciIcon, + loadingIcon, + }, + props: { + endpoint: { + type: String, + required: true, + }, + /* This prop can be used to replace some of the `render_commit_status` + used across GitLab, this way we could use this vue component and add a + realtime status where it makes sense + realtime: { + type: Boolean, + required: false, + default: true, + }, */ + }, + data() { + return { + ciStatus: {}, + isLoading: true, + }; + }, + computed: { + statusTitle() { + return sprintf(s__('Commits|Commit: %{commitText}'), { commitText: this.ciStatus.text }); + }, + }, + mounted() { + this.service = new CommitPipelineService(this.endpoint); + this.initPolling(); + }, + methods: { + successCallback(res) { + const pipelines = res.data.pipelines; + if (pipelines.length > 0) { + // The pipeline entity always keeps the latest pipeline info on the `details.status` + this.ciStatus = pipelines[0].details.status; + } + this.isLoading = false; + }, + errorCallback() { + this.ciStatus = { + text: 'not found', + icon: 'status_notfound', + group: 'notfound', + }; + this.isLoading = false; + Flash(s__('Something went wrong on our end')); + }, + initPolling() { + this.poll = new Poll({ + resource: this.service, + method: 'fetchData', + successCallback: response => this.successCallback(response), + errorCallback: this.errorCallback, + }); + + if (!Visibility.hidden()) { + this.isLoading = true; + this.poll.makeRequest(); + } else { + this.fetchPipelineCommitData(); + } + + Visibility.change(() => { + if (!Visibility.hidden()) { + this.poll.restart(); + } else { + this.poll.stop(); + } + }); + }, + fetchPipelineCommitData() { + this.service.fetchData() + .then(this.successCallback) + .catch(this.errorCallback); + }, + }, + destroy() { + this.poll.stop(); + }, + }; +</script> +<template> + <div> + <loading-icon + label="Loading pipeline status" + size="3" + v-if="isLoading" + /> + <a + v-else + :href="ciStatus.details_path" + > + <ci-icon + v-tooltip + :title="statusTitle" + :aria-label="statusTitle" + data-container="body" + :status="ciStatus" + /> + </a> + </div> +</template> diff --git a/app/assets/javascripts/projects/tree/services/commit_pipeline_service.js b/app/assets/javascripts/projects/tree/services/commit_pipeline_service.js new file mode 100644 index 00000000000..4b4189bc2de --- /dev/null +++ b/app/assets/javascripts/projects/tree/services/commit_pipeline_service.js @@ -0,0 +1,11 @@ +import axios from '~/lib/utils/axios_utils'; + +export default class CommitPipelineService { + constructor(endpoint) { + this.endpoint = endpoint; + } + + fetchData() { + return axios.get(this.endpoint); + } +} diff --git a/app/assets/javascripts/projects_dropdown/components/app.vue b/app/assets/javascripts/projects_dropdown/components/app.vue index 7606605be32..34a60dd574b 100644 --- a/app/assets/javascripts/projects_dropdown/components/app.vue +++ b/app/assets/javascripts/projects_dropdown/components/app.vue @@ -47,6 +47,22 @@ export default { return this.store.getSearchedProjects(); }, }, + created() { + if (this.currentProject.id) { + this.logCurrentProjectAccess(); + } + + eventHub.$on('dropdownOpen', this.fetchFrequentProjects); + eventHub.$on('searchProjects', this.fetchSearchedProjects); + eventHub.$on('searchCleared', this.handleSearchClear); + eventHub.$on('searchFailed', this.handleSearchFailure); + }, + beforeDestroy() { + eventHub.$off('dropdownOpen', this.fetchFrequentProjects); + eventHub.$off('searchProjects', this.fetchSearchedProjects); + eventHub.$off('searchCleared', this.handleSearchClear); + eventHub.$off('searchFailed', this.handleSearchFailure); + }, methods: { toggleFrequentProjectsList(state) { this.isLoadingProjects = !state; @@ -108,22 +124,6 @@ export default { this.toggleSearchProjectsList(true); }, }, - created() { - if (this.currentProject.id) { - this.logCurrentProjectAccess(); - } - - eventHub.$on('dropdownOpen', this.fetchFrequentProjects); - eventHub.$on('searchProjects', this.fetchSearchedProjects); - eventHub.$on('searchCleared', this.handleSearchClear); - eventHub.$on('searchFailed', this.handleSearchFailure); - }, - beforeDestroy() { - eventHub.$off('dropdownOpen', this.fetchFrequentProjects); - eventHub.$off('searchProjects', this.fetchSearchedProjects); - eventHub.$off('searchCleared', this.handleSearchClear); - eventHub.$off('searchFailed', this.handleSearchFailure); - }, }; </script> diff --git a/app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue b/app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue index 093554cd0bc..246dbeaaded 100644 --- a/app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue +++ b/app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue @@ -1,32 +1,32 @@ <script> -import { s__ } from '../../locale'; -import projectsListItem from './projects_list_item.vue'; + import { s__ } from '../../locale'; + import projectsListItem from './projects_list_item.vue'; -export default { - components: { - projectsListItem, - }, - props: { - projects: { - type: Array, - required: true, + export default { + components: { + projectsListItem, }, - localStorageFailed: { - type: Boolean, - required: true, + props: { + projects: { + type: Array, + required: true, + }, + localStorageFailed: { + type: Boolean, + required: true, + }, }, - }, - computed: { - isListEmpty() { - return this.projects.length === 0; + computed: { + isListEmpty() { + return this.projects.length === 0; + }, + listEmptyMessage() { + return this.localStorageFailed ? + s__('ProjectsDropdown|This feature requires browser localStorage support') : + s__('ProjectsDropdown|Projects you visit often will appear here'); + }, }, - listEmptyMessage() { - return this.localStorageFailed ? - s__('ProjectsDropdown|This feature requires browser localStorage support') : - s__('ProjectsDropdown|Projects you visit often will appear here'); - }, - }, -}; + }; </script> <template> @@ -40,7 +40,7 @@ export default { class="section-empty" v-if="isListEmpty" > - {{listEmptyMessage}} + {{ listEmptyMessage }} </li> <projects-list-item v-else diff --git a/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue b/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue index d482a7025de..759cdd1ded9 100644 --- a/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue +++ b/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue @@ -1,76 +1,77 @@ <script> -import identicon from '../../vue_shared/components/identicon.vue'; + /* eslint-disable vue/require-default-prop, vue/require-prop-types */ + import identicon from '../../vue_shared/components/identicon.vue'; -export default { - components: { - identicon, - }, - props: { - matcher: { - type: String, - required: false, + export default { + components: { + identicon, }, - projectId: { - type: Number, - required: true, - }, - projectName: { - type: String, - required: true, - }, - namespace: { - type: String, - required: true, - }, - webUrl: { - type: String, - required: true, - }, - avatarUrl: { - required: true, - validator(value) { - return value === null || typeof value === 'string'; + props: { + matcher: { + type: String, + required: false, + }, + projectId: { + type: Number, + required: true, + }, + projectName: { + type: String, + required: true, + }, + namespace: { + type: String, + required: true, + }, + webUrl: { + type: String, + required: true, + }, + avatarUrl: { + required: true, + validator(value) { + return value === null || typeof value === 'string'; + }, }, }, - }, - computed: { - hasAvatar() { - return this.avatarUrl !== null; - }, - highlightedProjectName() { - if (this.matcher) { - const matcherRegEx = new RegExp(this.matcher, 'gi'); - const matches = this.projectName.match(matcherRegEx); + computed: { + hasAvatar() { + return this.avatarUrl !== null; + }, + highlightedProjectName() { + if (this.matcher) { + const matcherRegEx = new RegExp(this.matcher, 'gi'); + const matches = this.projectName.match(matcherRegEx); - if (matches && matches.length > 0) { - return this.projectName.replace(matches[0], `<b>${matches[0]}</b>`); + if (matches && matches.length > 0) { + return this.projectName.replace(matches[0], `<b>${matches[0]}</b>`); + } } - } - return this.projectName; - }, - /** - * Smartly truncates project namespace by doing two things; - * 1. Only include Group names in path by removing project name - * 2. Only include first and last group names in the path - * when namespace has more than 2 groups present - * - * First part (removal of project name from namespace) can be - * done from backend but doing so involves migration of - * existing project namespaces which is not wise thing to do. - */ - truncatedNamespace() { - const namespaceArr = this.namespace.split(' / '); - namespaceArr.splice(-1, 1); - let namespace = namespaceArr.join(' / '); + return this.projectName; + }, + /** + * Smartly truncates project namespace by doing two things; + * 1. Only include Group names in path by removing project name + * 2. Only include first and last group names in the path + * when namespace has more than 2 groups present + * + * First part (removal of project name from namespace) can be + * done from backend but doing so involves migration of + * existing project namespaces which is not wise thing to do. + */ + truncatedNamespace() { + const namespaceArr = this.namespace.split(' / '); + namespaceArr.splice(-1, 1); + let namespace = namespaceArr.join(' / '); - if (namespaceArr.length > 2) { - namespace = `${namespaceArr[0]} / ... / ${namespaceArr.pop()}`; - } + if (namespaceArr.length > 2) { + namespace = `${namespaceArr[0]} / ... / ${namespaceArr.pop()}`; + } - return namespace; + return namespace; + }, }, - }, -}; + }; </script> <template> @@ -92,7 +93,7 @@ export default { <identicon v-else size-class="s32" - :entity-id=projectId + :entity-id="projectId" :entity-name="projectName" /> </div> @@ -108,7 +109,7 @@ export default { <div class="project-namespace" :title="namespace" - >{{truncatedNamespace}}</div> + >{{ truncatedNamespace }}</div> </div> </a> </li> diff --git a/app/assets/javascripts/projects_dropdown/components/search.vue b/app/assets/javascripts/projects_dropdown/components/search.vue index 53bc76d0f2d..0c46ed184be 100644 --- a/app/assets/javascripts/projects_dropdown/components/search.vue +++ b/app/assets/javascripts/projects_dropdown/components/search.vue @@ -1,47 +1,47 @@ <script> -import _ from 'underscore'; -import eventHub from '../event_hub'; + import _ from 'underscore'; + import eventHub from '../event_hub'; -export default { - data() { - return { - searchQuery: '', - }; - }, - watch: { - searchQuery() { - this.handleInput(); + export default { + data() { + return { + searchQuery: '', + }; }, - }, - methods: { - setFocus() { - this.$refs.search.focus(); + watch: { + searchQuery() { + this.handleInput(); + }, }, - emitSearchEvents() { - if (this.searchQuery) { - eventHub.$emit('searchProjects', this.searchQuery); - } else { - eventHub.$emit('searchCleared'); - } + mounted() { + eventHub.$on('dropdownOpen', this.setFocus); }, - /** - * Callback function within _.debounce is intentionally - * kept as ES5 `function() {}` instead of ES6 `() => {}` - * as it otherwise messes up function context - * and component reference is no longer accessible via `this` - */ - // eslint-disable-next-line func-names - handleInput: _.debounce(function () { - this.emitSearchEvents(); - }, 500), - }, - mounted() { - eventHub.$on('dropdownOpen', this.setFocus); - }, - beforeDestroy() { - eventHub.$off('dropdownOpen', this.setFocus); - }, -}; + beforeDestroy() { + eventHub.$off('dropdownOpen', this.setFocus); + }, + methods: { + setFocus() { + this.$refs.search.focus(); + }, + emitSearchEvents() { + if (this.searchQuery) { + eventHub.$emit('searchProjects', this.searchQuery); + } else { + eventHub.$emit('searchCleared'); + } + }, + /** + * Callback function within _.debounce is intentionally + * kept as ES5 `function() {}` instead of ES6 `() => {}` + * as it otherwise messes up function context + * and component reference is no longer accessible via `this` + */ + // eslint-disable-next-line func-names + handleInput: _.debounce(function () { + this.emitSearchEvents(); + }, 500), + }, + }; </script> <template> @@ -59,6 +59,7 @@ export default { v-if="!searchQuery" class="search-icon fa fa-fw fa-search" aria-hidden="true" - /> + > + </i> </div> </template> diff --git a/app/assets/javascripts/projects_dropdown/index.js b/app/assets/javascripts/projects_dropdown/index.js index 2660da3c558..e78ebce2923 100644 --- a/app/assets/javascripts/projects_dropdown/index.js +++ b/app/assets/javascripts/projects_dropdown/index.js @@ -19,11 +19,8 @@ document.addEventListener('DOMContentLoaded', () => { return; } - $(navEl).on('show.bs.dropdown', (e) => { - const dropdownEl = $(e.currentTarget).find('.projects-dropdown-menu'); - dropdownEl.one('transitionend', () => { - eventHub.$emit('dropdownOpen'); - }); + $(navEl).on('shown.bs.dropdown', () => { + eventHub.$emit('dropdownOpen'); }); // eslint-disable-next-line no-new diff --git a/app/assets/javascripts/projects_dropdown/service/projects_service.js b/app/assets/javascripts/projects_dropdown/service/projects_service.js index 9cbd8f21f2a..7231f520933 100644 --- a/app/assets/javascripts/projects_dropdown/service/projects_service.js +++ b/app/assets/javascripts/projects_dropdown/service/projects_service.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import VueResource from 'vue-resource'; diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js b/app/assets/javascripts/protected_branches/protected_branch_create.js index 0a9fdb074e5..2948baeab11 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_create.js +++ b/app/assets/javascripts/protected_branches/protected_branch_create.js @@ -1,6 +1,6 @@ import _ from 'underscore'; import ProtectedBranchAccessDropdown from './protected_branch_access_dropdown'; -import ProtectedBranchDropdown from './protected_branch_dropdown'; +import CreateItemDropdown from '../create_item_dropdown'; import AccessorUtilities from '../lib/utils/accessor'; const PB_LOCAL_STORAGE_KEY = 'protected-branches-defaults'; @@ -35,10 +35,12 @@ export default class ProtectedBranchCreate { onSelect: this.onSelectCallback, }); - // Protected branch dropdown - this.protectedBranchDropdown = new ProtectedBranchDropdown({ + this.createItemDropdown = new CreateItemDropdown({ $dropdown: $protectedBranchDropdown, + defaultToggleLabel: 'Protected Branch', + fieldName: 'protected_branch[name]', onSelect: this.onSelectCallback, + getData: ProtectedBranchCreate.getProtectedBranches, }); this.loadPreviousSelection($allowedToMergeDropdown.data('glDropdown'), $allowedToPushDropdown.data('glDropdown')); @@ -60,6 +62,10 @@ export default class ProtectedBranchCreate { this.$form.find('input[type="submit"]').attr('disabled', completedForm); } + static getProtectedBranches(term, callback) { + callback(gon.open_branches); + } + loadPreviousSelection(mergeDropdown, pushDropdown) { let mergeIndex = 0; let pushIndex = 0; diff --git a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js deleted file mode 100644 index 678882a8d2c..00000000000 --- a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js +++ /dev/null @@ -1,90 +0,0 @@ -import _ from 'underscore'; - -export default class ProtectedBranchDropdown { - /** - * @param {Object} options containing - * `$dropdown` target element - * `onSelect` event callback - * $dropdown must be an element created using `dropdown_branch()` rails helper - */ - constructor(options) { - this.onSelect = options.onSelect; - this.$dropdown = options.$dropdown; - this.$dropdownContainer = this.$dropdown.parent(); - this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer'); - this.$protectedBranch = this.$dropdownContainer.find('.js-create-new-protected-branch'); - - this.buildDropdown(); - this.bindEvents(); - - // Hide footer - this.toggleFooter(true); - } - - buildDropdown() { - this.$dropdown.glDropdown({ - data: this.getProtectedBranches.bind(this), - filterable: true, - remote: false, - search: { - fields: ['title'], - }, - selectable: true, - toggleLabel(selected) { - return (selected && 'id' in selected) ? selected.title : 'Protected Branch'; - }, - fieldName: 'protected_branch[name]', - text(protectedBranch) { - return _.escape(protectedBranch.title); - }, - id(protectedBranch) { - return _.escape(protectedBranch.id); - }, - onFilter: this.toggleCreateNewButton.bind(this), - clicked: (options) => { - options.e.preventDefault(); - this.onSelect(); - }, - }); - } - - bindEvents() { - this.$protectedBranch.on('click', this.onClickCreateWildcard.bind(this)); - } - - onClickCreateWildcard(e) { - e.preventDefault(); - - // Refresh the dropdown's data, which ends up calling `getProtectedBranches` - this.$dropdown.data('glDropdown').remote.execute(); - this.$dropdown.data('glDropdown').selectRowAtIndex(); - } - - getProtectedBranches(term, callback) { - if (this.selectedBranch) { - callback(gon.open_branches.concat(this.selectedBranch)); - } else { - callback(gon.open_branches); - } - } - - toggleCreateNewButton(branchName) { - if (branchName) { - this.selectedBranch = { - title: branchName, - id: branchName, - text: branchName, - }; - - this.$dropdownContainer - .find('.js-create-new-protected-branch code') - .text(branchName); - } - - this.toggleFooter(!branchName); - } - - toggleFooter(toggleState) { - this.$dropdownFooter.toggleClass('hidden', toggleState); - } -} diff --git a/app/assets/javascripts/protected_tags/protected_tag_create.js b/app/assets/javascripts/protected_tags/protected_tag_create.js index 91bd140bd12..d1e4a75c17b 100644 --- a/app/assets/javascripts/protected_tags/protected_tag_create.js +++ b/app/assets/javascripts/protected_tags/protected_tag_create.js @@ -1,5 +1,5 @@ import ProtectedTagAccessDropdown from './protected_tag_access_dropdown'; -import ProtectedTagDropdown from './protected_tag_dropdown'; +import CreateItemDropdown from '../create_item_dropdown'; export default class ProtectedTagCreate { constructor() { @@ -24,9 +24,12 @@ export default class ProtectedTagCreate { $allowedToCreateDropdown.data('glDropdown').selectRowAtIndex(0); // Protected tag dropdown - this.protectedTagDropdown = new ProtectedTagDropdown({ + this.createItemDropdown = new CreateItemDropdown({ $dropdown: this.$form.find('.js-protected-tag-select'), + defaultToggleLabel: 'Protected Tag', + fieldName: 'protected_tag[name]', onSelect: this.onSelectCallback, + getData: ProtectedTagCreate.getProtectedTags, }); } @@ -38,4 +41,8 @@ export default class ProtectedTagCreate { this.$form.find('input[type="submit"]').attr('disabled', !($tagInput.val() && $allowedToCreateInput.length)); } + + static getProtectedTags(term, callback) { + callback(gon.open_tags); + } } diff --git a/app/assets/javascripts/protected_tags/protected_tag_dropdown.js b/app/assets/javascripts/protected_tags/protected_tag_dropdown.js deleted file mode 100644 index a0224213aa0..00000000000 --- a/app/assets/javascripts/protected_tags/protected_tag_dropdown.js +++ /dev/null @@ -1,88 +0,0 @@ -import _ from 'underscore'; - -export default class ProtectedTagDropdown { - /** - * @param {Object} options containing - * `$dropdown` target element - * `onSelect` event callback - * $dropdown must be an element created using `dropdown_tag()` rails helper - */ - constructor(options) { - this.onSelect = options.onSelect; - this.$dropdown = options.$dropdown; - this.$dropdownContainer = this.$dropdown.parent(); - this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer'); - this.$protectedTag = this.$dropdownContainer.find('.js-create-new-protected-tag'); - - this.buildDropdown(); - this.bindEvents(); - - // Hide footer - this.toggleFooter(true); - } - - buildDropdown() { - this.$dropdown.glDropdown({ - data: this.getProtectedTags.bind(this), - filterable: true, - remote: false, - search: { - fields: ['title'], - }, - selectable: true, - toggleLabel(selected) { - return (selected && 'id' in selected) ? selected.title : 'Protected Tag'; - }, - fieldName: 'protected_tag[name]', - text(protectedTag) { - return _.escape(protectedTag.title); - }, - id(protectedTag) { - return _.escape(protectedTag.id); - }, - onFilter: this.toggleCreateNewButton.bind(this), - clicked: (options) => { - options.e.preventDefault(); - this.onSelect(); - }, - }); - } - - bindEvents() { - this.$protectedTag.on('click', this.onClickCreateWildcard.bind(this)); - } - - onClickCreateWildcard(e) { - this.$dropdown.data('glDropdown').remote.execute(); - this.$dropdown.data('glDropdown').selectRowAtIndex(); - e.preventDefault(); - } - - getProtectedTags(term, callback) { - if (this.selectedTag) { - callback(gon.open_tags.concat(this.selectedTag)); - } else { - callback(gon.open_tags); - } - } - - toggleCreateNewButton(tagName) { - if (tagName) { - this.selectedTag = { - title: tagName, - id: tagName, - text: tagName, - }; - - this.$dropdownContainer - .find('.js-create-new-protected-tag code') - .text(tagName); - } - - this.toggleFooter(!tagName); - } - - toggleFooter(toggleState) { - this.$dropdownFooter.toggleClass('hidden', toggleState); - } -} diff --git a/app/assets/javascripts/registry/components/app.vue b/app/assets/javascripts/registry/components/app.vue index 2d8ca443ea7..ea0f7199a70 100644 --- a/app/assets/javascripts/registry/components/app.vue +++ b/app/assets/javascripts/registry/components/app.vue @@ -1,14 +1,17 @@ <script> - /* globals Flash */ import { mapGetters, mapActions } from 'vuex'; - import '../../flash'; + import Flash from '../../flash'; import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import store from '../stores'; import collapsibleContainer from './collapsible_container.vue'; import { errorMessages, errorMessagesTypes } from '../constants'; export default { - name: 'registryListApp', + name: 'RegistryListApp', + components: { + collapsibleContainer, + loadingIcon, + }, props: { endpoint: { type: String, @@ -16,22 +19,12 @@ }, }, store, - components: { - collapsibleContainer, - loadingIcon, - }, computed: { ...mapGetters([ 'isLoading', 'repos', ]), }, - methods: { - ...mapActions([ - 'setMainEndpoint', - 'fetchRepos', - ]), - }, created() { this.setMainEndpoint(this.endpoint); }, @@ -39,6 +32,12 @@ this.fetchRepos() .catch(() => Flash(errorMessages[errorMessagesTypes.FETCH_REPOS])); }, + methods: { + ...mapActions([ + 'setMainEndpoint', + 'fetchRepos', + ]), + }, }; </script> <template> @@ -46,17 +45,18 @@ <loading-icon v-if="isLoading" size="3" - /> + /> <collapsible-container v-else-if="!isLoading && repos.length" v-for="(item, index) in repos" :key="index" :repo="item" - /> + /> <p v-else-if="!isLoading && !repos.length"> - {{__("No container images stored for this project. Add one by following the instructions above.")}} + {{ __(`No container images stored for this project. +Add one by following the instructions above.`) }} </p> </div> </template> diff --git a/app/assets/javascripts/registry/components/collapsible_container.vue b/app/assets/javascripts/registry/components/collapsible_container.vue index ac1c3ec253c..b4906ba4ee5 100644 --- a/app/assets/javascripts/registry/components/collapsible_container.vue +++ b/app/assets/javascripts/registry/components/collapsible_container.vue @@ -1,7 +1,6 @@ <script> - /* globals Flash */ import { mapActions } from 'vuex'; - import '../../flash'; + import Flash from '../../flash'; import clipboardButton from '../../vue_shared/components/clipboard_button.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import tooltip from '../../vue_shared/directives/tooltip'; @@ -9,13 +8,7 @@ import { errorMessages, errorMessagesTypes } from '../constants'; export default { - name: 'collapsibeContainerRegisty', - props: { - repo: { - type: Object, - required: true, - }, - }, + name: 'CollapsibeContainerRegisty', components: { clipboardButton, loadingIcon, @@ -24,6 +17,12 @@ directives: { tooltip, }, + props: { + repo: { + type: Object, + required: true, + }, + }, data() { return { isOpen: false, @@ -65,28 +64,29 @@ <template> <div class="container-image"> - <div - class="container-image-head"> + <div class="container-image-head"> <button type="button" @click="toggleRepo" - class="js-toggle-repo btn-link"> + class="js-toggle-repo btn-link" + > <i class="fa" :class="{ 'fa-chevron-right': !isOpen, 'fa-chevron-up': isOpen, }" - aria-hidden="true"> + aria-hidden="true" + > </i> - {{repo.name}} + {{ repo.name }} </button> <clipboard-button v-if="repo.location" :text="clipboardText" :title="repo.location" - /> + /> <div class="controls hidden-xs pull-right"> <button @@ -96,35 +96,38 @@ :title="s__('ContainerRegistry|Remove repository')" :aria-label="s__('ContainerRegistry|Remove repository')" v-tooltip - @click="handleDeleteRepository"> + @click="handleDeleteRepository" + > <i class="fa fa-trash" - aria-hidden="true"> + aria-hidden="true" + > </i> </button> </div> - </div> <loading-icon v-if="repo.isLoading" class="append-bottom-20" size="2" - /> + /> <div v-else-if="!repo.isLoading && isOpen" - class="container-image-tags"> + class="container-image-tags" + > <table-registry v-if="repo.list.length" :repo="repo" - /> + /> <div v-else - class="nothing-here-block"> - {{s__("ContainerRegistry|No tags in Container Registry for this container image.")}} + class="nothing-here-block" + > + {{ s__("ContainerRegistry|No tags in Container Registry for this container image.") }} </div> </div> </div> diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue index 14d43e135fe..bef850eddc0 100644 --- a/app/assets/javascripts/registry/components/table_registry.vue +++ b/app/assets/javascripts/registry/components/table_registry.vue @@ -1,8 +1,7 @@ <script> - /* globals Flash */ import { mapActions } from 'vuex'; import { n__ } from '../../locale'; - import '../../flash'; + import Flash from '../../flash'; import clipboardButton from '../../vue_shared/components/clipboard_button.vue'; import tablePagination from '../../vue_shared/components/table_pagination.vue'; import tooltip from '../../vue_shared/directives/tooltip'; @@ -11,21 +10,21 @@ import { numberToHumanSize } from '../../lib/utils/number_utils'; export default { - props: { - repo: { - type: Object, - required: true, - }, - }, components: { clipboardButton, tablePagination, }, + directives: { + tooltip, + }, mixins: [ timeagoMixin, ], - directives: { - tooltip, + props: { + repo: { + type: Object, + required: true, + }, }, computed: { shouldRenderPagination() { @@ -68,75 +67,78 @@ }; </script> <template> -<div> - <table class="table tags"> - <thead> - <tr> - <th>{{s__('ContainerRegistry|Tag')}}</th> - <th>{{s__('ContainerRegistry|Tag ID')}}</th> - <th>{{s__("ContainerRegistry|Size")}}</th> - <th>{{s__("ContainerRegistry|Created")}}</th> - <th></th> - </tr> - </thead> - <tbody> - <tr - v-for="(item, i) in repo.list" - :key="i"> - <td> + <div> + <table class="table tags"> + <thead> + <tr> + <th>{{ s__('ContainerRegistry|Tag') }}</th> + <th>{{ s__('ContainerRegistry|Tag ID') }}</th> + <th>{{ s__("ContainerRegistry|Size") }}</th> + <th>{{ s__("ContainerRegistry|Created") }}</th> + <th></th> + </tr> + </thead> + <tbody> + <tr + v-for="(item, i) in repo.list" + :key="i"> + <td> - {{item.tag}} + {{ item.tag }} - <clipboard-button - v-if="item.location" - :title="item.location" - :text="clipboardText(item.location)" + <clipboard-button + v-if="item.location" + :title="item.location" + :text="clipboardText(item.location)" /> - </td> - <td> - <span - v-tooltip - :title="item.revision" - data-placement="bottom"> - {{item.shortRevision}} + </td> + <td> + <span + v-tooltip + :title="item.revision" + data-placement="bottom" + > + {{ item.shortRevision }} </span> - </td> - <td> - {{formatSize(item.size)}} - <template v-if="item.size && item.layers"> - · - </template> - {{layers(item)}} - </td> + </td> + <td> + {{ formatSize(item.size) }} + <template v-if="item.size && item.layers"> + · + </template> + {{ layers(item) }} + </td> - <td> - {{timeFormated(item.createdAt)}} - </td> + <td> + {{ timeFormated(item.createdAt) }} + </td> - <td class="content"> - <button - v-if="item.canDelete" - type="button" - class="js-delete-registry btn btn-danger hidden-xs pull-right" - :title="s__('ContainerRegistry|Remove tag')" - :aria-label="s__('ContainerRegistry|Remove tag')" - data-container="body" - v-tooltip - @click="handleDeleteRegistry(item)"> - <i - class="fa fa-trash" - aria-hidden="true"> - </i> - </button> - </td> - </tr> - </tbody> - </table> + <td class="content"> + <button + v-if="item.canDelete" + type="button" + class="js-delete-registry btn btn-danger hidden-xs pull-right" + :title="s__('ContainerRegistry|Remove tag')" + :aria-label="s__('ContainerRegistry|Remove tag')" + data-container="body" + v-tooltip + @click="handleDeleteRegistry(item)" + > + <i + class="fa fa-trash" + aria-hidden="true" + > + </i> + </button> + </td> + </tr> + </tbody> + </table> - <table-pagination - v-if="shouldRenderPagination" - :change="onPageChange" - :page-info="repo.pagination" + <table-pagination + v-if="shouldRenderPagination" + :change="onPageChange" + :page-info="repo.pagination" /> -</div> + </div> </template> diff --git a/app/assets/javascripts/render_math.js b/app/assets/javascripts/render_math.js index a759992cd54..73b6aafdd12 100644 --- a/app/assets/javascripts/render_math.js +++ b/app/assets/javascripts/render_math.js @@ -7,7 +7,12 @@ // // <code class="js-render-math"></div> // - // Only load once + +import { __ } from './locale'; +import axios from './lib/utils/axios_utils'; +import flash from './flash'; + +// Only load once let katexLoaded = false; // Loop over all math elements and render math @@ -18,7 +23,7 @@ function renderWithKaTeX(elements) { const display = $this.attr('data-math-style') === 'display'; try { - katex.render($this.text(), mathNode.get(0), { displayMode: display }); + katex.render($this.text(), mathNode.get(0), { displayMode: display, throwOnError: false }); mathNode.insertAfter($this); $this.remove(); } catch (err) { @@ -33,19 +38,26 @@ export default function renderMath($els) { if (katexLoaded) { renderWithKaTeX($els); } else { - $.get(gon.katex_css_url, () => { - const css = $('<link>', { - rel: 'stylesheet', - type: 'text/css', - href: gon.katex_css_url, - }); - css.appendTo('head'); - - // Load KaTeX js - $.getScript(gon.katex_js_url, () => { + axios.get(gon.katex_css_url) + .then(() => { + const css = $('<link>', { + rel: 'stylesheet', + type: 'text/css', + href: gon.katex_css_url, + }); + css.appendTo('head'); + }) + .then(() => axios.get(gon.katex_js_url, { + responseType: 'text', + })) + .then(({ data }) => { + // Add katex js to our document + $.globalEval(data); + }) + .then(() => { katexLoaded = true; renderWithKaTeX($els); // Run KaTeX - }); - }); + }) + .catch(() => flash(__('An error occurred while rendering KaTeX'))); } } diff --git a/app/assets/javascripts/render_mermaid.js b/app/assets/javascripts/render_mermaid.js index b7cde6fb092..31c7a772cf4 100644 --- a/app/assets/javascripts/render_mermaid.js +++ b/app/assets/javascripts/render_mermaid.js @@ -19,7 +19,11 @@ export default function renderMermaid($els) { import(/* webpackChunkName: 'mermaid' */ 'blackst0ne-mermaid').then((mermaid) => { mermaid.initialize({ - loadOnStart: false, + // mermaid core options + mermaid: { + startOnLoad: false, + }, + // mermaidAPI options theme: 'neutral', }); diff --git a/app/assets/javascripts/shared/milestones/form.js b/app/assets/javascripts/shared/milestones/form.js new file mode 100644 index 00000000000..db466f722c4 --- /dev/null +++ b/app/assets/javascripts/shared/milestones/form.js @@ -0,0 +1,9 @@ +import ZenMode from '../../zen_mode'; +import DueDateSelectors from '../../due_date_select'; +import GLForm from '../../gl_form'; + +export default (initGFM = true) => { + new ZenMode(); // eslint-disable-line no-new + new DueDateSelectors(); // eslint-disable-line no-new + new GLForm($('.milestone-form'), initGFM); // eslint-disable-line no-new +}; diff --git a/app/assets/javascripts/shared/sessions/u2f.js b/app/assets/javascripts/shared/sessions/u2f.js new file mode 100644 index 00000000000..1d075f7e872 --- /dev/null +++ b/app/assets/javascripts/shared/sessions/u2f.js @@ -0,0 +1,16 @@ +import U2FAuthenticate from '../../u2f/authenticate'; + +export default () => { + if (!gon.u2f) return; + + const u2fAuthenticate = new U2FAuthenticate( + $('#js-authenticate-u2f'), + '#js-login-u2f-form', + gon.u2f, + document.querySelector('#js-login-2fa-device'), + document.querySelector('.js-2fa-form'), + ); + u2fAuthenticate.start(); + // needed in rspec + gl.u2fAuthenticate = u2fAuthenticate; +}; diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js index d2f0d7410da..cd5ab53eace 100644 --- a/app/assets/javascripts/shortcuts.js +++ b/app/assets/javascripts/shortcuts.js @@ -13,12 +13,10 @@ Mousetrap.stopCallback = (e, element, combo) => { }; export default class Shortcuts { - constructor(skipResetBindings) { + constructor() { this.onToggleHelp = this.onToggleHelp.bind(this); this.enabledHelp = []; - if (!skipResetBindings) { - Mousetrap.reset(); - } + Mousetrap.bind('?', this.onToggleHelp); Mousetrap.bind('s', Shortcuts.focusSearch); Mousetrap.bind('f', this.focusFilter.bind(this)); @@ -62,7 +60,7 @@ export default class Shortcuts { e.preventDefault(); const performanceBarCookieName = 'perf_bar_enabled'; if (Cookies.get(performanceBarCookieName) === 'true') { - Cookies.remove(performanceBarCookieName, { path: '/' }); + Cookies.set(performanceBarCookieName, 'false', { path: '/' }); } else { Cookies.set(performanceBarCookieName, 'true', { path: '/' }); } diff --git a/app/assets/javascripts/shortcuts_blob.js b/app/assets/javascripts/shortcuts_blob.js index cf309be4f6f..908b9cab93d 100644 --- a/app/assets/javascripts/shortcuts_blob.js +++ b/app/assets/javascripts/shortcuts_blob.js @@ -1,4 +1,4 @@ -/* global Mousetrap */ +import Mousetrap from 'mousetrap'; import { getLocationHash, visitUrl } from './lib/utils/url_utility'; import Shortcuts from './shortcuts'; diff --git a/app/assets/javascripts/shortcuts_find_file.js b/app/assets/javascripts/shortcuts_find_file.js index 81286c0010c..1e246a56b85 100644 --- a/app/assets/javascripts/shortcuts_find_file.js +++ b/app/assets/javascripts/shortcuts_find_file.js @@ -1,5 +1,4 @@ -/* global Mousetrap */ - +import Mousetrap from 'mousetrap'; import ShortcutsNavigation from './shortcuts_navigation'; export default class ShortcutsFindFile extends ShortcutsNavigation { diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js index 292e3d6a657..689befc742e 100644 --- a/app/assets/javascripts/shortcuts_issuable.js +++ b/app/assets/javascripts/shortcuts_issuable.js @@ -1,12 +1,10 @@ -/* global Mousetrap */ - +import Mousetrap from 'mousetrap'; import _ from 'underscore'; -import 'mousetrap'; import Sidebar from './right_sidebar'; -import ShortcutsNavigation from './shortcuts_navigation'; +import Shortcuts from './shortcuts'; import { CopyAsGFM } from './behaviors/copy_as_gfm'; -export default class ShortcutsIssuable extends ShortcutsNavigation { +export default class ShortcutsIssuable extends Shortcuts { constructor(isMergeRequest) { super(); diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js index b4562701a3e..a4d10850471 100644 --- a/app/assets/javascripts/shortcuts_navigation.js +++ b/app/assets/javascripts/shortcuts_navigation.js @@ -1,5 +1,4 @@ -/* global Mousetrap */ - +import Mousetrap from 'mousetrap'; import findAndFollowLink from './shortcuts_dashboard_navigation'; import Shortcuts from './shortcuts'; diff --git a/app/assets/javascripts/shortcuts_network.js b/app/assets/javascripts/shortcuts_network.js index 21823085ac4..a88c280fa3b 100644 --- a/app/assets/javascripts/shortcuts_network.js +++ b/app/assets/javascripts/shortcuts_network.js @@ -1,4 +1,4 @@ -/* global Mousetrap */ +import Mousetrap from 'mousetrap'; import ShortcutsNavigation from './shortcuts_navigation'; export default class ShortcutsNetwork extends ShortcutsNavigation { diff --git a/app/assets/javascripts/shortcuts_wiki.js b/app/assets/javascripts/shortcuts_wiki.js index 59b967dbe09..41865dcf4ba 100644 --- a/app/assets/javascripts/shortcuts_wiki.js +++ b/app/assets/javascripts/shortcuts_wiki.js @@ -1,16 +1,14 @@ -/* eslint-disable class-methods-use-this */ -/* global Mousetrap */ - +import Mousetrap from 'mousetrap'; import ShortcutsNavigation from './shortcuts_navigation'; import findAndFollowLink from './shortcuts_dashboard_navigation'; export default class ShortcutsWiki extends ShortcutsNavigation { constructor() { super(); - Mousetrap.bind('e', this.editWiki); + Mousetrap.bind('e', ShortcutsWiki.editWiki); } - editWiki() { + static editWiki() { findAndFollowLink('.js-wiki-edit'); } } diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_title.js b/app/assets/javascripts/sidebar/components/assignees/assignee_title.js index 77f070d48cc..129ba2e4e89 100644 --- a/app/assets/javascripts/sidebar/components/assignees/assignee_title.js +++ b/app/assets/javascripts/sidebar/components/assignees/assignee_title.js @@ -39,7 +39,7 @@ export default { class="js-sidebar-dropdown-toggle edit-link pull-right" href="#" > - Edit + {{ __('Edit') }} </a> <a v-if="showToggle" diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.js b/app/assets/javascripts/sidebar/components/assignees/assignees.js index 7e5feac622c..643877b9d47 100644 --- a/app/assets/javascripts/sidebar/components/assignees/assignees.js +++ b/app/assets/javascripts/sidebar/components/assignees/assignees.js @@ -84,7 +84,7 @@ export default { return !this.showLess || (index < this.defaultRenderCount && this.showLess); }, avatarUrl(user) { - return user.avatar || user.avatar_url; + return user.avatar || user.avatar_url || gon.default_avatar_url; }, assigneeUrl(user) { return `${this.rootPath}${user.username}`; diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue index 6ee4d487c0b..02153fb86a5 100644 --- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue +++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue @@ -1,48 +1,51 @@ <script> -import Flash from '../../../flash'; -import editForm from './edit_form.vue'; -import Icon from '../../../vue_shared/components/icon.vue'; + import Flash from '../../../flash'; + import editForm from './edit_form.vue'; + import Icon from '../../../vue_shared/components/icon.vue'; -export default { - components: { - editForm, - Icon, - }, - props: { - isConfidential: { - required: true, - type: Boolean, + export default { + components: { + editForm, + Icon, }, - isEditable: { - required: true, - type: Boolean, + props: { + isConfidential: { + required: true, + type: Boolean, + }, + isEditable: { + required: true, + type: Boolean, + }, + service: { + required: true, + type: Object, + }, }, - service: { - required: true, - type: Object, + data() { + return { + edit: false, + }; }, - }, - data() { - return { - edit: false, - }; - }, - computed: { - confidentialityIcon() { - return this.isConfidential ? 'eye-slash' : 'eye'; + computed: { + confidentialityIcon() { + return this.isConfidential ? 'eye-slash' : 'eye'; + }, }, - }, - methods: { - toggleForm() { - this.edit = !this.edit; + methods: { + toggleForm() { + this.edit = !this.edit; + }, + updateConfidentialAttribute(confidential) { + this.service.update('issue', { confidential }) + .then(() => location.reload()) + .catch(() => { + Flash(`Something went wrong trying to + change the confidentiality of this issue`); + }); + }, }, - updateConfidentialAttribute(confidential) { - this.service.update('issue', { confidential }) - .then(() => location.reload()) - .catch(() => new Flash('Something went wrong trying to change the confidentiality of this issue')); - }, - }, -}; + }; </script> <template> @@ -51,8 +54,8 @@ export default { <icon :name="confidentialityIcon" :size="16" - aria-hidden="true"> - </icon> + aria-hidden="true" + /> </div> <div class="title hide-collapsed"> Confidentiality @@ -62,7 +65,7 @@ export default { href="#" @click.prevent="toggleForm" > - Edit + {{ __('Edit') }} </a> </div> <div class="value sidebar-item-value hide-collapsed"> @@ -72,22 +75,26 @@ export default { :is-confidential="isConfidential" :update-confidential-attribute="updateConfidentialAttribute" /> - <div v-if="!isConfidential" class="no-value sidebar-item-value"> + <div + v-if="!isConfidential" + class="no-value sidebar-item-value"> <icon name="eye" :size="16" aria-hidden="true" - class="sidebar-item-icon inline"> - </icon> + class="sidebar-item-icon inline" + /> Not confidential </div> - <div v-else class="value sidebar-item-value hide-collapsed"> + <div + v-else + class="value sidebar-item-value hide-collapsed"> <icon name="eye-slash" :size="16" aria-hidden="true" - class="sidebar-item-icon inline is-active"> - </icon> + class="sidebar-item-icon inline is-active" + /> This issue is confidential </div> </div> diff --git a/app/assets/javascripts/sidebar/components/confidential/edit_form.vue b/app/assets/javascripts/sidebar/components/confidential/edit_form.vue index dd17b5abd46..6a81235a1a7 100644 --- a/app/assets/javascripts/sidebar/components/confidential/edit_form.vue +++ b/app/assets/javascripts/sidebar/components/confidential/edit_form.vue @@ -1,26 +1,25 @@ <script> -import editFormButtons from './edit_form_buttons.vue'; + import editFormButtons from './edit_form_buttons.vue'; -export default { - props: { - isConfidential: { - required: true, - type: Boolean, + export default { + components: { + editFormButtons, }, - toggleForm: { - required: true, - type: Function, + props: { + isConfidential: { + required: true, + type: Boolean, + }, + toggleForm: { + required: true, + type: Function, + }, + updateConfidentialAttribute: { + required: true, + type: Function, + }, }, - updateConfidentialAttribute: { - required: true, - type: Function, - }, - }, - - components: { - editFormButtons, - }, -}; + }; </script> <template> diff --git a/app/assets/javascripts/sidebar/components/lock/edit_form.vue b/app/assets/javascripts/sidebar/components/lock/edit_form.vue index 242e826d471..e7a87636aa7 100644 --- a/app/assets/javascripts/sidebar/components/lock/edit_form.vue +++ b/app/assets/javascripts/sidebar/components/lock/edit_form.vue @@ -1,45 +1,47 @@ <script> -import editFormButtons from './edit_form_buttons.vue'; -import issuableMixin from '../../../vue_shared/mixins/issuable'; + import editFormButtons from './edit_form_buttons.vue'; + import issuableMixin from '../../../vue_shared/mixins/issuable'; -export default { - props: { - isLocked: { - required: true, - type: Boolean, + export default { + components: { + editFormButtons, }, - - toggleForm: { - required: true, - type: Function, - }, - - updateLockedAttribute: { - required: true, - type: Function, + mixins: [ + issuableMixin, + ], + props: { + isLocked: { + required: true, + type: Boolean, + }, + + toggleForm: { + required: true, + type: Function, + }, + + updateLockedAttribute: { + required: true, + type: Function, + }, }, - }, - - mixins: [ - issuableMixin, - ], - - components: { - editFormButtons, - }, -}; + }; </script> <template> <div class="dropdown open"> <div class="dropdown-menu sidebar-item-warning-message"> - <p class="text" v-if="isLocked"> + <p + class="text" + v-if="isLocked"> Unlock this {{ issuableDisplayName }}? <strong>Everyone</strong> will be able to comment. </p> - <p class="text" v-else> + <p + class="text" + v-else> Lock this {{ issuableDisplayName }}? Only <strong>project members</strong> diff --git a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue index 04c3a96bf74..02876a6c175 100644 --- a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue +++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue @@ -1,63 +1,63 @@ <script> -/* global Flash */ -import editForm from './edit_form.vue'; -import issuableMixin from '../../../vue_shared/mixins/issuable'; -import Icon from '../../../vue_shared/components/icon.vue'; + import Flash from '../../../flash'; + import editForm from './edit_form.vue'; + import issuableMixin from '../../../vue_shared/mixins/issuable'; + import Icon from '../../../vue_shared/components/icon.vue'; -export default { - props: { - isLocked: { - required: true, - type: Boolean, + export default { + components: { + editForm, + Icon, }, + mixins: [ + issuableMixin, + ], - isEditable: { - required: true, - type: Boolean, - }, - - mediator: { - required: true, - type: Object, - validator(mediatorObject) { - return mediatorObject.service && mediatorObject.service.update && mediatorObject.store; + props: { + isLocked: { + required: true, + type: Boolean, }, - }, - }, - - mixins: [ - issuableMixin, - ], - components: { - editForm, - Icon, - }, + isEditable: { + required: true, + type: Boolean, + }, - computed: { - lockIcon() { - return this.isLocked ? 'lock' : 'lock-open'; + mediator: { + required: true, + type: Object, + validator(mediatorObject) { + return mediatorObject.service && mediatorObject.service.update && mediatorObject.store; + }, + }, }, - isLockDialogOpen() { - return this.mediator.store.isLockDialogOpen; - }, - }, + computed: { + lockIcon() { + return this.isLocked ? 'lock' : 'lock-open'; + }, - methods: { - toggleForm() { - this.mediator.store.isLockDialogOpen = !this.mediator.store.isLockDialogOpen; + isLockDialogOpen() { + return this.mediator.store.isLockDialogOpen; + }, }, - updateLockedAttribute(locked) { - this.mediator.service.update(this.issuableType, { - discussion_locked: locked, - }) - .then(() => location.reload()) - .catch(() => Flash(this.__(`Something went wrong trying to change the locked state of this ${this.issuableDisplayName}`))); + methods: { + toggleForm() { + this.mediator.store.isLockDialogOpen = !this.mediator.store.isLockDialogOpen; + }, + + updateLockedAttribute(locked) { + this.mediator.service.update(this.issuableType, { + discussion_locked: locked, + }) + .then(() => location.reload()) + .catch(() => Flash(this.__(`Something went wrong trying to + change the locked state of this ${this.issuableDisplayName}`))); + }, }, - }, -}; + }; </script> <template> @@ -67,8 +67,8 @@ export default { :name="lockIcon" :size="16" aria-hidden="true" - class="sidebar-item-icon is-active"> - </icon> + class="sidebar-item-icon is-active" + /> </div> <div class="title hide-collapsed"> @@ -100,8 +100,8 @@ export default { name="lock" :size="16" aria-hidden="true" - class="sidebar-item-icon inline is-active"> - </icon> + class="sidebar-item-icon inline is-active" + /> {{ __('Locked') }} </div> @@ -113,8 +113,8 @@ export default { name="lock-open" :size="16" aria-hidden="true" - class="sidebar-item-icon inline"> - </icon> + class="sidebar-item-icon inline" + /> {{ __('Unlocked') }} </div> </div> diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue index b8510a6ce3a..006a6d2905d 100644 --- a/app/assets/javascripts/sidebar/components/participants/participants.vue +++ b/app/assets/javascripts/sidebar/components/participants/participants.vue @@ -1,73 +1,73 @@ <script> -import { __, n__, sprintf } from '../../../locale'; -import loadingIcon from '../../../vue_shared/components/loading_icon.vue'; -import userAvatarImage from '../../../vue_shared/components/user_avatar/user_avatar_image.vue'; + import { __, n__, sprintf } from '../../../locale'; + import loadingIcon from '../../../vue_shared/components/loading_icon.vue'; + import userAvatarImage from '../../../vue_shared/components/user_avatar/user_avatar_image.vue'; -export default { - props: { - loading: { - type: Boolean, - required: false, - default: false, + export default { + components: { + loadingIcon, + userAvatarImage, }, - participants: { - type: Array, - required: false, - default: () => [], + props: { + loading: { + type: Boolean, + required: false, + default: false, + }, + participants: { + type: Array, + required: false, + default: () => [], + }, + numberOfLessParticipants: { + type: Number, + required: false, + default: 7, + }, }, - numberOfLessParticipants: { - type: Number, - required: false, - default: 7, + data() { + return { + isShowingMoreParticipants: false, + }; }, - }, - data() { - return { - isShowingMoreParticipants: false, - }; - }, - components: { - loadingIcon, - userAvatarImage, - }, - computed: { - lessParticipants() { - return this.participants.slice(0, this.numberOfLessParticipants); - }, - visibleParticipants() { - return this.isShowingMoreParticipants ? this.participants : this.lessParticipants; - }, - hasMoreParticipants() { - return this.participants.length > this.numberOfLessParticipants; - }, - toggleLabel() { - let label = ''; - if (this.isShowingMoreParticipants) { - label = __('- show less'); - } else { - label = sprintf(__('+ %{moreCount} more'), { - moreCount: this.participants.length - this.numberOfLessParticipants, - }); - } + computed: { + lessParticipants() { + return this.participants.slice(0, this.numberOfLessParticipants); + }, + visibleParticipants() { + return this.isShowingMoreParticipants ? this.participants : this.lessParticipants; + }, + hasMoreParticipants() { + return this.participants.length > this.numberOfLessParticipants; + }, + toggleLabel() { + let label = ''; + if (this.isShowingMoreParticipants) { + label = __('- show less'); + } else { + label = sprintf(__('+ %{moreCount} more'), { + moreCount: this.participants.length - this.numberOfLessParticipants, + }); + } - return label; - }, - participantLabel() { - return sprintf( - n__('%{count} participant', '%{count} participants', this.participants.length), - { count: this.loading ? '' : this.participantCount }, - ); - }, - participantCount() { - return this.participants.length; + return label; + }, + participantLabel() { + return sprintf( + n__('%{count} participant', '%{count} participants', this.participants.length), + { count: this.loading ? '' : this.participantCount }, + ); + }, + participantCount() { + return this.participants.length; + }, }, - }, - methods: { - toggleMoreParticipants() { - this.isShowingMoreParticipants = !this.isShowingMoreParticipants; + methods: { + toggleMoreParticipants() { + this.isShowingMoreParticipants = !this.isShowingMoreParticipants; + }, }, - }, -}; + }; </script> <template> @@ -75,14 +75,17 @@ export default { <div class="sidebar-collapsed-icon"> <i class="fa fa-users" - aria-hidden="true"> + aria-hidden="true" + > </i> <loading-icon v-if="loading" - class="js-participants-collapsed-loading-icon" /> + class="js-participants-collapsed-loading-icon" + /> <span v-else - class="js-participants-collapsed-count"> + class="js-participants-collapsed-count" + > {{ participantCount }} </span> </div> @@ -90,34 +93,40 @@ export default { <loading-icon v-if="loading" :inline="true" - class="js-participants-expanded-loading-icon" /> + class="js-participants-expanded-loading-icon" + /> {{ participantLabel }} </div> <div class="participants-list hide-collapsed"> <div v-for="participant in visibleParticipants" :key="participant.id" - class="participants-author js-participants-author"> + class="participants-author js-participants-author" + > <a class="author_link" - :href="participant.web_url"> + :href="participant.web_url" + > <user-avatar-image :lazy="true" :img-src="participant.avatar_url" css-classes="avatar-inline" :size="24" :tooltip-text="participant.name" - tooltip-placement="bottom" /> + tooltip-placement="bottom" + /> </a> </div> </div> <div v-if="hasMoreParticipants" - class="participants-more hide-collapsed"> + class="participants-more hide-collapsed" + > <button type="button" class="btn-transparent btn-blank js-toggle-participants-button" - @click="toggleMoreParticipants"> + @click="toggleMoreParticipants" + > {{ toggleLabel }} </button> </div> diff --git a/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue b/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue index 6fcd2f95309..5c1ead1a8ac 100644 --- a/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue +++ b/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue @@ -1,23 +1,23 @@ <script> -import Store from '../../stores/sidebar_store'; -import participants from './participants.vue'; + import Store from '../../stores/sidebar_store'; + import participants from './participants.vue'; -export default { - data() { - return { - store: new Store(), - }; - }, - props: { - mediator: { - type: Object, - required: true, + export default { + components: { + participants, }, - }, - components: { - participants, - }, -}; + props: { + mediator: { + type: Object, + required: true, + }, + }, + data() { + return { + store: new Store(), + }; + }, + }; </script> <template> diff --git a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue index f4bae1d3dd5..3e8cc7a6630 100644 --- a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue +++ b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue @@ -6,10 +6,8 @@ import { __ } from '../../../locale'; import subscriptions from './subscriptions.vue'; export default { - data() { - return { - store: new Store(), - }; + components: { + subscriptions, }, props: { mediator: { @@ -17,10 +15,17 @@ export default { required: true, }, }, - components: { - subscriptions, + data() { + return { + store: new Store(), + }; + }, + created() { + eventHub.$on('toggleSubscription', this.onToggleSubscription); + }, + beforeDestroy() { + eventHub.$off('toggleSubscription', this.onToggleSubscription); }, - methods: { onToggleSubscription() { this.mediator.toggleSubscription() @@ -29,14 +34,6 @@ export default { }); }, }, - - created() { - eventHub.$on('toggleSubscription', this.onToggleSubscription); - }, - - beforeDestroy() { - eventHub.$off('toggleSubscription', this.onToggleSubscription); - }, }; </script> @@ -44,6 +41,7 @@ export default { <div class="block subscriptions"> <subscriptions :loading="store.isFetching.subscriptions" - :subscribed="store.subscribed" /> + :subscribed="store.subscribed" + /> </div> </template> diff --git a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue index 940e1764f3d..d69d100a26c 100644 --- a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue +++ b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue @@ -1,64 +1,85 @@ <script> -import { __ } from '../../../locale'; -import eventHub from '../../event_hub'; -import loadingButton from '../../../vue_shared/components/loading_button.vue'; + import { __ } from '~/locale'; + import icon from '~/vue_shared/components/icon.vue'; + import toggleButton from '~/vue_shared/components/toggle_button.vue'; + import tooltip from '~/vue_shared/directives/tooltip'; + import eventHub from '../../event_hub'; -export default { - props: { - loading: { - type: Boolean, - required: false, - default: false, + const ICON_ON = 'notifications'; + const ICON_OFF = 'notifications-off'; + const LABEL_ON = __('Notifications on'); + const LABEL_OFF = __('Notifications off'); + + export default { + directives: { + tooltip, }, - subscribed: { - type: Boolean, - required: false, + components: { + icon, + toggleButton, }, - id: { - type: Number, - required: false, + props: { + loading: { + type: Boolean, + required: false, + default: false, + }, + subscribed: { + type: Boolean, + required: false, + default: null, + }, + id: { + type: Number, + required: false, + default: null, + }, }, - }, - components: { - loadingButton, - }, - computed: { - buttonLabel() { - let label; - if (this.subscribed === false) { - label = __('Subscribe'); - } else if (this.subscribed === true) { - label = __('Unsubscribe'); - } - - return label; + computed: { + showLoadingState() { + return this.subscribed === null; + }, + notificationIcon() { + return this.subscribed ? ICON_ON : ICON_OFF; + }, + notificationTooltip() { + return this.subscribed ? LABEL_ON : LABEL_OFF; + }, }, - }, - methods: { - toggleSubscription() { - eventHub.$emit('toggleSubscription', this.id); + methods: { + toggleSubscription() { + eventHub.$emit('toggleSubscription', this.id); + }, }, - }, -}; + }; </script> <template> <div> <div class="sidebar-collapsed-icon"> - <i - class="fa fa-rss" - aria-hidden="true"> - </i> + <span + v-tooltip + :title="notificationTooltip" + data-container="body" + data-placement="left" + > + <icon + :name="notificationIcon" + :size="16" + aria-hidden="true" + class="sidebar-item-icon is-active" + /> + </span> </div> <span class="issuable-header-text hide-collapsed pull-left"> {{ __('Notifications') }} </span> - <loading-button - ref="loadingButton" - class="btn btn-default pull-right hide-collapsed js-issuable-subscribe-button" - :loading="loading" - :label="buttonLabel" - @click="toggleSubscription" + <toggle-button + ref="toggleButton" + class="pull-right hide-collapsed js-issuable-subscribe-button" + :is-loading="showLoadingState" + :value="subscribed" + @change="toggleSubscription" /> </div> </template> diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js index 95e51bc4e7a..48dd91bdf06 100644 --- a/app/assets/javascripts/single_file_diff.js +++ b/app/assets/javascripts/single_file_diff.js @@ -1,5 +1,8 @@ /* 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 './locale'; +import axios from './lib/utils/axios_utils'; +import createFlash from './flash'; import FilesCommentButton from './files_comment_button'; import imageDiffHelper from './image_diff/helpers/index'; import syntaxHighlight from './syntax_highlight'; @@ -60,30 +63,31 @@ export default class SingleFileDiff { getContentHTML(cb) { this.collapsedContent.hide(); this.loadingContent.show(); - $.get(this.diffForPath, (function(_this) { - return function(data) { - _this.loadingContent.hide(); + + axios.get(this.diffForPath) + .then(({ data }) => { + this.loadingContent.hide(); if (data.html) { - _this.content = $(data.html); - syntaxHighlight(_this.content); + this.content = $(data.html); + syntaxHighlight(this.content); } else { - _this.hasError = true; - _this.content = $(ERROR_HTML); + this.hasError = true; + this.content = $(ERROR_HTML); } - _this.collapsedContent.after(_this.content); + this.collapsedContent.after(this.content); if (typeof gl.diffNotesCompileComponents !== 'undefined') { gl.diffNotesCompileComponents(); } - const $file = $(_this.file); + const $file = $(this.file); FilesCommentButton.init($file); const canCreateNote = $file.closest('.files').is('[data-can-create-note]'); imageDiffHelper.initImageDiff($file[0], canCreateNote); if (cb) cb(); - }; - })(this)); + }) + .catch(createFlash(__('An error occurred while retrieving diff'))); } } diff --git a/app/assets/javascripts/templates/issuable_template_selector.js b/app/assets/javascripts/templates/issuable_template_selector.js index 8e167f5bf08..4cc1c96b870 100644 --- a/app/assets/javascripts/templates/issuable_template_selector.js +++ b/app/assets/javascripts/templates/issuable_template_selector.js @@ -32,8 +32,8 @@ export default class IssuableTemplateSelector extends TemplateSelector { this.startLoadingSpinner(); Api.issueTemplate(this.namespacePath, this.projectPath, query.name, this.issuableType, (err, currentTemplate) => { this.currentTemplate = currentTemplate; - if (err) return; // Error handled by global AJAX error handler this.stopLoadingSpinner(); + if (err) return; // Error handled by global AJAX error handler this.setInputValueToTemplateContent(); }); return; diff --git a/app/assets/javascripts/toggle_buttons.js b/app/assets/javascripts/toggle_buttons.js new file mode 100644 index 00000000000..974dc3ee052 --- /dev/null +++ b/app/assets/javascripts/toggle_buttons.js @@ -0,0 +1,61 @@ +import $ from 'jquery'; +import Flash from './flash'; +import { __ } from './locale'; +import { convertPermissionToBoolean } from './lib/utils/common_utils'; + +/* + example HAML: + ``` + %button.js-project-feature-toggle.project-feature-toggle{ type: "button", + class: "#{'is-checked' if enabled?}", + 'aria-label': _('Toggle Cluster') } + %input{ type: "hidden", class: 'js-project-feature-toggle-input', value: enabled? } + ``` +*/ + +function updatetoggle(toggle, isOn) { + toggle.classList.toggle('is-checked', isOn); +} + +function onToggleClicked(toggle, input, clickCallback) { + const previousIsOn = convertPermissionToBoolean(input.value); + + // Visually change the toggle and start loading + updatetoggle(toggle, !previousIsOn); + toggle.setAttribute('disabled', true); + toggle.classList.toggle('is-loading', true); + + Promise.resolve(clickCallback(!previousIsOn, toggle)) + .then(() => { + // Actually change the input value + input.setAttribute('value', !previousIsOn); + }) + .catch(() => { + // Revert the visuals if something goes wrong + updatetoggle(toggle, previousIsOn); + }) + .then(() => { + // Remove the loading indicator in any case + toggle.removeAttribute('disabled'); + toggle.classList.toggle('is-loading', false); + + $(input).trigger('trigger-change'); + }) + .catch(() => { + Flash(__('Something went wrong when toggling the button')); + }); +} + +export default function setupToggleButtons(container, clickCallback = () => {}) { + const toggles = container.querySelectorAll('.js-project-feature-toggle'); + + toggles.forEach((toggle) => { + const input = toggle.querySelector('.js-project-feature-toggle-input'); + const isOn = convertPermissionToBoolean(input.value); + + // Get the visible toggle in sync with the hidden input + updatetoggle(toggle, isOn); + + toggle.addEventListener('click', onToggleClicked.bind(null, toggle, input, clickCallback)); + }); +} diff --git a/app/assets/javascripts/usage_ping.js b/app/assets/javascripts/usage_ping.js deleted file mode 100644 index 2389056bd02..00000000000 --- a/app/assets/javascripts/usage_ping.js +++ /dev/null @@ -1,12 +0,0 @@ -export default function UsagePing() { - const usageDataUrl = $('.usage-data').data('endpoint'); - - $.ajax({ - type: 'GET', - url: usageDataUrl, - dataType: 'html', - success(html) { - $('.usage-data').html(html); - }, - }); -} diff --git a/app/assets/javascripts/users/activity_calendar.js b/app/assets/javascripts/users/activity_calendar.js index 0581239d5a5..57306322aa4 100644 --- a/app/assets/javascripts/users/activity_calendar.js +++ b/app/assets/javascripts/users/activity_calendar.js @@ -1,7 +1,10 @@ import _ from 'underscore'; import { scaleLinear, scaleThreshold } from 'd3-scale'; import { select } from 'd3-selection'; -import { getDayName, getDayDifference } from '../lib/utils/datetime_utility'; +import { getDayName, getDayDifference } from '~/lib/utils/datetime_utility'; +import axios from '~/lib/utils/axios_utils'; +import flash from '~/flash'; +import { __ } from '~/locale'; const d3 = { select, scaleLinear, scaleThreshold }; @@ -98,7 +101,7 @@ export default class ActivityCalendar { const secondLastColMonth = this.timestampsTmp[group - 2][0].date.getMonth(); if (lastColMonth !== secondLastColMonth) { - extraWidthPadding = 3; + extraWidthPadding = 6; } return extraWidthPadding; @@ -221,14 +224,16 @@ export default class ActivityCalendar { this.currentSelectedDate.getDate(), ].join('-'); - $.ajax({ - url: this.calendarActivitiesPath, - data: { date }, - cache: false, - dataType: 'html', - beforeSend: () => $('.user-calendar-activities').html(LOADING_HTML), - success: data => $('.user-calendar-activities').html(data), - }); + $('.user-calendar-activities').html(LOADING_HTML); + + axios.get(this.calendarActivitiesPath, { + params: { + date, + }, + responseType: 'text', + }) + .then(({ data }) => $('.user-calendar-activities').html(data)) + .catch(() => flash(__('An error occurred while retrieving calendar activity'))); } else { this.currentSelectedDate = ''; $('.user-calendar-activities').html(''); diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index f249bd036d6..ab108906732 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -492,7 +492,7 @@ function UsersSelect(currentUser, els, options = {}) { renderRow: function(user) { var avatar, img, listClosingTags, listWithName, listWithUserName, username; username = user.username ? "@" + user.username : ""; - avatar = user.avatar_url ? user.avatar_url : false; + avatar = user.avatar_url ? user.avatar_url : gon.default_avatar_url; let selected = false; @@ -513,9 +513,7 @@ function UsersSelect(currentUser, els, options = {}) { if (user.beforeDivider != null) { `<li><a href='#' class='${selected === true ? 'is-active' : ''}'>${_.escape(user.name)}</a></li>`; } else { - if (avatar) { - img = "<img src='" + avatar + "' class='avatar avatar-inline' width='32' />"; - } + img = "<img src='" + avatar + "' class='avatar avatar-inline' width='32' />"; } return ` diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.js deleted file mode 100644 index 982b5e8e373..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.js +++ /dev/null @@ -1,28 +0,0 @@ -import tooltip from '../../vue_shared/directives/tooltip'; - -export default { - name: 'MRWidgetAuthor', - props: { - author: { type: Object, required: true }, - showAuthorName: { type: Boolean, required: false, default: true }, - showAuthorTooltip: { type: Boolean, required: false, default: false }, - }, - directives: { - tooltip, - }, - template: ` - <a - :href="author.webUrl || author.web_url" - class="author-link inline" - :v-tooltip="showAuthorTooltip" - :title="author.name"> - <img - :src="author.avatarUrl || author.avatar_url" - class="avatar avatar-inline s16" /> - <span - v-if="showAuthorName" - class="author">{{author.name}} - </span> - </a> - `, -}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue new file mode 100644 index 00000000000..7ac9eadcde0 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue @@ -0,0 +1,33 @@ +<script> + export default { + name: 'MRWidgetAuthor', + props: { + author: { + type: Object, + required: true, + }, + }, + computed: { + authorUrl() { + return this.author.webUrl || this.author.web_url; + }, + avatarUrl() { + return this.author.avatarUrl || this.author.avatar_url; + }, + }, + }; +</script> +<template> + <a + :href="authorUrl" + class="author-link inline" + > + <img + :src="avatarUrl" + class="avatar avatar-inline s16" + /> + <span class="author"> + {{ author.name }} + </span> + </a> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.js deleted file mode 100644 index 6d2ed5fda64..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.js +++ /dev/null @@ -1,27 +0,0 @@ -import MRWidgetAuthor from './mr_widget_author'; - -export default { - name: 'MRWidgetAuthorTime', - props: { - actionText: { type: String, required: true }, - author: { type: Object, required: true }, - dateTitle: { type: String, required: true }, - dateReadable: { type: String, required: true }, - }, - components: { - 'mr-widget-author': MRWidgetAuthor, - }, - template: ` - <h4 class="js-mr-widget-author"> - {{actionText}} - <mr-widget-author :author="author" /> - <time - :title="dateTitle" - data-toggle="tooltip" - data-placement="top" - data-container="body"> - {{dateReadable}} - </time> - </h4> - `, -}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue new file mode 100644 index 00000000000..8f1fd809a81 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue @@ -0,0 +1,42 @@ +<script> + import mrWidgetAuthor from './mr_widget_author.vue'; + + export default { + name: 'MRWidgetAuthorTime', + components: { + mrWidgetAuthor, + }, + props: { + actionText: { + type: String, + required: true, + }, + author: { + type: Object, + required: true, + }, + dateTitle: { + type: String, + required: true, + }, + dateReadable: { + type: String, + required: true, + }, + }, + }; +</script> +<template> + <h4 class="js-mr-widget-author"> + {{ actionText }} + <mr-widget-author :author="author" /> + <time + :title="dateTitle" + data-toggle="tooltip" + data-placement="top" + data-container="body" + > + {{ dateReadable }} + </time> + </h4> +</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 index d48f3a01420..d174a900f63 100644 --- 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 @@ -2,7 +2,7 @@ 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'; +import StatusIcon from './mr_widget_status_icon.vue'; import MRWidgetService from '../services/mr_widget_service'; export default { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js deleted file mode 100644 index 85bfd03a3cf..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js +++ /dev/null @@ -1,116 +0,0 @@ -import tooltip from '../../vue_shared/directives/tooltip'; -import { pluralize } from '../../lib/utils/text_utility'; -import icon from '../../vue_shared/components/icon.vue'; - -export default { - name: 'MRWidgetHeader', - props: { - mr: { type: Object, required: true }, - }, - directives: { - tooltip, - }, - components: { - icon, - }, - computed: { - shouldShowCommitsBehindText() { - return this.mr.divergedCommitsCount > 0; - }, - commitsText() { - return pluralize('commit', this.mr.divergedCommitsCount); - }, - branchNameClipboardData() { - // This supports code in app/assets/javascripts/copy_to_clipboard.js that - // works around ClipboardJS limitations to allow the context-specific - // copy/pasting of plain text or GFM. - return JSON.stringify({ - text: this.mr.sourceBranch, - gfm: `\`${this.mr.sourceBranch}\``, - }); - }, - }, - methods: { - isBranchTitleLong(branchTitle) { - return branchTitle.length > 32; - }, - }, - template: ` - <div class="mr-source-target"> - <div class="normal"> - <strong> - Request to merge - <span - class="label-branch" - :class="{'label-truncated': isBranchTitleLong(mr.sourceBranch)}" - :title="isBranchTitleLong(mr.sourceBranch) ? mr.sourceBranch : ''" - data-placement="bottom" - :v-tooltip="isBranchTitleLong(mr.sourceBranch)" - v-html="mr.sourceBranchLink"></span> - <button - v-tooltip - class="btn btn-transparent btn-clipboard" - data-title="Copy branch name to clipboard" - :data-clipboard-text="branchNameClipboardData"> - <i - aria-hidden="true" - class="fa fa-clipboard"></i> - </button> - into - <span - class="label-branch" - :v-tooltip="isBranchTitleLong(mr.sourceBranch)" - :class="{'label-truncatedtooltip': isBranchTitleLong(mr.targetBranch)}" - :title="isBranchTitleLong(mr.targetBranch) ? mr.targetBranch : ''" - data-placement="bottom"> - <a :href="mr.targetBranchTreePath">{{mr.targetBranch}}</a> - </span> - </strong> - <span - v-if="shouldShowCommitsBehindText" - class="diverged-commits-count"> - (<a :href="mr.targetBranchPath">{{mr.divergedCommitsCount}} {{commitsText}} behind</a>) - </span> - </div> - <div v-if="mr.isOpen"> - <a - href="#modal_merge_info" - data-toggle="modal" - class="btn btn-sm inline"> - Check out branch - </a> - <span class="dropdown prepend-left-10"> - <a - class="btn btn-sm inline dropdown-toggle" - data-toggle="dropdown" - aria-label="Download as" - role="button"> - <icon - name="download"> - </icon> - <i - class="fa fa-caret-down" - aria-hidden="true"> - </i> - </a> - <ul class="dropdown-menu dropdown-menu-align-right"> - <li> - <a - :href="mr.emailPatchesPath" - download> - Email patches - </a> - </li> - <li> - <a - :href="mr.plainDiffPath" - download> - Plain diff - </a> - </li> - </ul> - </span> - </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 new file mode 100644 index 00000000000..18a3787857d --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue @@ -0,0 +1,145 @@ +<script> + import tooltip from '~/vue_shared/directives/tooltip'; + import { n__ } from '~/locale'; + import icon from '~/vue_shared/components/icon.vue'; + import clipboardButton from '~/vue_shared/components/clipboard_button.vue'; + + export default { + name: 'MRWidgetHeader', + directives: { + tooltip, + }, + components: { + icon, + clipboardButton, + }, + props: { + mr: { + type: Object, + required: true, + }, + }, + computed: { + shouldShowCommitsBehindText() { + return this.mr.divergedCommitsCount > 0; + }, + commitsText() { + return n__('%d commit behind', '%d commits behind', this.mr.divergedCommitsCount); + }, + branchNameClipboardData() { + // This supports code in app/assets/javascripts/copy_to_clipboard.js that + // works around ClipboardJS limitations to allow the context-specific + // copy/pasting of plain text or GFM. + return JSON.stringify({ + text: this.mr.sourceBranch, + gfm: `\`${this.mr.sourceBranch}\``, + }); + }, + isSourceBranchLong() { + return this.isBranchTitleLong(this.mr.sourceBranch); + }, + isTargetBranchLong() { + return this.isBranchTitleLong(this.mr.targetBranch); + }, + }, + methods: { + isBranchTitleLong(branchTitle) { + return branchTitle.length > 32; + }, + }, + }; +</script> +<template> + <div class="mr-source-target"> + <div class="normal"> + <strong> + {{ s__("mrWidget|Request to merge") }} + <span + class="label-branch js-source-branch" + :class="{ 'label-truncated': isSourceBranchLong }" + :title="isSourceBranchLong ? mr.sourceBranch : ''" + data-placement="bottom" + :v-tooltip="isSourceBranchLong" + v-html="mr.sourceBranchLink" + > + </span> + + <clipboard-button + :text="branchNameClipboardData" + :title="__('Copy branch name to clipboard')" + /> + + {{ s__("mrWidget|into") }} + + <span + class="label-branch" + :v-tooltip="isTargetBranchLong" + :class="{ 'label-truncatedtooltip': isTargetBranchLong }" + :title="isTargetBranchLong ? mr.targetBranch : ''" + data-placement="bottom" + > + <a + :href="mr.targetBranchTreePath" + class="js-target-branch" + > + {{ mr.targetBranch }} + </a> + </span> + </strong> + <span + v-if="shouldShowCommitsBehindText" + class="diverged-commits-count" + > + (<a :href="mr.targetBranchPath">{{ commitsText }}</a>) + </span> + </div> + + <div v-if="mr.isOpen"> + <button + data-target="#modal_merge_info" + data-toggle="modal" + :disabled="mr.sourceBranchRemoved" + class="btn btn-sm btn-default inline js-check-out-branch" + type="button" + > + {{ s__("mrWidget|Check out branch") }} + </button> + <span class="dropdown prepend-left-10"> + <button + type="button" + class="btn btn-sm inline dropdown-toggle" + data-toggle="dropdown" + aria-label="Download as" + aria-haspopup="true" + aria-expanded="false" + > + <icon name="download" /> + <i + class="fa fa-caret-down" + aria-hidden="true"> + </i> + </button> + <ul class="dropdown-menu dropdown-menu-align-right"> + <li> + <a + class="js-download-email-patches" + :href="mr.emailPatchesPath" + download + > + {{ s__("mrWidget|Email patches") }} + </a> + </li> + <li> + <a + class="js-download-plain-diff" + :href="mr.plainDiffPath" + download + > + {{ s__("mrWidget|Plain diff") }} + </a> + </li> + </ul> + </span> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.js deleted file mode 100644 index 1d9f9863dd9..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.js +++ /dev/null @@ -1,23 +0,0 @@ -export default { - name: 'MRWidgetMergeHelp', - props: { - missingBranch: { type: String, required: false, default: '' }, - }, - template: ` - <section class="mr-widget-help"> - <template - v-if="missingBranch"> - If the {{missingBranch}} branch exists in your local repository, you - </template> - <template v-else> - You - </template> - can merge this merge request manually using the - <a - data-toggle="modal" - href="#modal_merge_info"> - command line - </a> - </section> - `, -}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.vue new file mode 100644 index 00000000000..62b61e1f41f --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.vue @@ -0,0 +1,41 @@ +<script> + import { sprintf, s__ } from '~/locale'; + + export default { + name: 'MRWidgetMergeHelp', + props: { + missingBranch: { + type: String, + required: false, + default: '', + }, + }, + computed: { + missingBranchInfo() { + return sprintf( + s__('mrWidget|If the %{branch} branch exists in your local repository, you can merge this merge request manually using the'), + { branch: this.missingBranch }, + ); + }, + }, + }; +</script> +<template> + <section class="mr-widget-help"> + <template v-if="missingBranch"> + {{ missingBranchInfo }} + </template> + <template v-else> + {{ s__("mrWidget|You can merge this merge request manually using the") }} + </template> + + <button + type="button" + class="btn-link btn-blank js-open-modal-help" + data-toggle="modal" + data-target="#modal_merge_info" + > + {{ s__("mrWidget|command line") }} + </button> + </section> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue index dbc65462377..109a302a172 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue @@ -1,10 +1,16 @@ <script> + /* eslint-disable vue/require-default-prop */ import pipelineStage from '../../pipelines/components/stage.vue'; import ciIcon from '../../vue_shared/components/ci_icon.vue'; import icon from '../../vue_shared/components/icon.vue'; export default { name: 'MRWidgetPipeline', + components: { + pipelineStage, + ciIcon, + icon, + }, props: { pipeline: { type: Object, @@ -21,11 +27,6 @@ required: false, }, }, - components: { - pipelineStage, - ciIcon, - icon, - }, computed: { hasPipeline() { return this.pipeline && Object.keys(this.pipeline).length > 0; @@ -62,7 +63,8 @@ <template v-else-if="hasPipeline"> <a class="append-right-10" - :href="this.status.details_path"> + :href="status.details_path" + > <ci-icon :status="status" /> </a> @@ -70,33 +72,37 @@ Pipeline <a :href="pipeline.path" - class="pipeline-id"> - #{{pipeline.id}} + class="pipeline-id" + > + #{{ pipeline.id }} </a> - {{pipeline.details.status.label}} for + {{ pipeline.details.status.label }} for <a :href="pipeline.commit.commit_path" - class="commit-sha js-commit-link"> - {{pipeline.commit.short_id}}</a>. + class="commit-sha js-commit-link" + > + {{ pipeline.commit.short_id }}</a>. <span class="mr-widget-pipeline-graph"> - <span class="stage-cell"> + <span + class="stage-cell" + v-if="hasStages" + > <div - v-if="hasStages" v-for="(stage, i) in pipeline.details.stages" :key="i" - class="stage-container dropdown js-mini-pipeline-graph"> + class="stage-container dropdown js-mini-pipeline-graph" + > <pipeline-stage :stage="stage" /> </div> </span> </span> <template v-if="pipeline.coverage"> - Coverage {{pipeline.coverage}}% + Coverage {{ pipeline.coverage }}% </template> - </div> </template> </div> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.js deleted file mode 100644 index 563267ad044..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.js +++ /dev/null @@ -1,37 +0,0 @@ -export default { - name: 'MRWidgetRelatedLinks', - props: { - relatedLinks: { type: Object, required: true }, - state: { type: String, required: false }, - }, - computed: { - hasLinks() { - const { closing, mentioned, assignToMe } = this.relatedLinks; - return closing || mentioned || assignToMe; - }, - closesText() { - if (this.state === 'merged') { - return 'Closed'; - } - if (this.state === 'closed') { - return 'Did not close'; - } - return 'Closes'; - }, - }, - template: ` - <section - v-if="hasLinks" - class="mr-info-list mr-links"> - <p v-if="relatedLinks.closing"> - {{closesText}} <span v-html="relatedLinks.closing"></span> - </p> - <p v-if="relatedLinks.mentioned"> - Mentions <span v-html="relatedLinks.mentioned"></span> - </p> - <p v-if="relatedLinks.assignToMe"> - <span v-html="relatedLinks.assignToMe"></span> - </p> - </section> - `, -}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue new file mode 100644 index 00000000000..88d0fcd70f5 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue @@ -0,0 +1,43 @@ +<script> + import { s__ } from '~/locale'; + + export default { + name: 'MRWidgetRelatedLinks', + props: { + relatedLinks: { + type: Object, + required: true, + default: () => ({}), + }, + state: { + type: String, + required: false, + default: '', + }, + }, + computed: { + closesText() { + if (this.state === 'merged') { + return s__('mrWidget|Closed'); + } + if (this.state === 'closed') { + return s__('mrWidget|Did not close'); + } + return s__('mrWidget|Closes'); + }, + }, + }; +</script> +<template> + <section class="mr-info-list mr-links"> + <p v-if="relatedLinks.closing"> + {{ closesText }} <span v-html="relatedLinks.closing"></span> + </p> + <p v-if="relatedLinks.mentioned"> + {{ s__("mrWidget|Mentions") }} <span v-html="relatedLinks.mentioned"></span> + </p> + <p v-if="relatedLinks.assignToMe"> + <span v-html="relatedLinks.assignToMe"></span> + </p> + </section> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js deleted file mode 100644 index eeb990908f6..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js +++ /dev/null @@ -1,36 +0,0 @@ -import ciIcon from '../../vue_shared/components/ci_icon.vue'; -import loadingIcon from '../../vue_shared/components/loading_icon.vue'; - -export default { - props: { - status: { type: String, required: true }, - showDisabledButton: { type: Boolean, required: false }, - }, - components: { - ciIcon, - loadingIcon, - }, - computed: { - statusObj() { - return { - group: this.status, - icon: `status_${this.status}`, - }; - }, - }, - template: ` - <div class="space-children flex-container-block append-right-10"> - <div v-if="status === 'loading'" class="mr-widget-icon"> - <loading-icon /> - </div> - <ci-icon v-else :status="statusObj" /> - <button - v-if="showDisabledButton" - type="button" - class="js-disabled-merge-button btn btn-success btn-sm" - disabled="true"> - Merge - </button> - </div> - `, -}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue new file mode 100644 index 00000000000..1fdc3218671 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue @@ -0,0 +1,57 @@ +<script> + import ciIcon from '../../vue_shared/components/ci_icon.vue'; + import loadingIcon from '../../vue_shared/components/loading_icon.vue'; + + export default { + components: { + ciIcon, + loadingIcon, + }, + props: { + status: { + type: String, + required: true, + }, + showDisabledButton: { + type: Boolean, + required: false, + default: false, + }, + }, + computed: { + isLoading() { + return this.status === 'loading'; + }, + statusObj() { + return { + group: this.status, + icon: `status_${this.status}`, + }; + }, + }, + }; +</script> +<template> + <div class="space-children flex-container-block append-right-10"> + <div + v-if="isLoading" + class="mr-widget-icon" + > + <loading-icon /> + </div> + + <ci-icon + v-else + :status="statusObj" + /> + + <button + v-if="showDisabledButton" + type="button" + class="js-disabled-merge-button btn btn-success btn-sm" + disabled="true" + > + {{ s__("mrWidget|Merge") }} + </button> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.js deleted file mode 100644 index b4e4a6aa161..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.js +++ /dev/null @@ -1,26 +0,0 @@ -import statusIcon from '../mr_widget_status_icon'; - -export default { - name: 'MRWidgetArchived', - components: { - statusIcon, - }, - template: ` - <div class="mr-widget-body media"> - <div class="space-children"> - <status-icon status="failed" /> - <button - type="button" - class="btn btn-success btn-sm" - disabled="true"> - Merge - </button> - </div> - <div class="media-body"> - <span class="bold"> - This project is archived, write access has been disabled - </span> - </div> - </div> - `, -}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.vue new file mode 100644 index 00000000000..cfbd44d41b2 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.vue @@ -0,0 +1,31 @@ +<script> + import statusIcon from '../mr_widget_status_icon.vue'; + + export default { + name: 'MRWidgetArchived', + components: { + statusIcon, + }, + }; +</script> +<template> + <div class="mr-widget-body media"> + <div class="space-children"> + <status-icon + status="warning" + /> + <button + type="button" + class="btn btn-success btn-sm" + disabled="true" + > + {{ s__("mrWidget|Merge") }} + </button> + </div> + <div class="media-body"> + <span class="bold"> + {{ s__("mrWidget|This project is archived, write access has been disabled") }} + </span> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.js deleted file mode 100644 index 5648208f7b1..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.js +++ /dev/null @@ -1,47 +0,0 @@ -import eventHub from '../../event_hub'; -import statusIcon from '../mr_widget_status_icon'; - -export default { - name: 'MRWidgetAutoMergeFailed', - props: { - mr: { type: Object, required: true }, - }, - data() { - return { - isRefreshing: false, - }; - }, - components: { - statusIcon, - }, - methods: { - refreshWidget() { - this.isRefreshing = true; - eventHub.$emit('MRWidgetUpdateRequested', () => { - this.isRefreshing = false; - }); - }, - }, - template: ` - <div class="mr-widget-body media"> - <status-icon status="failed" /> - <div class="media-body space-children"> - <span class="bold"> - <template v-if="mr.mergeError">{{mr.mergeError}}.</template> - This merge request failed to be merged automatically - </span> - <button - @click="refreshWidget" - :disabled="isRefreshing" - type="button" - class="btn btn-xs btn-default"> - <i - v-if="isRefreshing" - class="fa fa-spinner fa-spin" - aria-hidden="true" /> - Refresh - </button> - </div> - </div> - `, -}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue new file mode 100644 index 00000000000..40c3cb500bb --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue @@ -0,0 +1,52 @@ +<script> + import loadingIcon from '~/vue_shared/components/loading_icon.vue'; + import eventHub from '../../event_hub'; + import statusIcon from '../mr_widget_status_icon.vue'; + + export default { + name: 'MRWidgetAutoMergeFailed', + components: { + statusIcon, + loadingIcon, + }, + props: { + mr: { + type: Object, + required: true, + }, + }, + data() { + return { + isRefreshing: false, + }; + }, + methods: { + refreshWidget() { + this.isRefreshing = true; + eventHub.$emit('MRWidgetUpdateRequested', () => { + this.isRefreshing = false; + }); + }, + }, + }; +</script> +<template> + <div class="mr-widget-body media"> + <status-icon status="warning" /> + <div class="media-body space-children"> + <span class="bold"> + <template v-if="mr.mergeError">{{ mr.mergeError }}.</template> + {{ s__("mrWidget|This merge request failed to be merged automatically") }} + </span> + <button + @click="refreshWidget" + :disabled="isRefreshing" + type="button" + class="btn btn-xs btn-default" + > + <loading-icon v-if="isRefreshing" /> + {{ s__("mrWidget|Refresh") }} + </button> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js deleted file mode 100644 index 09561694939..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js +++ /dev/null @@ -1,18 +0,0 @@ -import statusIcon from '../mr_widget_status_icon'; - -export default { - name: 'MRWidgetChecking', - components: { - statusIcon, - }, - template: ` - <div class="mr-widget-body media"> - <status-icon status="loading" :show-disabled-button="true" /> - <div class="media-body space-children"> - <span class="bold"> - Checking ability to merge automatically - </span> - </div> - </div> - `, -}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue new file mode 100644 index 00000000000..caeaac75b45 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue @@ -0,0 +1,23 @@ +<script> + import statusIcon from '../mr_widget_status_icon.vue'; + + export default { + name: 'MRWidgetChecking', + components: { + statusIcon, + }, + }; +</script> +<template> + <div class="mr-widget-body media"> + <status-icon + status="loading" + :show-disabled-button="true" + /> + <div class="media-body space-children"> + <span class="bold"> + {{ s__("mrWidget|Checking ability to merge automatically") }} + </span> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js deleted file mode 100644 index dd8b2665b1d..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js +++ /dev/null @@ -1,35 +0,0 @@ -import mrWidgetAuthorTime from '../../components/mr_widget_author_time'; -import statusIcon from '../mr_widget_status_icon'; - -export default { - name: 'MRWidgetClosed', - props: { - mr: { type: Object, required: true }, - }, - components: { - 'mr-widget-author-and-time': mrWidgetAuthorTime, - statusIcon, - }, - template: ` - <div class="mr-widget-body media"> - <status-icon status="failed" /> - <div class="media-body"> - <mr-widget-author-and-time - actionText="Closed by" - :author="mr.metrics.closedBy" - :dateTitle="mr.metrics.closedAt" - :dateReadable="mr.metrics.readableClosedAt" - /> - <section class="mr-info-list"> - <p> - The changes were not merged into - <a - :href="mr.targetBranchPath" - class="label-branch"> - {{mr.targetBranch}}</a> - </p> - </section> - </div> - </div> - `, -}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue new file mode 100644 index 00000000000..68b691fc914 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue @@ -0,0 +1,48 @@ +<script> + import mrWidgetAuthorTime from '../../components/mr_widget_author_time.vue'; + import statusIcon from '../mr_widget_status_icon.vue'; + + export default { + name: 'MRWidgetClosed', + components: { + mrWidgetAuthorTime, + statusIcon, + }, + props: { + /* TODO: This is providing all store and service down when it + only needs metrics and targetBranch */ + mr: { + type: Object, + required: true, + default: () => ({}), + }, + }, + }; +</script> +<template> + <div class="mr-widget-body media"> + <status-icon + status="warning" + /> + <div class="media-body"> + <mr-widget-author-time + :action-text="s__('mrWidget|Closed by')" + :author="mr.metrics.closedBy" + :date-title="mr.metrics.closedAt" + :date-readable="mr.metrics.readableClosedAt" + /> + + <section class="mr-info-list"> + <p> + {{ s__("mrWidget|The changes were not merged into") }} + <a + :href="mr.targetBranchPath" + class="label-branch" + > + {{ mr.targetBranch }} + </a> + </p> + </section> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js deleted file mode 100644 index 5d468a085cb..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js +++ /dev/null @@ -1,47 +0,0 @@ -import statusIcon from '../mr_widget_status_icon'; - -export default { - name: 'MRWidgetConflicts', - props: { - mr: { type: Object, required: true }, - }, - components: { - statusIcon, - }, - template: ` - <div class="mr-widget-body media"> - <status-icon - status="failed" - :show-disabled-button="true" /> - <div class="media-body space-children"> - <span - v-if="mr.shouldBeRebased" - class="bold"> - Fast-forward merge is not possible. - To merge this request, first rebase locally. - </span> - <template v-else> - <span class="bold"> - There are merge conflicts<span v-if="!mr.canMerge">.</span> - <span v-if="!mr.canMerge"> - Resolve these conflicts or ask someone with write access to this repository to merge it locally - </span> - </span> - <a - v-if="mr.canMerge && mr.conflictResolutionPath" - :href="mr.conflictResolutionPath" - class="js-resolve-conflicts-button btn btn-default btn-xs"> - Resolve conflicts - </a> - <a - v-if="mr.canMerge" - class="js-merge-locally-button btn btn-default btn-xs" - data-toggle="modal" - href="#modal_merge_info"> - Merge locally - </a> - </template> - </div> - </div> - `, -}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue new file mode 100644 index 00000000000..dad4b0fe49d --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue @@ -0,0 +1,61 @@ +<script> + import statusIcon from '../mr_widget_status_icon.vue'; + + export default { + name: 'MRWidgetConflicts', + components: { + statusIcon, + }, + props: { + /* TODO: This is providing all store and service down when it + only needs a few props */ + mr: { + type: Object, + required: true, + default: () => ({}), + }, + }, + }; +</script> +<template> + <div class="mr-widget-body media"> + <status-icon + status="warning" + :show-disabled-button="true" + /> + + <div class="media-body space-children"> + <span + v-if="mr.shouldBeRebased" + class="bold" + > + {{ s__(`mrWidget|Fast-forward merge is not possible. +To merge this request, first rebase locally.`) }} + </span> + <template v-else> + <span class="bold"> + {{ s__("mrWidget|There are merge conflicts") }}<span v-if="!mr.canMerge">.</span> + <span v-if="!mr.canMerge"> + {{ s__(`mrWidget|Resolve these conflicts or ask someone + with write access to this repository to merge it locally`) }} + </span> + </span> + <a + v-if="mr.canMerge && mr.conflictResolutionPath" + :href="mr.conflictResolutionPath" + class="js-resolve-conflicts-button btn btn-default btn-xs" + > + {{ s__("mrWidget|Resolve conflicts") }} + </a> + <button + v-if="mr.canMerge" + class="js-merge-locally-button btn btn-default btn-xs" + data-toggle="modal" + data-target="#modal_merge_info" + > + {{ s__("mrWidget|Merge locally") }} + </button> + </template> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js deleted file mode 100644 index c25d6c359bb..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js +++ /dev/null @@ -1,78 +0,0 @@ -import statusIcon from '../mr_widget_status_icon'; -import eventHub from '../../event_hub'; - -export default { - name: 'MRWidgetFailedToMerge', - props: { - mr: { type: Object, required: true }, - }, - data() { - return { - timer: 10, - isRefreshing: false, - }; - }, - mounted() { - setInterval(() => { - this.updateTimer(); - }, 1000); - }, - created() { - eventHub.$emit('DisablePolling'); - }, - computed: { - timerText() { - return this.timer > 1 ? `${this.timer} seconds` : 'a second'; - }, - }, - methods: { - refresh() { - this.isRefreshing = true; - eventHub.$emit('MRWidgetUpdateRequested'); - eventHub.$emit('EnablePolling'); - }, - updateTimer() { - this.timer = this.timer - 1; - - if (this.timer === 0) { - this.refresh(); - } - }, - }, - components: { - statusIcon, - }, - template: ` - <div class="mr-widget-body media"> - <template v-if="isRefreshing"> - <status-icon status="loading" /> - <span class="media-body bold js-refresh-label"> - Refreshing now - </span> - </template> - <template v-else> - <status-icon status="failed" :show-disabled-button="true" /> - <div class="media-body space-children"> - <span class="bold"> - <span - class="has-error-message" - v-if="mr.mergeError"> - {{mr.mergeError}}. - </span> - <span v-else>Merge failed.</span> - <span - :class="{ 'has-custom-error': mr.mergeError }"> - Refreshing in {{timerText}} to show the updated status... - </span> - </span> - <button - @click="refresh" - class="btn btn-default btn-xs js-refresh-button" - type="button"> - Refresh now - </button> - </div> - </template> - </div> - `, -}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue new file mode 100644 index 00000000000..602b68ea572 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue @@ -0,0 +1,105 @@ +<script> + import { n__ } from '~/locale'; + import statusIcon from '../mr_widget_status_icon.vue'; + import eventHub from '../../event_hub'; + + export default { + name: 'MRWidgetFailedToMerge', + + components: { + statusIcon, + }, + + props: { + mr: { + type: Object, + required: true, + default: () => ({}), + }, + }, + + data() { + return { + timer: 10, + isRefreshing: false, + }; + }, + + computed: { + timerText() { + return n__( + 'Refreshing in a second to show the updated status...', + 'Refreshing in %d seconds to show the updated status...', + this.timer, + ); + }, + }, + + mounted() { + setInterval(() => { + this.updateTimer(); + }, 1000); + }, + + created() { + eventHub.$emit('DisablePolling'); + }, + + methods: { + refresh() { + this.isRefreshing = true; + eventHub.$emit('MRWidgetUpdateRequested'); + eventHub.$emit('EnablePolling'); + }, + updateTimer() { + this.timer = this.timer - 1; + + if (this.timer === 0) { + this.refresh(); + } + }, + }, + + }; +</script> +<template> + <div class="mr-widget-body media"> + <template v-if="isRefreshing"> + <status-icon status="loading" /> + <span class="media-body bold js-refresh-label"> + {{ s__("mrWidget|Refreshing now") }} + </span> + </template> + <template v-else> + <status-icon + status="warning" + :show-disabled-button="true" + /> + <div class="media-body space-children"> + <span class="bold"> + <span + class="has-error-message" + v-if="mr.mergeError" + > + {{ mr.mergeError }}. + </span> + <span v-else> + {{ s__("mrWidget|Merge failed.") }} + </span> + <span + :class="{ 'has-custom-error': mr.mergeError }" + > + {{ timerText }} + </span> + </span> + <button + @click="refresh" + class="btn btn-default btn-xs js-refresh-button" + type="button" + > + {{ s__("mrWidget|Refresh now") }} + </button> + </div> + </template> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js deleted file mode 100644 index bd349111bbd..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js +++ /dev/null @@ -1,124 +0,0 @@ -import Flash from '../../../flash'; -import statusIcon from '../mr_widget_status_icon'; -import MRWidgetAuthor from '../../components/mr_widget_author'; -import eventHub from '../../event_hub'; - -export default { - name: 'MRWidgetMergeWhenPipelineSucceeds', - props: { - mr: { type: Object, required: true }, - service: { type: Object, required: true }, - }, - components: { - 'mr-widget-author': MRWidgetAuthor, - statusIcon, - }, - data() { - return { - isCancellingAutoMerge: false, - isRemovingSourceBranch: false, - }; - }, - computed: { - canRemoveSourceBranch() { - const { shouldRemoveSourceBranch, canRemoveSourceBranch, - mergeUserId, currentUserId } = this.mr; - - return !shouldRemoveSourceBranch && canRemoveSourceBranch && mergeUserId === currentUserId; - }, - }, - methods: { - cancelAutomaticMerge() { - this.isCancellingAutoMerge = true; - this.service.cancelAutomaticMerge() - .then(res => res.data) - .then((data) => { - eventHub.$emit('UpdateWidgetData', data); - }) - .catch(() => { - this.isCancellingAutoMerge = false; - new Flash('Something went wrong. Please try again.'); // eslint-disable-line - }); - }, - removeSourceBranch() { - const options = { - sha: this.mr.sha, - merge_when_pipeline_succeeds: true, - should_remove_source_branch: true, - }; - - this.isRemovingSourceBranch = true; - this.service.mergeResource.save(options) - .then(res => res.data) - .then((data) => { - if (data.status === 'merge_when_pipeline_succeeds') { - eventHub.$emit('MRWidgetUpdateRequested'); - } - }) - .catch(() => { - this.isRemovingSourceBranch = false; - new Flash('Something went wrong. Please try again.'); // eslint-disable-line - }); - }, - }, - template: ` - <div class="mr-widget-body media"> - <status-icon status="success" /> - <div class="media-body"> - <h4 class="flex-container-block"> - <span class="append-right-10"> - Set by - <mr-widget-author :author="mr.setToMWPSBy" /> - to be merged automatically when the pipeline succeeds - </span> - <a - v-if="mr.canCancelAutomaticMerge" - @click.prevent="cancelAutomaticMerge" - :disabled="isCancellingAutoMerge" - role="button" - href="#" - class="btn btn-xs btn-default js-cancel-auto-merge"> - <i - v-if="isCancellingAutoMerge" - class="fa fa-spinner fa-spin" - aria-hidden="true" /> - Cancel automatic merge - </a> - </h4> - <section class="mr-info-list"> - <p>The changes will be merged into - <a - :href="mr.targetBranchPath" - class="label-branch"> - {{mr.targetBranch}} - </a> - </p> - <p v-if="mr.shouldRemoveSourceBranch"> - The source branch will be removed - </p> - <p - v-else - class="flex-container-block" - > - <span class="append-right-10"> - The source branch will not be removed - </span> - <a - v-if="canRemoveSourceBranch" - :disabled="isRemovingSourceBranch" - @click.prevent="removeSourceBranch" - role="button" - class="btn btn-xs btn-default js-remove-source-branch" - href="#"> - <i - v-if="isRemovingSourceBranch" - class="fa fa-spinner fa-spin" - aria-hidden="true" /> - Remove source branch - </a> - </p> - </section> - </div> - </div> - `, -}; 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 new file mode 100644 index 00000000000..de98a77be6f --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue @@ -0,0 +1,147 @@ +<script> + import Flash from '../../../flash'; + import statusIcon from '../mr_widget_status_icon.vue'; + import mrWidgetAuthor from '../../components/mr_widget_author.vue'; + import eventHub from '../../event_hub'; + + export default { + name: 'MRWidgetMergeWhenPipelineSucceeds', + components: { + mrWidgetAuthor, + statusIcon, + }, + props: { + mr: { + type: Object, + required: true, + default: () => ({}), + }, + service: { + type: Object, + required: true, + default: () => ({}), + }, + }, + data() { + return { + isCancellingAutoMerge: false, + isRemovingSourceBranch: false, + }; + }, + computed: { + canRemoveSourceBranch() { + const { + shouldRemoveSourceBranch, + canRemoveSourceBranch, + mergeUserId, + currentUserId, + } = this.mr; + + return !shouldRemoveSourceBranch && + canRemoveSourceBranch && + mergeUserId === currentUserId; + }, + }, + methods: { + cancelAutomaticMerge() { + this.isCancellingAutoMerge = true; + this.service.cancelAutomaticMerge() + .then(res => res.data) + .then((data) => { + eventHub.$emit('UpdateWidgetData', data); + }) + .catch(() => { + this.isCancellingAutoMerge = false; + Flash('Something went wrong. Please try again.'); + }); + }, + removeSourceBranch() { + const options = { + sha: this.mr.sha, + merge_when_pipeline_succeeds: true, + should_remove_source_branch: true, + }; + + this.isRemovingSourceBranch = true; + this.service.mergeResource.save(options) + .then(res => res.data) + .then((data) => { + if (data.status === 'merge_when_pipeline_succeeds') { + eventHub.$emit('MRWidgetUpdateRequested'); + } + }) + .catch(() => { + this.isRemovingSourceBranch = false; + Flash('Something went wrong. Please try again.'); + }); + }, + }, + }; +</script> +<template> + <div class="mr-widget-body media"> + <status-icon status="success" /> + <div class="media-body"> + <h4 class="flex-container-block"> + <span class="append-right-10"> + {{ s__("mrWidget|Set by") }} + <mr-widget-author :author="mr.setToMWPSBy" /> + {{ s__("mrWidget|to be merged automatically when the pipeline succeeds") }} + </span> + <a + v-if="mr.canCancelAutomaticMerge" + @click.prevent="cancelAutomaticMerge" + :disabled="isCancellingAutoMerge" + role="button" + href="#" + class="btn btn-xs btn-default js-cancel-auto-merge"> + <i + v-if="isCancellingAutoMerge" + class="fa fa-spinner fa-spin" + aria-hidden="true" + > + </i> + {{ s__("mrWidget|Cancel automatic merge") }} + </a> + </h4> + <section class="mr-info-list"> + <p> + {{ s__("mrWidget|The changes will be merged into") }} + <a + :href="mr.targetBranchPath" + class="label-branch" + > + {{ mr.targetBranch }} + </a> + </p> + <p v-if="mr.shouldRemoveSourceBranch"> + {{ s__("mrWidget|The source branch will be removed") }} + </p> + <p + v-else + class="flex-container-block" + > + <span class="append-right-10"> + {{ s__("mrWidget|The source branch will not be removed") }} + </span> + <a + v-if="canRemoveSourceBranch" + :disabled="isRemovingSourceBranch" + @click.prevent="removeSourceBranch" + role="button" + class="btn btn-xs btn-default js-remove-source-branch" + href="#" + > + <i + v-if="isRemovingSourceBranch" + class="fa fa-spinner fa-spin" + aria-hidden="true" + > + </i> + {{ s__("mrWidget|Remove source branch") }} + </a> + </p> + </section> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js deleted file mode 100644 index ba9681680ef..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js +++ /dev/null @@ -1,139 +0,0 @@ -import Flash from '../../../flash'; -import mrWidgetAuthorTime from '../../components/mr_widget_author_time'; -import tooltip from '../../../vue_shared/directives/tooltip'; -import loadingIcon from '../../../vue_shared/components/loading_icon.vue'; -import statusIcon from '../mr_widget_status_icon'; -import eventHub from '../../event_hub'; - -export default { - name: 'MRWidgetMerged', - props: { - mr: { type: Object, required: true }, - service: { type: Object, required: true }, - }, - data() { - return { - isMakingRequest: false, - }; - }, - directives: { - tooltip, - }, - components: { - 'mr-widget-author-and-time': mrWidgetAuthorTime, - loadingIcon, - statusIcon, - }, - computed: { - shouldShowRemoveSourceBranch() { - const { sourceBranchRemoved, isRemovingSourceBranch, canRemoveSourceBranch } = this.mr; - - return !sourceBranchRemoved && canRemoveSourceBranch && - !this.isMakingRequest && !isRemovingSourceBranch; - }, - shouldShowSourceBranchRemoving() { - const { sourceBranchRemoved, isRemovingSourceBranch } = this.mr; - return !sourceBranchRemoved && (isRemovingSourceBranch || this.isMakingRequest); - }, - shouldShowMergedButtons() { - const { canRevertInCurrentMR, canCherryPickInCurrentMR, revertInForkPath, - cherryPickInForkPath } = this.mr; - - return canRevertInCurrentMR || canCherryPickInCurrentMR || - revertInForkPath || cherryPickInForkPath; - }, - }, - methods: { - removeSourceBranch() { - this.isMakingRequest = true; - this.service.removeSourceBranch() - .then(res => res.data) - .then((data) => { - if (data.message === 'Branch was removed') { - eventHub.$emit('MRWidgetUpdateRequested', () => { - this.isMakingRequest = false; - }); - } - }) - .catch(() => { - this.isMakingRequest = false; - new Flash('Something went wrong. Please try again.'); // eslint-disable-line - }); - }, - }, - template: ` - <div class="mr-widget-body media"> - <status-icon status="success" /> - <div class="media-body"> - <div class="space-children"> - <mr-widget-author-and-time - actionText="Merged by" - :author="mr.metrics.mergedBy" - :date-title="mr.metrics.mergedAt" - :date-readable="mr.metrics.readableMergedAt" /> - <a - v-if="mr.canRevertInCurrentMR" - v-tooltip - class="btn btn-close btn-xs" - href="#modal-revert-commit" - data-toggle="modal" - data-container="body" - title="Revert this merge request in a new merge request"> - Revert - </a> - <a - v-else-if="mr.revertInForkPath" - v-tooltip - class="btn btn-close btn-xs" - data-method="post" - :href="mr.revertInForkPath" - title="Revert this merge request in a new merge request"> - Revert - </a> - <a - v-if="mr.canCherryPickInCurrentMR" - v-tooltip - class="btn btn-default btn-xs" - href="#modal-cherry-pick-commit" - data-toggle="modal" - data-container="body" - title="Cherry-pick this merge request in a new merge request"> - Cherry-pick - </a> - <a - v-else-if="mr.cherryPickInForkPath" - v-tooltip - class="btn btn-default btn-xs" - data-method="post" - :href="mr.cherryPickInForkPath" - title="Cherry-pick this merge request in a new merge request"> - Cherry-pick - </a> - </div> - <section class="mr-info-list"> - <p> - The changes were merged into - <span class="label-branch"> - <a :href="mr.targetBranchPath">{{mr.targetBranch}}</a> - </span> - </p> - <p v-if="mr.sourceBranchRemoved">The source branch has been removed</p> - <p v-if="shouldShowRemoveSourceBranch" class="space-children"> - <span>You can remove source branch now</span> - <button - @click="removeSourceBranch" - :disabled="isMakingRequest" - type="button" - class="btn btn-xs btn-default js-remove-branch-button"> - Remove Source Branch - </button> - </p> - <p v-if="shouldShowSourceBranchRemoving"> - <loading-icon inline /> - <span>The source branch is being removed</span> - </p> - </section> - </div> - </div> - `, -}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue new file mode 100644 index 00000000000..c1618bc6ea0 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue @@ -0,0 +1,192 @@ +<script> + import Flash from '~/flash'; + import tooltip from '~/vue_shared/directives/tooltip'; + import loadingIcon from '~/vue_shared/components/loading_icon.vue'; + import { s__, __ } from '~/locale'; + import mrWidgetAuthorTime from '../../components/mr_widget_author_time.vue'; + import statusIcon from '../mr_widget_status_icon.vue'; + import eventHub from '../../event_hub'; + + export default { + name: 'MRWidgetMerged', + directives: { + tooltip, + }, + components: { + mrWidgetAuthorTime, + loadingIcon, + statusIcon, + }, + props: { + mr: { + type: Object, + required: true, + default: () => ({}), + }, + service: { + type: Object, + required: true, + default: () => ({}), + }, + }, + data() { + return { + isMakingRequest: false, + }; + }, + computed: { + shouldShowRemoveSourceBranch() { + const { + sourceBranchRemoved, + isRemovingSourceBranch, + canRemoveSourceBranch, + } = this.mr; + + return !sourceBranchRemoved && + canRemoveSourceBranch && + !this.isMakingRequest && + !isRemovingSourceBranch; + }, + shouldShowSourceBranchRemoving() { + const { + sourceBranchRemoved, + isRemovingSourceBranch, + } = this.mr; + return !sourceBranchRemoved && + (isRemovingSourceBranch || this.isMakingRequest); + }, + shouldShowMergedButtons() { + const { + canRevertInCurrentMR, + canCherryPickInCurrentMR, + revertInForkPath, + cherryPickInForkPath, + } = this.mr; + + return canRevertInCurrentMR || + canCherryPickInCurrentMR || + revertInForkPath || + cherryPickInForkPath; + }, + revertTitle() { + return s__('mrWidget|Revert this merge request in a new merge request'); + }, + cherryPickTitle() { + return s__('mrWidget|Cherry-pick this merge request in a new merge request'); + }, + revertLabel() { + return s__('mrWidget|Revert'); + }, + cherryPickLabel() { + return s__('mrWidget|Cherry-pick'); + }, + }, + methods: { + removeSourceBranch() { + this.isMakingRequest = true; + + this.service.removeSourceBranch() + .then(res => res.data) + .then((data) => { + if (data.message === 'Branch was removed') { + eventHub.$emit('MRWidgetUpdateRequested', () => { + this.isMakingRequest = false; + }); + } + }) + .catch(() => { + this.isMakingRequest = false; + Flash(__('Something went wrong. Please try again.')); + }); + }, + }, + }; +</script> +<template> + <div class="mr-widget-body media"> + <status-icon status="success" /> + <div class="media-body"> + <div class="space-children"> + <mr-widget-author-time + :action-text="s__('mrWidget|Merged by')" + :author="mr.metrics.mergedBy" + :date-title="mr.metrics.mergedAt" + :date-readable="mr.metrics.readableMergedAt" + /> + <a + v-if="mr.canRevertInCurrentMR" + v-tooltip + class="btn btn-close btn-xs" + href="#modal-revert-commit" + data-toggle="modal" + data-container="body" + :title="revertTitle" + > + {{ revertLabel }} + </a> + <a + v-else-if="mr.revertInForkPath" + v-tooltip + class="btn btn-close btn-xs" + data-method="post" + :href="mr.revertInForkPath" + :title="revertTitle" + > + {{ revertLabel }} + </a> + <a + v-if="mr.canCherryPickInCurrentMR" + v-tooltip + class="btn btn-default btn-xs" + href="#modal-cherry-pick-commit" + data-toggle="modal" + data-container="body" + :title="cherryPickTitle" + > + {{ cherryPickLabel }} + </a> + <a + v-else-if="mr.cherryPickInForkPath" + v-tooltip + class="btn btn-default btn-xs" + data-method="post" + :href="mr.cherryPickInForkPath" + :title="cherryPickTitle" + > + {{ cherryPickLabel }} + </a> + </div> + <section class="mr-info-list"> + <p> + {{ s__("mrWidget|The changes were merged into") }} + <span class="label-branch"> + <a :href="mr.targetBranchPath">{{ mr.targetBranch }}</a> + </span> + </p> + <p v-if="mr.sourceBranchRemoved"> + {{ s__("mrWidget|The source branch has been removed") }} + </p> + <p + v-if="shouldShowRemoveSourceBranch" + class="space-children" + > + <span>{{ s__("mrWidget|You can remove source branch now") }}</span> + <button + @click="removeSourceBranch" + :disabled="isMakingRequest" + type="button" + class="btn btn-xs btn-default js-remove-branch-button" + > + {{ s__("mrWidget|Remove Source Branch") }} + </button> + </p> + <p v-if="shouldShowSourceBranchRemoving"> + <loading-icon :inline="true" /> + <span> + {{ s__("mrWidget|The source branch is being removed") }} + </span> + </p> + </section> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.js deleted file mode 100644 index f6d1a4feeb2..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.js +++ /dev/null @@ -1,29 +0,0 @@ -import statusIcon from '../mr_widget_status_icon'; - -export default { - name: 'MRWidgetMerging', - props: { - mr: { type: Object, required: true }, - }, - components: { - statusIcon, - }, - template: ` - <div class="mr-widget-body mr-state-locked media"> - <status-icon status="loading" /> - <div class="media-body"> - <h4> - This merge request is in the process of being merged - </h4> - <section class="mr-info-list"> - <p> - The changes will be merged into - <span class="label-branch"> - <a :href="mr.targetBranchPath">{{mr.targetBranch}}</a> - </span> - </p> - </section> - </div> - </div> - `, -}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue new file mode 100644 index 00000000000..953ddf40a51 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue @@ -0,0 +1,35 @@ +<script> + import statusIcon from '../mr_widget_status_icon.vue'; + + export default { + name: 'MRWidgetMerging', + components: { + statusIcon, + }, + props: { + mr: { + type: Object, + required: true, + default: () => ({}), + }, + }, + }; +</script> +<template> + <div class="mr-widget-body mr-state-locked media"> + <status-icon status="loading" /> + <div class="media-body"> + <h4> + {{ s__("mrWidget|This merge request is in the process of being merged") }} + </h4> + <section class="mr-info-list"> + <p> + {{ s__("mrWidget|The changes will be merged into") }} + <span class="label-branch"> + <a :href="mr.targetBranchPath">{{ mr.targetBranch }}</a> + </span> + </p> + </section> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js index 1bc0b7e0819..7733fb74afe 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js @@ -1,6 +1,6 @@ -import statusIcon from '../mr_widget_status_icon'; +import statusIcon from '../mr_widget_status_icon.vue'; import tooltip from '../../../vue_shared/directives/tooltip'; -import mrWidgetMergeHelp from '../../components/mr_widget_merge_help'; +import mrWidgetMergeHelp from '../../components/mr_widget_merge_help.vue'; export default { name: 'MRWidgetMissingBranch', @@ -24,7 +24,7 @@ export default { }, template: ` <div class="mr-widget-body media"> - <status-icon status="failed" :show-disabled-button="true" /> + <status-icon status="warning" :show-disabled-button="true" /> <div class="media-body space-children"> <span class="bold js-branch-text"> <span class="capitalize"> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js index 00047718201..cea3d97fa88 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js @@ -1,4 +1,4 @@ -import statusIcon from '../mr_widget_status_icon'; +import statusIcon from '../mr_widget_status_icon.vue'; export default { name: 'MRWidgetNotAllowed', diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js index 1cedf86e811..e66ce071ab4 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js @@ -1,4 +1,4 @@ -import statusIcon from '../mr_widget_status_icon'; +import statusIcon from '../mr_widget_status_icon.vue'; export default { name: 'MRWidgetPipelineBlocked', @@ -7,7 +7,7 @@ export default { }, template: ` <div class="mr-widget-body media"> - <status-icon status="failed" :show-disabled-button="true" /> + <status-icon status="warning" :show-disabled-button="true" /> <div class="media-body space-children"> <span class="bold"> Pipeline blocked. The pipeline for this merge request requires a manual action to proceed diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js index 6853ba4b9f8..4d9a2ca530f 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js @@ -1,4 +1,4 @@ -import statusIcon from '../mr_widget_status_icon'; +import statusIcon from '../mr_widget_status_icon.vue'; export default { name: 'MRWidgetPipelineBlocked', @@ -7,7 +7,7 @@ export default { }, template: ` <div class="mr-widget-body media"> - <status-icon status="failed" :show-disabled-button="true" /> + <status-icon status="warning" :show-disabled-button="true" /> <div class="media-body space-children"> <span class="bold"> The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure 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 e82fb979162..7ba6c29006a 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 @@ -1,8 +1,9 @@ import successSvg from 'icons/_icon_status_success.svg'; import warningSvg from 'icons/_icon_status_warning.svg'; import simplePoll from '~/lib/utils/simple_poll'; +import MergeRequest from '../../../merge_request'; import Flash from '../../../flash'; -import statusIcon from '../mr_widget_status_icon'; +import statusIcon from '../mr_widget_status_icon.vue'; import eventHub from '../../event_hub'; export default { @@ -68,7 +69,7 @@ export default { }, iconClass() { if (this.status === 'failed' || !this.commitMessage.length || !this.mr.isMergeAllowed || this.mr.preventMerge) { - return 'failed'; + return 'warning'; } return 'success'; }, @@ -165,11 +166,9 @@ export default { // If state is merged we should update the widget and stop the polling eventHub.$emit('MRWidgetUpdateRequested'); eventHub.$emit('FetchActionsContent'); - if (window.mergeRequest) { - window.mergeRequest.updateStatusText('status-box-open', 'status-box-merged', 'Merged'); - window.mergeRequest.hideCloseButton(); - window.mergeRequest.decreaseCounter(); - } + MergeRequest.setStatusBoxToMerged(); + MergeRequest.hideCloseButton(); + MergeRequest.decreaseCounter(); stopPolling(); // If user checked remove source branch and we didn't remove the branch yet diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue index 09276ba2769..2968af0d5cb 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue @@ -1,12 +1,16 @@ <script> import simplePoll from '../../../lib/utils/simple_poll'; import eventHub from '../../event_hub'; - import statusIcon from '../mr_widget_status_icon'; + import statusIcon from '../mr_widget_status_icon.vue'; import loadingIcon from '../../../vue_shared/components/loading_icon.vue'; import Flash from '../../../flash'; export default { name: 'MRWidgetRebase', + components: { + statusIcon, + loadingIcon, + }, props: { mr: { type: Object, @@ -17,10 +21,6 @@ required: true, }, }, - components: { - statusIcon, - loadingIcon, - }, data() { return { isMakingRequest: false, @@ -88,7 +88,7 @@ <status-icon :status="status" :show-disabled-button="showDisabledButton" - /> + /> <div class="rebase-state-find-class-convention media media-body space-children"> <template v-if="mr.rebaseInProgress || isMakingRequest"> @@ -100,23 +100,27 @@ <span class="bold"> Fast-forward merge is not possible. Rebase the source branch onto - <span class="label-branch">{{mr.targetBranch}}</span> + <span class="label-branch">{{ mr.targetBranch }}</span> to allow this merge request to be merged. </span> </template> <template v-if="!mr.rebaseInProgress && mr.canPushToSourceBranch && !isMakingRequest"> - <div class="accept-merge-holder clearfix js-toggle-container accept-action media space-children"> + <div + class="accept-merge-holder clearfix +js-toggle-container accept-action media space-children"> <button type="button" class="btn btn-sm btn-reopen btn-success" :disabled="isMakingRequest" - @click="rebase"> + @click="rebase" + > <loading-icon v-if="isMakingRequest" /> Rebase </button> <span v-if="!rebasingError" - class="bold"> + class="bold" + > Fast-forward merge is not possible. Rebase the source branch onto the target branch or merge target branch into source branch to allow this merge request to be merged. @@ -124,7 +128,7 @@ <span v-else class="bold danger"> - {{rebasingError}} + {{ rebasingError }} </span> </div> </template> 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 index af19cf6ab87..142ddf477f1 100644 --- 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 @@ -1,4 +1,4 @@ -import statusIcon from '../mr_widget_status_icon'; +import statusIcon from '../mr_widget_status_icon.vue'; export default { name: 'MRWidgetSHAMismatch', @@ -7,7 +7,7 @@ export default { }, template: ` <div class="mr-widget-body media"> - <status-icon status="failed" :show-disabled-button="true" /> + <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 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 index a119ecbbdfe..67b271c69ca 100644 --- 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 @@ -1,4 +1,4 @@ -import statusIcon from '../mr_widget_status_icon'; +import statusIcon from '../mr_widget_status_icon.vue'; export default { name: 'MRWidgetUnresolvedDiscussions', @@ -10,7 +10,7 @@ export default { }, template: ` <div class="mr-widget-body media"> - <status-icon status="failed" :show-disabled-button="true" /> + <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 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 13461440ef2..bbca641f65e 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,4 +1,4 @@ -import statusIcon from '../mr_widget_status_icon'; +import statusIcon from '../mr_widget_status_icon.vue'; import tooltip from '../../../vue_shared/directives/tooltip'; import eventHub from '../../event_hub'; @@ -37,7 +37,7 @@ export default { }, template: ` <div class="mr-widget-body media"> - <status-icon status="failed" :show-disabled-button="Boolean(mr.removeWIPPath)" /> + <status-icon status="warning" :show-disabled-button="Boolean(mr.removeWIPPath)" /> <div class="media-body space-children"> <span class="bold"> This is a Work in Progress diff --git a/app/assets/javascripts/vue_merge_request_widget/dependencies.js b/app/assets/javascripts/vue_merge_request_widget/dependencies.js index 940f3d9b2d0..7ca15537719 100644 --- a/app/assets/javascripts/vue_merge_request_widget/dependencies.js +++ b/app/assets/javascripts/vue_merge_request_widget/dependencies.js @@ -11,18 +11,18 @@ export { default as Vue } from 'vue'; export { default as SmartInterval } from '~/smart_interval'; -export { default as WidgetHeader } from './components/mr_widget_header'; -export { default as WidgetMergeHelp } from './components/mr_widget_merge_help'; +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 WidgetRelatedLinks } from './components/mr_widget_related_links'; -export { default as MergedState } from './components/states/mr_widget_merged'; -export { default as FailedToMerge } from './components/states/mr_widget_failed_to_merge'; -export { default as ClosedState } from './components/states/mr_widget_closed'; -export { default as MergingState } from './components/states/mr_widget_merging'; +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'; +export { default as ClosedState } from './components/states/mr_widget_closed.vue'; +export { default as MergingState } from './components/states/mr_widget_merging.vue'; export { default as WipState } from './components/states/mr_widget_wip'; -export { default as ArchivedState } from './components/states/mr_widget_archived'; -export { default as ConflictsState } from './components/states/mr_widget_conflicts'; +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 MissingBranchState } from './components/states/mr_widget_missing_branch'; export { default as NotAllowedState } from './components/states/mr_widget_not_allowed'; @@ -31,10 +31,10 @@ export { default as SHAMismatchState } from './components/states/mr_widget_sha_m export { default as UnresolvedDiscussionsState } from './components/states/mr_widget_unresolved_discussions'; export { default as PipelineBlockedState } from './components/states/mr_widget_pipeline_blocked'; export { default as PipelineFailedState } from './components/states/mr_widget_pipeline_failed'; -export { default as MergeWhenPipelineSucceedsState } from './components/states/mr_widget_merge_when_pipeline_succeeds'; +export { default as MergeWhenPipelineSucceedsState } from './components/states/mr_widget_merge_when_pipeline_succeeds.vue'; export { default as RebaseState } from './components/states/mr_widget_rebase.vue'; -export { default as AutoMergeFailed } from './components/states/mr_widget_auto_merge_failed'; -export { default as CheckingState } from './components/states/mr_widget_checking'; +export { default as AutoMergeFailed } from './components/states/mr_widget_auto_merge_failed.vue'; +export { default as CheckingState } from './components/states/mr_widget_checking.vue'; 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'; diff --git a/app/assets/javascripts/vue_merge_request_widget/index.js b/app/assets/javascripts/vue_merge_request_widget/index.js index 43ef468c303..6b9918b65b0 100644 --- a/app/assets/javascripts/vue_merge_request_widget/index.js +++ b/app/assets/javascripts/vue_merge_request_widget/index.js @@ -2,6 +2,9 @@ import { Vue, mrWidgetOptions, } from './dependencies'; +import Translate from '../vue_shared/translate'; + +Vue.use(Translate); document.addEventListener('DOMContentLoaded', () => { gl.mrWidgetData.gitlabLogo = gon.gitlab_logo; 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 2075f8e4fec..edf67fcd0a7 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 @@ -1,4 +1,4 @@ -import Project from '~/project'; +import Project from '~/pages/projects/project'; import SmartInterval from '~/smart_interval'; import Flash from '../flash'; import { @@ -257,7 +257,8 @@ export default { <mr-widget-related-links v-if="shouldRenderRelatedLinks" :state="mr.state" - :related-links="mr.relatedLinks" /> + :related-links="mr.relatedLinks" + /> </div> <div class="mr-widget-footer" diff --git a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue index fc795936abf..5324d5dc797 100644 --- a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue +++ b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue @@ -23,6 +23,12 @@ */ export default { + components: { + ciIcon, + }, + directives: { + tooltip, + }, props: { status: { type: Object, @@ -34,12 +40,6 @@ default: true, }, }, - components: { - ciIcon, - }, - directives: { - tooltip, - }, computed: { cssClass() { const className = this.status.group; @@ -53,11 +53,12 @@ :href="status.details_path" :class="cssClass" v-tooltip - :title="!showText ? status.text : ''"> + :title="!showText ? status.text : ''" + > <ci-icon :status="status" /> <template v-if="showText"> - {{status.text}} + {{ status.text }} </template> </a> </template> diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon.vue index 2a018f38366..8fea746f4de 100644 --- a/app/assets/javascripts/vue_shared/components/ci_icon.vue +++ b/app/assets/javascripts/vue_shared/components/ci_icon.vue @@ -23,6 +23,9 @@ * - Jobs show view sidebar */ export default { + components: { + icon, + }, props: { status: { type: Object, @@ -30,10 +33,6 @@ }, }, - components: { - icon, - }, - computed: { cssClass() { const status = this.status.group; @@ -43,9 +42,7 @@ }; </script> <template> - <span - :class="cssClass"> - <icon - :name="status.icon"/> + <span :class="cssClass"> + <icon :name="status.icon" /> </span> </template> diff --git a/app/assets/javascripts/vue_shared/components/clipboard_button.vue b/app/assets/javascripts/vue_shared/components/clipboard_button.vue index 3a7143c450e..31d9b9d9c48 100644 --- a/app/assets/javascripts/vue_shared/components/clipboard_button.vue +++ b/app/assets/javascripts/vue_shared/components/clipboard_button.vue @@ -1,10 +1,14 @@ <script> + import tooltip from '../directives/tooltip'; /** * Falls back to the code used in `copy_to_clipboard.js` */ export default { - name: 'clipboardButton', + name: 'ClipboardButton', + directives: { + tooltip, + }, props: { text: { type: String, @@ -14,6 +18,16 @@ type: String, required: true, }, + tooltipPlacement: { + type: String, + required: false, + default: 'top', + }, + tooltipContainer: { + type: [String, Boolean], + required: false, + default: false, + }, }, }; </script> @@ -22,11 +36,16 @@ <button type="button" class="btn btn-transparent btn-clipboard" - :data-title="title" - :data-clipboard-text="text"> - <i - aria-hidden="true" - class="fa fa-clipboard"> - </i> + :title="title" + :data-clipboard-text="text" + v-tooltip + :data-container="tooltipContainer" + :data-placement="tooltipPlacement" + > + <i + aria-hidden="true" + class="fa fa-clipboard" + > + </i> </button> </template> diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue index 59ca9a0a6d4..6d1fe7ee8ca 100644 --- a/app/assets/javascripts/vue_shared/components/commit.vue +++ b/app/assets/javascripts/vue_shared/components/commit.vue @@ -2,9 +2,16 @@ import commitIconSvg from 'icons/_icon_commit.svg'; import userAvatarLink from './user_avatar/user_avatar_link.vue'; import tooltip from '../directives/tooltip'; - import Icon from '../../vue_shared/components/icon.vue'; + import icon from '../../vue_shared/components/icon.vue'; export default { + directives: { + tooltip, + }, + components: { + userAvatarLink, + icon, + }, props: { /** * Indicates the existance of a tag. @@ -103,13 +110,6 @@ this.author.username ? `${this.author.username}'s avatar` : null; }, }, - directives: { - tooltip, - }, - components: { - userAvatarLink, - Icon, - }, created() { this.commitIconSvg = commitIconSvg; }, @@ -118,17 +118,17 @@ <template> <div class="branch-commit"> <template v-if="hasCommitRef && showBranch"> - <div - class="icon-container hidden-xs"> + <div class="icon-container hidden-xs"> <i v-if="tag" class="fa fa-tag" - aria-hidden="true"> + aria-hidden="true" + > </i> <icon v-if="!tag" - name="fork"> - </icon> + name="fork" + /> </div> <a @@ -136,25 +136,29 @@ :href="commitRef.ref_url" v-tooltip data-container="body" - :title="commitRef.name"> - {{commitRef.name}} + :title="commitRef.name" + > + {{ commitRef.name }} </a> </template> <div v-html="commitIconSvg" - class="commit-icon js-commit-icon"> + class="commit-icon js-commit-icon" + > </div> <a class="commit-sha" - :href="commitUrl"> - {{shortSha}} + :href="commitUrl" + > + {{ shortSha }} </a> <div class="commit-title flex-truncate-parent"> <span v-if="title" - class="flex-truncate-child"> + class="flex-truncate-child" + > <user-avatar-link v-if="hasAuthor" class="avatar-image-container" @@ -165,8 +169,9 @@ /> <a class="commit-row-message" - :href="commitUrl"> - {{title}} + :href="commitUrl" + > + {{ title }} </a> </span> <span v-else> diff --git a/app/assets/javascripts/vue_shared/components/confirmation_input.vue b/app/assets/javascripts/vue_shared/components/confirmation_input.vue new file mode 100644 index 00000000000..1aa03ea6317 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/confirmation_input.vue @@ -0,0 +1,62 @@ +<script> + import _ from 'underscore'; + import { __, sprintf } from '~/locale'; + + export default { + props: { + inputId: { + type: String, + required: true, + }, + confirmationKey: { + type: String, + required: true, + }, + confirmationValue: { + type: String, + required: true, + }, + shouldEscapeConfirmationValue: { + type: Boolean, + required: false, + default: true, + }, + }, + computed: { + inputLabel() { + let value = this.confirmationValue; + if (this.shouldEscapeConfirmationValue) { + value = _.escape(value); + } + + return sprintf( + __('Type %{value} to confirm:'), + { value: `<code>${value}</code>` }, + false, + ); + }, + }, + methods: { + hasCorrectValue() { + return this.$refs.enteredValue.value === this.confirmationValue; + }, + }, + }; +</script> + +<template> + <div> + <label + v-html="inputLabel" + :for="inputId" + > + </label> + <input + :id="inputId" + :name="confirmationKey" + type="text" + ref="enteredValue" + class="form-control" + /> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/expand_button.vue b/app/assets/javascripts/vue_shared/components/expand_button.vue index 05e48ed297f..3595a9389e9 100644 --- a/app/assets/javascripts/vue_shared/components/expand_button.vue +++ b/app/assets/javascripts/vue_shared/components/expand_button.vue @@ -11,7 +11,7 @@ * </expand-button> */ export default { - name: 'expandButton', + name: 'ExpandButton', data() { return { isCollapsed: true, diff --git a/app/assets/javascripts/vue_shared/components/file_icon.vue b/app/assets/javascripts/vue_shared/components/file_icon.vue index 65c64967fdc..c9d7c0f4999 100644 --- a/app/assets/javascripts/vue_shared/components/file_icon.vue +++ b/app/assets/javascripts/vue_shared/components/file_icon.vue @@ -16,6 +16,10 @@ */ export default { + components: { + loadingIcon, + icon, + }, props: { fileName: { type: String, @@ -52,10 +56,6 @@ default: '', }, }, - components: { - loadingIcon, - icon, - }, computed: { spriteHref() { const iconName = getIconForFile(this.fileName) || 'file'; @@ -75,9 +75,9 @@ <span> <svg :class="[iconSizeClass, cssClasses]" - v-if="!loading && !folder"> - <use - v-bind="{'xlink:href':spriteHref}"/> + v-if="!loading && !folder" + > + <use v-bind="{ 'xlink:href':spriteHref }" /> </svg> <icon v-if="!loading && folder" diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue index 2209bc0f9cf..1f72dea1b33 100644 --- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue +++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue @@ -1,80 +1,78 @@ <script> -import ciIconBadge from './ci_badge_link.vue'; -import loadingIcon from './loading_icon.vue'; -import timeagoTooltip from './time_ago_tooltip.vue'; -import tooltip from '../directives/tooltip'; -import userAvatarImage from './user_avatar/user_avatar_image.vue'; - -/** - * Renders header component for job and pipeline page based on UI mockups - * - * Used in: - * - job show page - * - pipeline show page - */ -export default { - props: { - status: { - type: Object, - required: true, - }, - itemName: { - type: String, - required: true, - }, - itemId: { - type: Number, - required: true, - }, - time: { - type: String, - required: true, - }, - user: { - type: Object, - required: false, - default: () => ({}), + import ciIconBadge from './ci_badge_link.vue'; + import loadingIcon from './loading_icon.vue'; + import timeagoTooltip from './time_ago_tooltip.vue'; + import tooltip from '../directives/tooltip'; + import userAvatarImage from './user_avatar/user_avatar_image.vue'; + + /** + * Renders header component for job and pipeline page based on UI mockups + * + * Used in: + * - job show page + * - pipeline show page + */ + export default { + components: { + ciIconBadge, + loadingIcon, + timeagoTooltip, + userAvatarImage, }, - actions: { - type: Array, - required: false, - default: () => [], + directives: { + tooltip, }, - hasSidebarButton: { - type: Boolean, - required: false, - default: false, + props: { + status: { + type: Object, + required: true, + }, + itemName: { + type: String, + required: true, + }, + itemId: { + type: Number, + required: true, + }, + time: { + type: String, + required: true, + }, + user: { + type: Object, + required: false, + default: () => ({}), + }, + actions: { + type: Array, + required: false, + default: () => [], + }, + hasSidebarButton: { + type: Boolean, + required: false, + default: false, + }, + shouldRenderTriggeredLabel: { + type: Boolean, + required: false, + default: true, + }, }, - shouldRenderTriggeredLabel: { - type: Boolean, - required: false, - default: true, - }, - }, - - directives: { - tooltip, - }, - - components: { - ciIconBadge, - loadingIcon, - timeagoTooltip, - userAvatarImage, - }, - computed: { - userAvatarAltText() { - return `${this.user.name}'s avatar`; + computed: { + userAvatarAltText() { + return `${this.user.name}'s avatar`; + }, }, - }, - methods: { - onClickAction(action) { - this.$emit('actionClicked', action); + methods: { + onClickAction(action) { + this.$emit('actionClicked', action); + }, }, - }, -}; + }; </script> <template> @@ -84,7 +82,7 @@ export default { <ci-icon-badge :status="status" /> <strong> - {{itemName}} #{{itemId}} + {{ itemName }} #{{ itemId }} </strong> <template v-if="shouldRenderTriggeredLabel"> @@ -103,16 +101,17 @@ export default { v-tooltip :href="user.path" :title="user.email" - class="js-user-link commit-committer-link"> + class="js-user-link commit-committer-link" + > <user-avatar-image :img-src="user.avatar_url" :img-alt="userAvatarAltText" :tooltip-text="user.name" :img-size="24" - /> + /> - {{user.name}} + {{ user.name }} </a> </template> </section> @@ -121,12 +120,15 @@ export default { class="header-action-buttons" v-if="actions.length"> <template - v-for="action in actions"> + v-for="(action, i) in actions" + > <a v-if="action.type === 'link'" :href="action.path" - :class="action.cssClass"> - {{action.label}} + :class="action.cssClass" + :key="i" + > + {{ action.label }} </a> <a @@ -134,8 +136,10 @@ export default { :href="action.path" data-method="post" rel="nofollow" - :class="action.cssClass"> - {{action.label}} + :class="action.cssClass" + :key="i" + > + {{ action.label }} </a> <button @@ -143,25 +147,31 @@ export default { @click="onClickAction(action)" :disabled="action.isLoading" :class="action.cssClass" - type="button"> - {{action.label}} + type="button" + :key="i" + > + {{ action.label }} <i v-show="action.isLoading" class="fa fa-spin fa-spinner" - aria-hidden="true"> + aria-hidden="true" + > </i> </button> </template> <button v-if="hasSidebarButton" type="button" - class="btn btn-default visible-xs-block visible-sm-block sidebar-toggle-btn js-sidebar-build-toggle js-sidebar-build-toggle-header" + class="btn btn-default visible-xs-block +visible-sm-block sidebar-toggle-btn js-sidebar-build-toggle js-sidebar-build-toggle-header" aria-label="Toggle Sidebar" - id="toggleSidebar"> + id="toggleSidebar" + > <i class="fa fa-angle-double-left" aria-hidden="true" - aria-labelledby="toggleSidebar"> + aria-labelledby="toggleSidebar" + > </i> </button> </section> diff --git a/app/assets/javascripts/vue_shared/components/icon.vue b/app/assets/javascripts/vue_shared/components/icon.vue index 365229ea274..6a2e05000e1 100644 --- a/app/assets/javascripts/vue_shared/components/icon.vue +++ b/app/assets/javascripts/vue_shared/components/icon.vue @@ -1,17 +1,17 @@ <script> -/* This is a re-usable vue component for rendering a svg sprite - icon + /* This is a re-usable vue component for rendering a svg sprite + icon - Sample configuration: + Sample configuration: - <icon - name="retry" - :size="32" - css-classes="top" - /> + <icon + name="retry" + :size="32" + css-classes="top" + /> -*/ + */ // only allow classes in images.scss e.g. s12 const validSizes = [8, 12, 16, 18, 24, 32, 48, 72]; @@ -80,7 +80,6 @@ :height="height" :x="x" :y="y"> - <use - v-bind="{'xlink:href':spriteHref}"/> + <use v-bind="{ 'xlink:href':spriteHref }" /> </svg> </template> diff --git a/app/assets/javascripts/vue_shared/components/identicon.vue b/app/assets/javascripts/vue_shared/components/identicon.vue index 7cf2e029cf6..0a30f467b08 100644 --- a/app/assets/javascripts/vue_shared/components/identicon.vue +++ b/app/assets/javascripts/vue_shared/components/identicon.vue @@ -46,6 +46,6 @@ export default { class="avatar identicon" :class="sizeClass" :style="identiconStyles"> - {{identiconTitle}} + {{ identiconTitle }} </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue index 564fc5029af..b48828ae81f 100644 --- a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue +++ b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue @@ -1,7 +1,10 @@ <script> - import Icon from '../../../vue_shared/components/icon.vue'; + import icon from '../../../vue_shared/components/icon.vue'; export default { + components: { + icon, + }, props: { isLocked: { type: Boolean, @@ -16,10 +19,6 @@ }, }, - components: { - Icon, - }, - computed: { warningIcon() { if (this.isConfidential) return 'eye-slash'; @@ -37,16 +36,17 @@ <template> <div class="issuable-note-warning"> <icon - :name="warningIcon" - :size="16" - class="icon inline" - aria-hidden="true" - v-if="!isLockedAndConfidential"> - </icon> + :name="warningIcon" + :size="16" + class="icon inline" + aria-hidden="true" + v-if="!isLockedAndConfidential" + /> <span v-if="isLockedAndConfidential"> {{ __('This issue is confidential and locked.') }} - {{ __('People without permission will never get a notification and won\'t be able to comment.') }} + {{ __(`People without permission will never +get a notification and won't be able to comment.`) }} </span> <span v-else-if="isConfidential"> diff --git a/app/assets/javascripts/vue_shared/components/loading_button.vue b/app/assets/javascripts/vue_shared/components/loading_button.vue index 247943f83e6..ff8c0f7c1d2 100644 --- a/app/assets/javascripts/vue_shared/components/loading_button.vue +++ b/app/assets/javascripts/vue_shared/components/loading_button.vue @@ -1,55 +1,56 @@ <script> + /* eslint-disable vue/require-default-prop */ -/* This is a re-usable vue component for rendering a button - that will probably be sending off ajax requests and need - to show the loading status by setting the `loading` option. - This can also be used for initial page load when you don't - know the action of the button yet by setting - `loading: true, label: undefined`. + /* This is a re-usable vue component for rendering a button + that will probably be sending off ajax requests and need + to show the loading status by setting the `loading` option. + This can also be used for initial page load when you don't + know the action of the button yet by setting + `loading: true, label: undefined`. - Sample configuration: + Sample configuration: - <loading-button - :loading="true" - :label="Hello" - @click="..." - /> + <loading-button + :loading="true" + :label="Hello" + @click="..." + /> -*/ + */ -import loadingIcon from './loading_icon.vue'; + import loadingIcon from './loading_icon.vue'; -export default { - props: { - loading: { - type: Boolean, - required: false, - default: false, + export default { + components: { + loadingIcon, }, - disabled: { - type: Boolean, - required: false, - default: false, + props: { + loading: { + type: Boolean, + required: false, + default: false, + }, + disabled: { + type: Boolean, + required: false, + default: false, + }, + label: { + type: String, + required: false, + }, + containerClass: { + type: String, + required: false, + default: 'btn btn-align-content', + }, }, - label: { - type: String, - required: false, + methods: { + onClick(e) { + this.$emit('click', e); + }, }, - containerClass: { - type: String, - required: false, - default: 'btn btn-align-content', - }, - }, - components: { - loadingIcon, - }, - methods: { - onClick(e) { - this.$emit('click', e); - }, - }, -}; + }; </script> <template> @@ -59,23 +60,23 @@ export default { :class="containerClass" :disabled="loading || disabled" > - <transition name="fade"> - <loading-icon - v-if="loading" - :inline="true" - class="js-loading-button-icon" - :class="{ - 'append-right-5': label - }" - /> - </transition> - <transition name="fade"> - <span - v-if="label" - class="js-loading-button-label" - > - {{ label }} - </span> - </transition> + <transition name="fade"> + <loading-icon + v-if="loading" + :inline="true" + class="js-loading-button-icon" + :class="{ + 'append-right-5': label + }" + /> + </transition> + <transition name="fade"> + <span + v-if="label" + class="js-loading-button-label" + > + {{ label }} + </span> + </transition> </button> </template> diff --git a/app/assets/javascripts/vue_shared/components/loading_icon.vue b/app/assets/javascripts/vue_shared/components/loading_icon.vue index 15581d5c2a0..12a75e016d7 100644 --- a/app/assets/javascripts/vue_shared/components/loading_icon.vue +++ b/app/assets/javascripts/vue_shared/components/loading_icon.vue @@ -32,13 +32,14 @@ </script> <template> <component - :is="this.rootElementType" - class="text-center"> + :is="rootElementType" + class="loading-container text-center"> <i class="fa fa-spin fa-spinner" :class="cssClass" aria-hidden="true" - :aria-label="label"> + :aria-label="label" + > </i> </component> </template> diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index 15e3d713448..1371dca0c35 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -6,6 +6,11 @@ import icon from '../icon.vue'; export default { + components: { + markdownHeader, + markdownToolbar, + icon, + }, props: { markdownPreviewPath: { type: String, @@ -24,6 +29,7 @@ quickActionsDocsPath: { type: String, required: false, + default: '', }, canAttachFile: { type: Boolean, @@ -45,17 +51,24 @@ previewMarkdown: false, }; }, - components: { - markdownHeader, - markdownToolbar, - icon, - }, computed: { shouldShowReferencedUsers() { const referencedUsersThreshold = 10; return this.referencedUsers.length >= referencedUsersThreshold; }, }, + mounted() { + /* + GLForm class handles all the toolbar buttons + */ + return new GLForm($(this.$refs['gl-form']), this.enableAutocomplete); + }, + beforeDestroy() { + const glForm = $(this.$refs['gl-form']).data('gl-form'); + if (glForm) { + glForm.destroy(); + } + }, methods: { showPreviewTab() { if (this.previewMarkdown) return; @@ -98,18 +111,6 @@ }); }, }, - mounted() { - /* - GLForm class handles all the toolbar buttons - */ - return new GLForm($(this.$refs['gl-form']), this.enableAutocomplete); - }, - beforeDestroy() { - const glForm = $(this.$refs['gl-form']).data('gl-form'); - if (glForm) { - glForm.destroy(); - } - }, }; </script> @@ -121,34 +122,39 @@ <markdown-header :preview-markdown="previewMarkdown" @preview-markdown="showPreviewTab" - @write-markdown="showWriteTab" /> + @write-markdown="showWriteTab" + /> <div class="md-write-holder" - v-show="!previewMarkdown"> + v-show="!previewMarkdown" + > <div class="zen-backdrop"> <slot name="textarea"></slot> <a class="zen-control zen-control-leave js-zen-leave" href="#" - aria-label="Enter zen mode"> + aria-label="Enter zen mode" + > <icon name="screen-normal" - :size="32"> - </icon> + :size="32" + /> </a> <markdown-toolbar :markdown-docs-path="markdownDocsPath" :quick-actions-docs-path="quickActionsDocsPath" :can-attach-file="canAttachFile" - /> + /> </div> </div> <div class="md md-preview-holder md-preview" - v-show="previewMarkdown"> + v-show="previewMarkdown" + > <div ref="markdown-preview" - v-html="markdownPreview"> + v-html="markdownPreview" + > </div> <span v-if="markdownPreviewLoading"> Loading... @@ -158,23 +164,27 @@ <div v-if="referencedCommands" v-html="referencedCommands" - class="referenced-commands"></div> + class="referenced-commands" + > + </div> <div v-if="shouldShowReferencedUsers" - class="referenced-users"> - <span> - <i - class="fa fa-exclamation-triangle" - aria-hidden="true"> - </i> - You are about to add - <strong> - <span class="js-referenced-users-count"> - {{referencedUsers.length}} - </span> - </strong> people to the discussion. Proceed with caution. - </span> - </div> + class="referenced-users" + > + <span> + <i + class="fa fa-exclamation-triangle" + aria-hidden="true" + > + </i> + You are about to add + <strong> + <span class="js-referenced-users-count"> + {{ referencedUsers.length }} + </span> + </strong> people to the discussion. Proceed with caution. + </span> + </div> </template> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index 36d2d1dc164..f65eab11a27 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -4,18 +4,26 @@ import icon from '../icon.vue'; export default { + directives: { + tooltip, + }, + components: { + toolbarButton, + icon, + }, props: { previewMarkdown: { type: Boolean, required: true, }, }, - directives: { - tooltip, + mounted() { + $(document).on('markdown-preview:show.vue', this.previewMarkdownTab); + $(document).on('markdown-preview:hide.vue', this.writeMarkdownTab); }, - components: { - toolbarButton, - icon, + beforeDestroy() { + $(document).off('markdown-preview:show.vue', this.previewMarkdownTab); + $(document).off('markdown-preview:hide.vue', this.writeMarkdownTab); }, methods: { isMarkdownForm(form) { @@ -36,14 +44,6 @@ this.$emit('write-markdown'); }, }, - mounted() { - $(document).on('markdown-preview:show.vue', this.previewMarkdownTab); - $(document).on('markdown-preview:hide.vue', this.writeMarkdownTab); - }, - beforeDestroy() { - $(document).off('markdown-preview:show.vue', this.previewMarkdownTab); - $(document).off('markdown-preview:hide.vue', this.writeMarkdownTab); - }, }; </script> @@ -52,12 +52,14 @@ <ul class="nav-links clearfix"> <li class="md-header-tab" - :class="{ active: !previewMarkdown }"> + :class="{ active: !previewMarkdown }" + > <a class="js-write-link" href="#md-write-holder" tabindex="-1" - @click.prevent="writeMarkdownTab($event)"> + @click.prevent="writeMarkdownTab($event)" + > Write </a> </li> @@ -68,46 +70,55 @@ class="js-preview-link" href="#md-preview-holder" tabindex="-1" - @click.prevent="previewMarkdownTab($event)"> + @click.prevent="previewMarkdownTab($event)" + > Preview </a> </li> <li class="md-header-toolbar" - :class="{ active: !previewMarkdown }"> + :class="{ active: !previewMarkdown }" + > <toolbar-button tag="**" button-title="Add bold text" - icon="bold" /> + icon="bold" + /> <toolbar-button tag="*" button-title="Add italic text" - icon="italic" /> + icon="italic" + /> <toolbar-button tag="> " :prepend="true" button-title="Insert a quote" - icon="quote" /> + icon="quote" + /> <toolbar-button tag="`" tag-block="```" button-title="Insert code" - icon="code" /> + icon="code" + /> <toolbar-button tag="* " :prepend="true" button-title="Add a bullet list" - icon="list-bulleted" /> + icon="list-bulleted" + /> <toolbar-button tag="1. " :prepend="true" button-title="Add a numbered list" - icon="list-numbered" /> + icon="list-numbered" + /> <toolbar-button tag="* [ ] " :prepend="true" button-title="Add a task list" - icon="task-done" /> + icon="task-done" + /> <button v-tooltip aria-label="Go full screen" @@ -115,10 +126,11 @@ data-container="body" tabindex="-1" title="Go full screen" - type="button"> + type="button" + > <icon - name="screen-full"> - </icon> + name="screen-full" + /> </button> </li> </ul> diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue index ea2509d2839..c0ee88bbf72 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue @@ -8,6 +8,7 @@ quickActionsDocsPath: { type: String, required: false, + default: '', }, canAttachFile: { type: Boolean, @@ -15,32 +16,40 @@ default: true, }, }, + computed: { + hasQuickActionsDocsPath() { + return this.quickActionsDocsPath !== ''; + }, + }, }; </script> <template> <div class="comment-toolbar clearfix"> <div class="toolbar-text"> - <template v-if="!quickActionsDocsPath && markdownDocsPath"> + <template v-if="!hasQuickActionsDocsPath && markdownDocsPath"> <a :href="markdownDocsPath" target="_blank" - tabindex="-1"> + tabindex="-1" + > Markdown is supported </a> </template> - <template v-if="quickActionsDocsPath && markdownDocsPath"> - <a + <template v-if="hasQuickActionsDocsPath && markdownDocsPath"> + <a :href="markdownDocsPath" target="_blank" - tabindex="-1"> + tabindex="-1" + > Markdown </a> and - <a + <a :href="quickActionsDocsPath" target="_blank" - tabindex="-1"> + tabindex="-1" + > quick actions </a> are supported @@ -53,46 +62,58 @@ <span class="uploading-progress-container hide"> <i class="fa fa-file-image-o toolbar-button-icon" - aria-hidden="true"></i> + aria-hidden="true" + > + </i> <span class="attaching-file-message"></span> <span class="uploading-progress">0%</span> <span class="uploading-spinner"> <i class="fa fa-spinner fa-spin toolbar-button-icon" - aria-hidden="true"></i> + aria-hidden="true" + > + </i> </span> </span> <span class="uploading-error-container hide"> <span class="uploading-error-icon"> <i class="fa fa-file-image-o toolbar-button-icon" - aria-hidden="true"></i> + aria-hidden="true" + > + </i> </span> <span class="uploading-error-message"></span> <button class="retry-uploading-link" - type="button"> - Try again + type="button" + > + Try again </button> or <button class="attach-new-file markdown-selector" - type="button"> + type="button" + > attach a new file </button> </span> <button class="markdown-selector button-attach-file" tabindex="-1" - type="button"> + type="button" + > <i class="fa fa-file-image-o toolbar-button-icon" - aria-hidden="true"></i> + aria-hidden="true" + > + </i> Attach a file </button> <button class="btn btn-default btn-xs hide button-cancel-uploading-files" - type="button"> + type="button" + > Cancel </button> </span> diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue index e3e41f8f0ca..2d2d69ebeb2 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue @@ -3,6 +3,12 @@ import icon from '../icon.vue'; export default { + components: { + icon, + }, + directives: { + tooltip, + }, props: { buttonTitle: { type: String, @@ -27,12 +33,6 @@ default: false, }, }, - components: { - icon, - }, - directives: { - tooltip, - }, }; </script> @@ -47,9 +47,10 @@ :data-md-block="tagBlock" :data-md-prepend="prepend" :title="buttonTitle" - :aria-label="buttonTitle"> + :aria-label="buttonTitle" + > <icon - :name="icon"> - </icon> + :name="icon" + /> </button> </template> diff --git a/app/assets/javascripts/vue_shared/components/modal.vue b/app/assets/javascripts/vue_shared/components/modal.vue index 00089dfef38..8227428d8ba 100644 --- a/app/assets/javascripts/vue_shared/components/modal.vue +++ b/app/assets/javascripts/vue_shared/components/modal.vue @@ -1,143 +1,153 @@ <script> -export default { - name: 'modal', + /* eslint-disable vue/require-default-prop */ + export default { + name: 'Modal', - props: { - id: { - type: String, - required: false, + props: { + id: { + type: String, + required: false, + }, + title: { + type: String, + required: false, + }, + text: { + type: String, + required: false, + }, + hideFooter: { + type: Boolean, + required: false, + default: false, + }, + kind: { + type: String, + required: false, + default: 'primary', + }, + modalDialogClass: { + type: String, + required: false, + default: '', + }, + closeKind: { + type: String, + required: false, + default: 'default', + }, + closeButtonLabel: { + type: String, + required: false, + default: 'Cancel', + }, + primaryButtonLabel: { + type: String, + required: false, + default: '', + }, + submitDisabled: { + type: Boolean, + required: false, + default: false, + }, }, - title: { - type: String, - required: false, - }, - text: { - type: String, - required: false, - }, - hideFooter: { - type: Boolean, - required: false, - default: false, - }, - kind: { - type: String, - required: false, - default: 'primary', - }, - modalDialogClass: { - type: String, - required: false, - default: '', - }, - closeKind: { - type: String, - required: false, - default: 'default', - }, - closeButtonLabel: { - type: String, - required: false, - default: 'Cancel', - }, - primaryButtonLabel: { - type: String, - required: false, - default: '', - }, - submitDisabled: { - type: Boolean, - required: false, - default: false, - }, - }, - computed: { - btnKindClass() { - return { - [`btn-${this.kind}`]: true, - }; + computed: { + btnKindClass() { + return { + [`btn-${this.kind}`]: true, + }; + }, + btnCancelKindClass() { + return { + [`btn-${this.closeKind}`]: true, + }; + }, }, - btnCancelKindClass() { - return { - [`btn-${this.closeKind}`]: true, - }; - }, - }, - methods: { - emitCancel(event) { - this.$emit('cancel', event); - }, - emitSubmit(event) { - this.$emit('submit', event); + methods: { + emitCancel(event) { + this.$emit('cancel', event); + }, + emitSubmit(event) { + this.$emit('submit', event); + }, }, - }, -}; + }; </script> <template> -<div class="modal-open"> - <div - :id="id" - class="modal" - :class="id ? '' : 'show'" - role="dialog" - tabindex="-1" - > + <div class="modal-open"> <div - :class="modalDialogClass" - class="modal-dialog" - role="document" + :id="id" + class="modal" + :class="id ? '' : 'show'" + role="dialog" + tabindex="-1" > - <div class="modal-content"> - <div class="modal-header"> - <slot name="header"> - <h4 class="modal-title pull-left"> - {{this.title}} - </h4> + <div + :class="modalDialogClass" + class="modal-dialog" + role="document" + > + <div class="modal-content"> + <div class="modal-header"> + <slot name="header"> + <h4 class="modal-title pull-left"> + {{ title }} + </h4> + <button + type="button" + class="close pull-right" + @click="emitCancel($event)" + data-dismiss="modal" + aria-label="Close" + > + <span aria-hidden="true">×</span> + </button> + </slot> + </div> + <div class="modal-body"> + <slot + name="body" + :text="text" + > + <p>{{ text }}</p> + </slot> + </div> + <div + class="modal-footer" + v-if="!hideFooter" + > <button type="button" - class="close pull-right" + class="btn" + :class="btnCancelKindClass" @click="emitCancel($event)" data-dismiss="modal" - aria-label="Close" > - <span aria-hidden="true">×</span> + {{ closeButtonLabel }} </button> - </slot> - </div> - <div class="modal-body"> - <slot name="body" :text="text"> - <p>{{this.text}}</p> - </slot> - </div> - <div class="modal-footer" v-if="!hideFooter"> - <button - type="button" - class="btn pull-left" - :class="btnCancelKindClass" - @click="emitCancel($event)" - data-dismiss="modal"> - {{ closeButtonLabel }} - </button> - <button - v-if="primaryButtonLabel" - type="button" - class="btn pull-right js-primary-button" - :disabled="submitDisabled" - :class="btnKindClass" - @click="emitSubmit($event)" - data-dismiss="modal"> - {{ primaryButtonLabel }} - </button> + <button + v-if="primaryButtonLabel" + type="button" + class="btn js-primary-button" + :disabled="submitDisabled" + :class="btnKindClass" + @click="emitSubmit($event)" + data-dismiss="modal" + > + {{ primaryButtonLabel }} + </button> + </div> </div> </div> </div> + <div + v-if="!id" + class="modal-backdrop fade in" + > + </div> </div> - <div - v-if="!id" - class="modal-backdrop fade in"> - </div> -</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 a2ddd565170..cb8e6072a9b 100644 --- a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue +++ b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue @@ -45,7 +45,7 @@ this.$emit('onChangeTab', tab.scope); }, }, -}; + }; </script> <template> <ul class="nav-links scrolling-tabs"> @@ -55,21 +55,20 @@ :class="{ active: tab.isActive, }" - > + > <a role="button" @click="onTabClick(tab)" :class="`js-${scope}-tab-${tab.scope}`" - > + > {{ tab.name }} <span v-if="shouldRenderBadge(tab.count)" class="badge" - > - {{tab.count}} + > + {{ tab.count }} </span> - </a> </li> </ul> diff --git a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue index e467ca56704..50b1508691b 100644 --- a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue @@ -20,16 +20,16 @@ import userAvatarLink from '../user_avatar/user_avatar_link.vue'; export default { - name: 'placeholderNote', + name: 'PlaceholderNote', + components: { + userAvatarLink, + }, props: { note: { type: Object, required: true, }, }, - components: { - userAvatarLink, - }, computed: { ...mapGetters([ 'getUserData', @@ -46,7 +46,7 @@ :link-href="getUserData.path" :img-src="getUserData.avatar_url" :img-size="40" - /> + /> </div> <div :class="{ discussion: !note.individual_note }" @@ -54,14 +54,14 @@ <div class="note-header"> <div class="note-header-info"> <a :href="getUserData.path"> - <span class="hidden-xs">{{getUserData.name}}</span> - <span class="note-headline-light">@{{getUserData.username}}</span> + <span class="hidden-xs">{{ getUserData.name }}</span> + <span class="note-headline-light">@{{ getUserData.username }}</span> </a> </div> </div> <div class="note-body"> <div class="note-text"> - <p>{{note.body}}</p> + <p>{{ note.body }}</p> </div> </div> </div> diff --git a/app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue index d805fea8006..95e2b38e292 100644 --- a/app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue @@ -8,7 +8,7 @@ * /> */ export default { - name: 'placeholderSystemNote', + name: 'PlaceholderSystemNote', props: { note: { type: Object, @@ -20,10 +20,10 @@ <template> <li class="note system-note timeline-entry being-posted fade-in-half"> - <div class="timeline-entry-inner"> - <div class="timeline-content"> - <em>{{note.body}}</em> - </div> - </div> + <div class="timeline-entry-inner"> + <div class="timeline-content"> + <em>{{ note.body }}</em> + </div> + </div> </li> </template> diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue index 2248699c399..aac10f84087 100644 --- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue @@ -21,16 +21,16 @@ import { spriteIcon } from '../../../lib/utils/common_utils'; export default { - name: 'systemNote', + name: 'SystemNote', + components: { + noteHeader, + }, props: { note: { type: Object, required: true, }, }, - components: { - noteHeader, - }, computed: { ...mapGetters([ 'targetNoteHash', diff --git a/app/assets/javascripts/vue_shared/components/panel_resizer.vue b/app/assets/javascripts/vue_shared/components/panel_resizer.vue index 4371534d345..abbe9a22717 100644 --- a/app/assets/javascripts/vue_shared/components/panel_resizer.vue +++ b/app/assets/javascripts/vue_shared/components/panel_resizer.vue @@ -1,87 +1,87 @@ <script> -export default { - props: { - startSize: { - type: Number, - required: true, + export default { + props: { + startSize: { + type: Number, + required: true, + }, + side: { + type: String, + required: true, + }, + minSize: { + type: Number, + required: false, + default: 0, + }, + maxSize: { + type: Number, + required: false, + default: Number.MAX_VALUE, + }, + enabled: { + type: Boolean, + required: false, + default: true, + }, }, - side: { - type: String, - required: true, + data() { + return { + size: this.startSize, + }; }, - minSize: { - type: Number, - required: false, - default: 0, + computed: { + className() { + return `drag${this.side}`; + }, + cursorStyle() { + if (this.enabled) { + return { cursor: 'ew-resize' }; + } + return {}; + }, }, - maxSize: { - type: Number, - required: false, - default: Number.MAX_VALUE, - }, - enabled: { - type: Boolean, - required: false, - default: true, - }, - }, - data() { - return { - size: this.startSize, - }; - }, - computed: { - className() { - return `drag${this.side}`; - }, - cursorStyle() { - if (this.enabled) { - return { cursor: 'ew-resize' }; - } - return {}; - }, - }, - methods: { - resetSize(e) { - e.preventDefault(); - this.size = this.startSize; - this.$emit('update:size', this.size); - }, - startDrag(e) { - if (this.enabled) { + methods: { + resetSize(e) { e.preventDefault(); - this.startPos = e.clientX; - this.currentStartSize = this.size; - document.addEventListener('mousemove', this.drag); - document.addEventListener('mouseup', this.endDrag, { once: true }); - this.$emit('resize-start', this.size); - } - }, - drag(e) { - e.preventDefault(); - let moved = e.clientX - this.startPos; - if (this.side === 'left') moved = -moved; - let newSize = this.currentStartSize + moved; - if (newSize < this.minSize) { - newSize = this.minSize; - } else if (newSize > this.maxSize) { - newSize = this.maxSize; - } - this.size = newSize; + this.size = this.startSize; + this.$emit('update:size', this.size); + }, + startDrag(e) { + if (this.enabled) { + e.preventDefault(); + this.startPos = e.clientX; + this.currentStartSize = this.size; + document.addEventListener('mousemove', this.drag); + document.addEventListener('mouseup', this.endDrag, { once: true }); + this.$emit('resize-start', this.size); + } + }, + drag(e) { + e.preventDefault(); + let moved = e.clientX - this.startPos; + if (this.side === 'left') moved = -moved; + let newSize = this.currentStartSize + moved; + if (newSize < this.minSize) { + newSize = this.minSize; + } else if (newSize > this.maxSize) { + newSize = this.maxSize; + } + this.size = newSize; - this.$emit('update:size', newSize); - }, - endDrag(e) { - e.preventDefault(); - document.removeEventListener('mousemove', this.drag); - this.$emit('resize-end', this.size); + this.$emit('update:size', newSize); + }, + endDrag(e) { + e.preventDefault(); + document.removeEventListener('mousemove', this.drag); + this.$emit('resize-end', this.size); + }, }, - }, -}; + }; </script> <template> - <div + <div class="dragHandle" :class="className" :style="cursorStyle" diff --git a/app/assets/javascripts/vue_shared/components/pikaday.vue b/app/assets/javascripts/vue_shared/components/pikaday.vue index d8d974a2ff7..bfeece12077 100644 --- a/app/assets/javascripts/vue_shared/components/pikaday.vue +++ b/app/assets/javascripts/vue_shared/components/pikaday.vue @@ -3,7 +3,7 @@ import { parsePikadayDate, pikadayToString } from '../../lib/utils/datefix'; export default { - name: 'datePicker', + name: 'DatePicker', props: { label: { type: String, @@ -13,22 +13,17 @@ selectedDate: { type: Date, required: false, + default: null, }, minDate: { type: Date, required: false, + default: null, }, maxDate: { type: Date, required: false, - }, - }, - methods: { - selected(dateText) { - this.$emit('newDateSelected', this.calendar.toString(dateText)); - }, - toggled() { - this.$emit('hidePicker'); + default: null, }, }, mounted() { @@ -53,6 +48,14 @@ beforeDestroy() { this.calendar.destroy(); }, + methods: { + selected(dateText) { + this.$emit('newDateSelected', this.calendar.toString(dateText)); + }, + toggled() { + this.$emit('hidePicker'); + }, + }, }; </script> @@ -66,7 +69,7 @@ @click="toggled" > <span class="dropdown-toggle-text"> - {{label}} + {{ label }} </span> <i class="fa fa-chevron-down" diff --git a/app/assets/javascripts/vue_shared/components/project_avatar/image.vue b/app/assets/javascripts/vue_shared/components/project_avatar/image.vue index dce23bd65f6..279cc1de5bb 100644 --- a/app/assets/javascripts/vue_shared/components/project_avatar/image.vue +++ b/app/assets/javascripts/vue_shared/components/project_avatar/image.vue @@ -1,85 +1,85 @@ <script> -/* This is a re-usable vue component for rendering a project avatar that - does not need to link to the project's profile. The image and an optional - tooltip can be configured by props passed to this component. + /* This is a re-usable vue component for rendering a project avatar that + does not need to link to the project's profile. The image and an optional + tooltip can be configured by props passed to this component. - Sample configuration: + Sample configuration: - <project-avatar-image - :lazy="true" - :img-src="projectAvatarSrc" - :img-alt="tooltipText" - :tooltip-text="tooltipText" - tooltip-placement="top" - /> + <project-avatar-image + :lazy="true" + :img-src="projectAvatarSrc" + :img-alt="tooltipText" + :tooltip-text="tooltipText" + tooltip-placement="top" + /> -*/ + */ -import defaultAvatarUrl from 'images/no_avatar.png'; -import { placeholderImage } from '../../../lazy_loader'; -import tooltip from '../../directives/tooltip'; + import defaultAvatarUrl from 'images/no_avatar.png'; + import { placeholderImage } from '../../../lazy_loader'; + import tooltip from '../../directives/tooltip'; -export default { - name: 'ProjectAvatarImage', - props: { - lazy: { - type: Boolean, - required: false, - default: false, - }, - imgSrc: { - type: String, - required: false, - default: defaultAvatarUrl, - }, - cssClasses: { - type: String, - required: false, - default: '', - }, - imgAlt: { - type: String, - required: false, - default: 'project avatar', - }, - size: { - type: Number, - required: false, - default: 20, - }, - tooltipText: { - type: String, - required: false, - default: '', - }, - tooltipPlacement: { - type: String, - required: false, - default: 'top', - }, - }, - directives: { - tooltip, - }, - computed: { - // API response sends null when gravatar is disabled and - // we provide an empty string when we use it inside project avatar link. - // In both cases we should render the defaultAvatarUrl - sanitizedSource() { - return this.imgSrc === '' || this.imgSrc === null ? defaultAvatarUrl : this.imgSrc; - }, - resultantSrcAttribute() { - return this.lazy ? placeholderImage : this.sanitizedSource; + export default { + name: 'ProjectAvatarImage', + directives: { + tooltip, }, - tooltipContainer() { - return this.tooltipText ? 'body' : null; + props: { + lazy: { + type: Boolean, + required: false, + default: false, + }, + imgSrc: { + type: String, + required: false, + default: defaultAvatarUrl, + }, + cssClasses: { + type: String, + required: false, + default: '', + }, + imgAlt: { + type: String, + required: false, + default: 'project avatar', + }, + size: { + type: Number, + required: false, + default: 20, + }, + tooltipText: { + type: String, + required: false, + default: '', + }, + tooltipPlacement: { + type: String, + required: false, + default: 'top', + }, }, - avatarSizeClass() { - return `s${this.size}`; + computed: { + // API response sends null when gravatar is disabled and + // we provide an empty string when we use it inside project avatar link. + // In both cases we should render the defaultAvatarUrl + sanitizedSource() { + return this.imgSrc === '' || this.imgSrc === null ? defaultAvatarUrl : this.imgSrc; + }, + resultantSrcAttribute() { + return this.lazy ? placeholderImage : this.sanitizedSource; + }, + tooltipContainer() { + return this.tooltipText ? 'body' : null; + }, + avatarSizeClass() { + return `s${this.size}`; + }, }, - }, -}; + }; </script> <template> @@ -87,7 +87,7 @@ export default { v-tooltip class="avatar" :class="{ - lazy, + lazy: lazy, [avatarSizeClass]: true, [cssClasses]: true }" diff --git a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue index 16d60bb2876..c35621c9ef3 100644 --- a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue +++ b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue @@ -1,85 +1,86 @@ <script> -import modal from './modal.vue'; + import modal from './modal.vue'; -export default { - name: 'recaptcha-modal', + export default { + name: 'RecaptchaModal', - props: { - html: { - type: String, - required: false, - default: '', + components: { + modal, }, - }, - data() { - return { - script: {}, - scriptSrc: 'https://www.google.com/recaptcha/api.js', - }; - }, + props: { + html: { + type: String, + required: false, + default: '', + }, + }, - components: { - modal, - }, + data() { + return { + script: {}, + scriptSrc: 'https://www.google.com/recaptcha/api.js', + }; + }, - methods: { - appendRecaptchaScript() { - this.removeRecaptchaScript(); + watch: { + html() { + this.appendRecaptchaScript(); + }, + }, - const script = document.createElement('script'); - script.src = this.scriptSrc; - script.classList.add('js-recaptcha-script'); - script.async = true; - script.defer = true; + mounted() { + window.recaptchaDialogCallback = this.submit.bind(this); + }, - this.script = script; + methods: { + appendRecaptchaScript() { + this.removeRecaptchaScript(); - document.body.appendChild(script); - }, + const script = document.createElement('script'); + script.src = this.scriptSrc; + script.classList.add('js-recaptcha-script'); + script.async = true; + script.defer = true; - removeRecaptchaScript() { - if (this.script instanceof Element) this.script.remove(); - }, + this.script = script; - close() { - this.removeRecaptchaScript(); - this.$emit('close'); - }, + document.body.appendChild(script); + }, - submit() { - this.$el.querySelector('form').submit(); - }, - }, + removeRecaptchaScript() { + if (this.script instanceof Element) this.script.remove(); + }, - watch: { - html() { - this.appendRecaptchaScript(); - }, - }, + close() { + this.removeRecaptchaScript(); + this.$emit('close'); + }, - mounted() { - window.recaptchaDialogCallback = this.submit.bind(this); - }, -}; + submit() { + this.$el.querySelector('form').submit(); + }, + }, + }; </script> <template> -<modal - kind="warning" - class="recaptcha-modal js-recaptcha-modal" - :hide-footer="true" - :title="__('Please solve the reCAPTCHA')" - @cancel="close" -> - <div slot="body"> - <p> - {{__('We want to be sure it is you, please confirm you are not a robot.')}} - </p> - <div - ref="recaptcha" - v-html="html" - ></div> - </div> -</modal> + <modal + kind="warning" + class="recaptcha-modal js-recaptcha-modal" + :hide-footer="true" + :title="__('Please solve the reCAPTCHA')" + @cancel="close" + > + <div slot="body"> + <p> + {{ __('We want to be sure it is you, please confirm you are not a robot.') }} + </p> + <div + ref="recaptcha" + v-html="html" + > + </div> + </div> + </modal> </template> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue index a88e1310131..7f1eb6bcec4 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue @@ -1,6 +1,6 @@ <script> export default { - name: 'collapsedCalendarIcon', + name: 'CollapsedCalendarIcon', props: { containerClass: { type: String, diff --git a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue index 9ede5553bc5..dac438a702d 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue @@ -4,7 +4,11 @@ import collapsedCalendarIcon from './collapsed_calendar_icon.vue'; export default { - name: 'sidebarCollapsedGroupedDatePicker', + name: 'SidebarCollapsedGroupedDatePicker', + components: { + toggleSidebar, + collapsedCalendarIcon, + }, props: { collapsed: { type: Boolean, @@ -19,10 +23,12 @@ minDate: { type: Date, required: false, + default: null, }, maxDate: { type: Date, required: false, + default: null, }, disableClickableIcons: { type: Boolean, @@ -30,10 +36,6 @@ default: false, }, }, - components: { - toggleSidebar, - collapsedCalendarIcon, - }, computed: { hasMinAndMaxDates() { return this.minDate && this.maxDate; diff --git a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue index 9c3413377a3..1413dd69f24 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue @@ -6,7 +6,13 @@ import { dateInWords } from '../../../lib/utils/datetime_utility'; export default { - name: 'sidebarDatePicker', + name: 'SidebarDatePicker', + components: { + datePicker, + toggleSidebar, + loadingIcon, + collapsedCalendarIcon, + }, props: { collapsed: { type: Boolean, @@ -36,14 +42,17 @@ selectedDate: { type: Date, required: false, + default: null, }, minDate: { type: Date, required: false, + default: null, }, maxDate: { type: Date, required: false, + default: null, }, }, data() { @@ -51,12 +60,6 @@ editing: false, }; }, - components: { - datePicker, - toggleSidebar, - loadingIcon, - collapsedCalendarIcon, - }, computed: { selectedAndEditable() { return this.selectedDate && this.editable; diff --git a/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue b/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue index 5ae76adad71..8211d425b1f 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue @@ -1,6 +1,6 @@ <script> export default { - name: 'toggleSidebar', + name: 'ToggleSidebar', props: { collapsed: { type: Boolean, @@ -24,7 +24,11 @@ <i aria-label="toggle collapse" class="fa" - :class="{ 'fa-angle-double-right': !collapsed, 'fa-angle-double-left': collapsed }" - ></i> + :class="{ + 'fa-angle-double-right': !collapsed, + 'fa-angle-double-left': collapsed + }" + > + </i> </button> </template> diff --git a/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue b/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue new file mode 100644 index 00000000000..86f06c8d266 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue @@ -0,0 +1,127 @@ +<script> +import tooltip from '~/vue_shared/directives/tooltip'; + +export default { + directives: { + tooltip, + }, + props: { + cssClass: { + type: String, + required: false, + default: '', + }, + successLabel: { + type: String, + required: true, + }, + failureLabel: { + type: String, + required: true, + }, + neutralLabel: { + type: String, + required: true, + }, + successCount: { + type: Number, + required: true, + }, + failureCount: { + type: Number, + required: true, + }, + totalCount: { + type: Number, + required: true, + }, + }, + computed: { + neutralCount() { + return this.totalCount - this.successCount - this.failureCount; + }, + successPercent() { + return this.getPercent(this.successCount); + }, + successBarStyle() { + return this.barStyle(this.successPercent); + }, + successTooltip() { + return this.getTooltip(this.successLabel, this.successCount); + }, + failurePercent() { + return this.getPercent(this.failureCount); + }, + failureBarStyle() { + return this.barStyle(this.failurePercent); + }, + failureTooltip() { + return this.getTooltip(this.failureLabel, this.failureCount); + }, + neutralPercent() { + return this.getPercent(this.neutralCount); + }, + neutralBarStyle() { + return this.barStyle(this.neutralPercent); + }, + neutralTooltip() { + return this.getTooltip(this.neutralLabel, this.neutralCount); + }, + }, + methods: { + getPercent(count) { + return Math.ceil((count / this.totalCount) * 100); + }, + barStyle(percent) { + return `width: ${percent}%;`; + }, + getTooltip(label, count) { + return `${label}: ${count}`; + }, + }, +}; +</script> + +<template> + <div + class="stacked-progress-bar" + :class="cssClass" + > + <span + v-if="!totalCount" + class="status-unavailable" + > + {{ __("Not available") }} + </span> + <span + v-tooltip + v-if="successPercent" + class="status-green" + data-placement="bottom" + :title="successTooltip" + :style="successBarStyle" + > + {{ successPercent }}% + </span> + <span + v-tooltip + v-if="neutralPercent" + class="status-neutral" + data-placement="bottom" + :title="neutralTooltip" + :style="neutralBarStyle" + > + {{ neutralPercent }}% + </span> + <span + v-tooltip + v-if="failurePercent" + class="status-red" + data-placement="bottom" + :title="failureTooltip" + :style="failureBarStyle" + > + {{ failurePercent }}% + </span> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.vue b/app/assets/javascripts/vue_shared/components/table_pagination.vue index 33096b53cf8..c44c606a8b2 100644 --- a/app/assets/javascripts/vue_shared/components/table_pagination.vue +++ b/app/assets/javascripts/vue_shared/components/table_pagination.vue @@ -1,132 +1,125 @@ <script> -import { s__ } from '../../locale'; - -const PAGINATION_UI_BUTTON_LIMIT = 4; -const UI_LIMIT = 6; -const SPREAD = '...'; -const PREV = s__('Pagination|Prev'); -const NEXT = s__('Pagination|Next'); -const FIRST = s__('Pagination|« First'); -const LAST = s__('Pagination|Last »'); - -export default { - props: { - /** - This function will take the information given by the pagination component - - Here is an example `change` method: - - change(pagenum) { - gl.utils.visitUrl(`?page=${pagenum}`); + import { s__ } from '../../locale'; + + const PAGINATION_UI_BUTTON_LIMIT = 4; + const UI_LIMIT = 6; + const SPREAD = '...'; + const PREV = s__('Pagination|Prev'); + const NEXT = s__('Pagination|Next'); + const FIRST = s__('Pagination|« First'); + const LAST = s__('Pagination|Last »'); + + export default { + props: { + /** + This function will take the information given by the pagination component + */ + change: { + type: Function, + required: true, }, - */ - change: { - type: Function, - required: true, - }, - /** - pageInfo will come from the headers of the API call - in the `.then` clause of the VueResource API call - there should be a function that contructs the pageInfo for this component - - This is an example: - - const pageInfo = headers => ({ - perPage: +headers['X-Per-Page'], - page: +headers['X-Page'], - total: +headers['X-Total'], - totalPages: +headers['X-Total-Pages'], - nextPage: +headers['X-Next-Page'], - previousPage: +headers['X-Prev-Page'], - }); - */ - pageInfo: { - type: Object, - required: true, - }, - }, - methods: { - changePage(e) { - if (e.target.parentElement.classList.contains('disabled')) return; - - const text = e.target.innerText; - const { totalPages, nextPage, previousPage } = this.pageInfo; - - switch (text) { - case SPREAD: - break; - case LAST: - this.change(totalPages); - break; - case NEXT: - this.change(nextPage); - break; - case PREV: - this.change(previousPage); - break; - case FIRST: - this.change(1); - break; - default: - this.change(+text); - break; - } - }, - }, - computed: { - prev() { - return this.pageInfo.previousPage; - }, - next() { - return this.pageInfo.nextPage; + /** + pageInfo will come from the headers of the API call + in the `.then` clause of the VueResource API call + there should be a function that contructs the pageInfo for this component + + This is an example: + + const pageInfo = headers => ({ + perPage: +headers['X-Per-Page'], + page: +headers['X-Page'], + total: +headers['X-Total'], + totalPages: +headers['X-Total-Pages'], + nextPage: +headers['X-Next-Page'], + previousPage: +headers['X-Prev-Page'], + }); + */ + pageInfo: { + type: Object, + required: true, + }, }, - getItems() { - const total = this.pageInfo.totalPages; - const page = this.pageInfo.page; - const items = []; - - if (page > 1) { - items.push({ title: FIRST, first: true }); - } - - if (page > 1) { - items.push({ title: PREV, prev: true }); - } else { - items.push({ title: PREV, disabled: true, prev: true }); - } - - if (page > UI_LIMIT) items.push({ title: SPREAD, separator: true }); - - const start = Math.max(page - PAGINATION_UI_BUTTON_LIMIT, 1); - const end = Math.min(page + PAGINATION_UI_BUTTON_LIMIT, total); - - for (let i = start; i <= end; i += 1) { - const isActive = i === page; - items.push({ title: i, active: isActive, page: true }); - } - - if (total - page > PAGINATION_UI_BUTTON_LIMIT) { - items.push({ title: SPREAD, separator: true, page: true }); - } - - if (page === total) { - items.push({ title: NEXT, disabled: true, next: true }); - } else if (total - page >= 1) { - items.push({ title: NEXT, next: true }); - } - - if (total - page >= 1) { - items.push({ title: LAST, last: true }); - } - - return items; + computed: { + prev() { + return this.pageInfo.previousPage; + }, + next() { + return this.pageInfo.nextPage; + }, + getItems() { + const total = this.pageInfo.totalPages; + const page = this.pageInfo.page; + const items = []; + + if (page > 1) { + items.push({ title: FIRST, first: true }); + } + + if (page > 1) { + items.push({ title: PREV, prev: true }); + } else { + items.push({ title: PREV, disabled: true, prev: true }); + } + + if (page > UI_LIMIT) items.push({ title: SPREAD, separator: true }); + + const start = Math.max(page - PAGINATION_UI_BUTTON_LIMIT, 1); + const end = Math.min(page + PAGINATION_UI_BUTTON_LIMIT, total); + + for (let i = start; i <= end; i += 1) { + const isActive = i === page; + items.push({ title: i, active: isActive, page: true }); + } + + if (total - page > PAGINATION_UI_BUTTON_LIMIT) { + items.push({ title: SPREAD, separator: true, page: true }); + } + + if (page === total) { + items.push({ title: NEXT, disabled: true, next: true }); + } else if (total - page >= 1) { + items.push({ title: NEXT, next: true }); + } + + if (total - page >= 1) { + items.push({ title: LAST, last: true }); + } + + return items; + }, + showPagination() { + return this.pageInfo.totalPages > 1; + }, }, - showPagination() { - return this.pageInfo.totalPages > 1; + methods: { + changePage(text, isDisabled) { + if (isDisabled) return; + + const { totalPages, nextPage, previousPage } = this.pageInfo; + + switch (text) { + case SPREAD: + break; + case LAST: + this.change(totalPages); + break; + case NEXT: + this.change(nextPage); + break; + case PREV: + this.change(previousPage); + break; + case FIRST: + this.change(1); + break; + default: + this.change(+text); + break; + } + }, }, - }, -}; + }; </script> <template> <div @@ -135,7 +128,8 @@ export default { > <ul class="pagination clearfix"> <li - v-for="item in getItems" + v-for="(item, index) in getItems" + :key="index" :class="{ page: item.page, 'js-previous-button': item.prev, @@ -145,8 +139,11 @@ export default { separator: item.separator, active: item.active, disabled: item.disabled - }"> - <a @click.prevent="changePage($event)">{{item.title}}</a> + }" + > + <a @click.prevent="changePage(item.title, item.disabled)"> + {{ item.title }} + </a> </li> </ul> </div> diff --git a/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue index 3ff7f6e2c4e..bec4e7c99b6 100644 --- a/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue +++ b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue @@ -8,6 +8,12 @@ import '../../lib/utils/datetime_utility'; */ export default { + directives: { + tooltip, + }, + mixins: [ + timeagoMixin, + ], props: { time: { type: String, @@ -26,14 +32,6 @@ export default { default: '', }, }, - - mixins: [ - timeagoMixin, - ], - - directives: { - tooltip, - }, }; </script> <template> @@ -43,6 +41,6 @@ export default { :title="tooltipTitle(time)" :data-placement="tooltipPlacement" data-container="body"> - {{timeFormated(time)}} + {{ timeFormated(time) }} </time> </template> diff --git a/app/assets/javascripts/vue_shared/components/toggle_button.vue b/app/assets/javascripts/vue_shared/components/toggle_button.vue index 4277d9281a0..09031d3ffa1 100644 --- a/app/assets/javascripts/vue_shared/components/toggle_button.vue +++ b/app/assets/javascripts/vue_shared/components/toggle_button.vue @@ -9,15 +9,26 @@ const LABEL_OFF = s__('ToggleButton|Toggle Status: OFF'); export default { + components: { + icon, + loadingIcon, + }, + + model: { + prop: 'value', + event: 'change', + }, + props: { name: { type: String, required: false, - default: '', + default: null, }, value: { type: Boolean, - required: true, + required: false, + default: null, }, disabledInput: { type: Boolean, @@ -31,16 +42,6 @@ }, }, - components: { - icon, - loadingIcon, - }, - - model: { - prop: 'value', - event: 'change', - }, - computed: { toggleIcon() { return this.value ? ICON_ON : ICON_OFF; @@ -61,6 +62,7 @@ <template> <label class="toggle-wrapper"> <input + v-if="name" type="hidden" :name="name" :value="value" diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue index 1ac61a3c39b..cc9cc46bb4c 100644 --- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue +++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue @@ -22,6 +22,9 @@ import tooltip from '../../directives/tooltip'; export default { name: 'UserAvatarImage', + directives: { + tooltip, + }, props: { lazy: { type: Boolean, @@ -59,9 +62,6 @@ export default { default: 'top', }, }, - directives: { - tooltip, - }, computed: { // API response sends null when gravatar is disabled and // we provide an empty string when we use it inside user avatar link. @@ -87,7 +87,7 @@ export default { v-tooltip class="avatar" :class="{ - lazy, + lazy: lazy, [avatarSizeClass]: true, [cssClasses]: true }" diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue index dc32e783258..6955d164def 100644 --- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue +++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue @@ -26,6 +26,9 @@ export default { components: { userAvatarImage, }, + directives: { + tooltip, + }, props: { linkHref: { type: String, @@ -76,9 +79,6 @@ export default { return this.shouldShowUsername ? '' : this.tooltipText; }, }, - directives: { - tooltip, - }, }; </script> @@ -98,6 +98,6 @@ export default { v-tooltip :title="tooltipText" :tooltip-placement="tooltipPlacement" - >{{username}}</span> + >{{ username }}</span> </a> </template> diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_svg.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_svg.vue index d2ff2ac006e..ef3b16edf5f 100644 --- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_svg.vue +++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_svg.vue @@ -39,7 +39,7 @@ export default { :class="avatarSizeClass" :height="size" :width="size" - v-html="svg"> - </svg> + v-html="svg" + /> </template> diff --git a/app/assets/javascripts/zen_mode.js b/app/assets/javascripts/zen_mode.js index 06a86f3b94a..4592003f57e 100644 --- a/app/assets/javascripts/zen_mode.js +++ b/app/assets/javascripts/zen_mode.js @@ -1,5 +1,4 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, consistent-return, camelcase, comma-dangle, max-len, class-methods-use-this */ -/* global Mousetrap */ // Zen Mode (full screen) textarea // @@ -8,9 +7,11 @@ import 'vendor/jquery.scrollTo'; import Dropzone from 'dropzone'; -import 'mousetrap'; +import Mousetrap from 'mousetrap'; import 'mousetrap/plugins/pause/mousetrap-pause'; +Dropzone.autoDiscover = false; + // // ### Events // diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 43b16d3cf7d..cff47ea76ec 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -59,3 +59,4 @@ @import "framework/snippets"; @import "framework/memory_graph"; @import "framework/responsive_tables"; +@import "framework/stacked-progress-bar"; diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss index e0d2ed80de5..a538b5a2946 100644 --- a/app/assets/stylesheets/framework/awards.scss +++ b/app/assets/stylesheets/framework/awards.scss @@ -174,12 +174,13 @@ &.user-authored { cursor: default; - opacity: 0.65; + background-color: $gray-light; + border-color: $theme-gray-200; + color: $gl-text-color-disabled; - &:hover, - &:active { - background-color: $white-light; - border-color: $border-color; + gl-emoji { + opacity: 0.4; + filter: grayscale(100%); } } diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index fcc420923f9..d0b0c69b18f 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -220,14 +220,6 @@ @include btn-with-margin; } - &.disabled { - pointer-events: auto !important; - } - - &[disabled] { - pointer-events: none !important; - } - .fa-caret-down, .fa-chevron-down { margin-left: 5px; @@ -450,3 +442,28 @@ .btn-svg svg { @include btn-svg; } + +// All disabled buttons, regardless of color, type, etc +%disabled { + background-color: $gray-light !important; + border-color: $theme-gray-200 !important; + color: $gl-text-color-disabled !important; + opacity: 1 !important; + cursor: default !important; + + i { + color: $gl-text-color-disabled !important; + } +} + +.btn.disabled, +.btn[disabled], +fieldset[disabled] .btn, +.dropdown-toggle[disabled], +[disabled].dropdown-menu-toggle { + @extend %disabled; + + &:hover { + @extend %disabled; + } +} diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index bc907a390d8..691df098c70 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -28,7 +28,9 @@ .dropdown-menu, .dropdown-menu-nav { @include set-visible; - min-height: 40px; + min-height: $dropdown-min-height; + max-height: $dropdown-max-height; + overflow-y: auto; @media (max-width: $screen-xs-max) { width: 100%; @@ -61,11 +63,6 @@ border-radius: $border-radius-base; white-space: nowrap; - &[disabled] { - opacity: .65; - cursor: not-allowed; - } - &.no-outline { outline: 0; } @@ -664,6 +661,16 @@ } } +.dropdown-create-new-item-button { + @include dropdown-link; + + width: 100%; + background-color: transparent; + border: 0; + text-align: left; + text-overflow: ellipsis; +} + .dropdown-loading { position: absolute; top: 0; diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 1588036aeae..d835d49d8b2 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -18,14 +18,9 @@ margin: $gl-padding 0; &.limited-width-container .file-content { - max-width: $limited-layout-width-sm; + max-width: $limited-layout-width; margin-left: auto; margin-right: auto; - - @media (min-width: $screen-md-min) { - padding-top: 64px; - padding-bottom: 64px; - } } } @@ -128,7 +123,11 @@ } &.wiki { - padding: 30px $gl-padding; + padding: $gl-padding; + + @media (min-width: $screen-md-min) { + padding: $gl-padding * 2; + } } &.blob-no-preview { diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 2d7465401f1..621a4adc0cb 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -260,7 +260,7 @@ } .filtered-search-input-dropdown-menu { - max-height: 260px; + max-height: $dropdown-max-height; max-width: 280px; overflow: auto; diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index ad160f37641..634593aefd0 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -104,7 +104,10 @@ img { height: 28px; - margin-right: 8px; + + + .logo-text { + margin-left: 8px; + } } &.wrap { @@ -300,6 +303,8 @@ .projects-dropdown-menu { padding: 0; + overflow-y: initial; + max-height: initial; } .dropdown-chevron { diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss index e2084e8f85f..30314f3d6cb 100644 --- a/app/assets/stylesheets/framework/icons.scss +++ b/app/assets/stylesheets/framework/icons.scss @@ -26,15 +26,15 @@ } .ci-status-icon-canceled, -.ci-status-icon-disabled, -.ci-status-icon-not-found { +.ci-status-icon-disabled { svg { fill: $gl-text-color; } } .ci-status-icon-created, -.ci-status-icon-skipped { +.ci-status-icon-skipped, +.ci-status-icon-notfound { svg { fill: $gray-darkest; } diff --git a/app/assets/stylesheets/framework/images.scss b/app/assets/stylesheets/framework/images.scss index fd5c3c81a53..2d015ef086b 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; + $image-widths: 250 306 394 430; @each $width in $image-widths { &.svg-#{$width} { img, diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss index 1d8bd26cf1a..d8c57a0e2d9 100644 --- a/app/assets/stylesheets/framework/issue_box.scss +++ b/app/assets/stylesheets/framework/issue_box.scss @@ -24,15 +24,13 @@ font-size: $gl-font-size; line-height: 25px; + &.status-box-closed, &.status-box-mr-closed { background-color: $gl-danger; } - &.status-box-issue-closed { - background-color: $gl-primary; - } - - &.status-box-merged { + &.status-box-issue-closed, + &.status-box-mr-merged { background-color: $gl-primary; } diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss index 3f0268541a4..d107422e517 100644 --- a/app/assets/stylesheets/framework/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -1,10 +1,16 @@ html { overflow-y: scroll; - &.touch .tooltip { display: none !important; } + &.touch .tooltip { + display: none !important; + } } body { + // Improves readability for dyslexic users; supported only in Chrome/Safari so far + // scss-lint:disable PropertySpelling + text-decoration-skip: ink; + // scss-lint:enable PropertySpelling &.navless { background-color: $white-light !important; } @@ -106,10 +112,6 @@ body { } } -.layout-page > .content-wrapper { - min-height: calc(100vh - #{$header-height}); -} - .with-performance-bar .layout-page { margin-top: $header-height + $performance-bar-height; } diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index e12b5aab381..ddd9dbb2be4 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -17,6 +17,8 @@ */ @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 1be66d0ab21..32b9894ae04 100644 --- a/app/assets/stylesheets/framework/modal.scss +++ b/app/assets/stylesheets/framework/modal.scss @@ -1,4 +1,5 @@ .modal-header { + background-color: $modal-body-bg; padding: #{3 * $grid-size} #{2 * $grid-size}; .page-title { @@ -8,8 +9,11 @@ .modal-body { background-color: $modal-body-bg; + line-height: $line-height-base; + min-height: $modal-body-height; position: relative; padding: #{3 * $grid-size} #{2 * $grid-size}; + text-align: left; .form-actions { margin: #{2 * $grid-size} #{-2 * $grid-size} #{-2 * $grid-size}; @@ -20,6 +24,30 @@ } } +.modal-footer { + display: flex; + flex-direction: row; + + .btn + .btn { + margin-left: $grid-size; + } + + @media (max-width: $screen-xs-max) { + flex-direction: column; + + .btn + .btn { + margin-left: 0; + margin-top: $grid-size; + } + } + + @media (min-width: $screen-sm-min) { + .btn:first-of-type { + margin-left: auto; + } + } +} + body.modal-open { overflow: hidden; } @@ -32,12 +60,6 @@ body.modal-open { } } -@media (min-width: $screen-md-min) { - .modal-dialog { - width: 860px; - } -} - @media (min-width: $screen-lg-min) { .modal-full { width: 98%; diff --git a/app/assets/stylesheets/framework/stacked-progress-bar.scss b/app/assets/stylesheets/framework/stacked-progress-bar.scss new file mode 100644 index 00000000000..4869cda73e5 --- /dev/null +++ b/app/assets/stylesheets/framework/stacked-progress-bar.scss @@ -0,0 +1,54 @@ +.stacked-progress-bar { + display: flex; + height: 16px; + border-radius: 10px; + overflow: hidden; + background-color: $theme-gray-100; + + .status-unavailable, + .status-green, + .status-neutral, + .status-red, { + height: 100%; + min-width: 25px; + padding: 0 5px; + font-size: $tooltip-font-size; + font-weight: normal; + color: $white-light; + line-height: 16px; + + &:hover { + cursor: pointer; + } + } + + .status-unavailable { + padding: 0 10px; + color: $theme-gray-700; + } + + .status-green { + background-color: $green-500; + + &:hover { + background-color: $green-600; + } + } + + .status-neutral { + background-color: $theme-gray-200; + color: $gl-gray-dark; + + &:hover { + background-color: $theme-gray-300; + } + } + + .status-red { + background-color: $red-500; + + &:hover { + background-color: $red-600; + } + } +} diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss index a23131e0818..d04e555769b 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss @@ -194,6 +194,6 @@ $modal-body-bg: $white-light; //** Modal footer border color // $modal-footer-border-color: $modal-header-border-color -// $modal-lg: 900px -// $modal-md: 600px +$modal-lg: 860px; +$modal-md: 540px; // $modal-sm: 300px diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index f7853909f56..f76c6866463 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -164,6 +164,7 @@ $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-disabled: #919191; $gl-text-green: $green-600; $gl-text-green-hover: $green-700; $gl-text-red: $red-500; @@ -258,6 +259,8 @@ $general-hover-transition-duration: 100ms; $general-hover-transition-curve: linear; $highlight-changes-color: rgb(235, 255, 232); $performance-bar-height: 35px; +$flash-height: 52px; +$context-header-height: 60px; /* * Common component specific colors @@ -334,7 +337,8 @@ $regular_font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-San * Dropdowns */ $dropdown-width: 300px; -$dropdown-max-height: 215px; +$dropdown-min-height: 40px; +$dropdown-max-height: 312px; $dropdown-vertical-offset: 4px; $dropdown-link-color: #555; $dropdown-link-hover-bg: $row-hover; @@ -732,3 +736,8 @@ $popup-box-shadow-color: rgba(90, 90, 90, 0.05); Multi file editor */ $border-color-settings: #e1e1e1; + +/* +Modals +*/ +$modal-body-height: 134px; diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 0cba223c6a6..17801ed5910 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -195,6 +195,18 @@ .commit-actions { @media (min-width: $screen-sm-min) { font-size: 0; + + div { + display: inline; + } + + .fa-spinner { + font-size: 12px; + } + + span { + font-size: 6px; + } } .ci-status-link { @@ -219,6 +231,11 @@ font-size: 14px; font-weight: $gl-font-weight-bold; } + + .ci-status-icon { + position: relative; + top: 1px; + } } .commit, @@ -310,8 +327,8 @@ } &.invalid { - @include status-color($gray-dark, $gray, $common-gray-dark); - border-color: $common-gray-light; + @include status-color($gray-dark, $gray, $gray-darkest); + border-color: $gray-darkest; } } @@ -335,8 +352,8 @@ &.invalid { svg { - border: 1px solid $common-gray-light; - fill: $common-gray-light; + border: 1px solid $gray-darkest; + fill: $gray-darkest; } } diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index 3b35beb7695..cfef6476d4d 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -117,47 +117,6 @@ top: $gl-padding-top; } - .content-list { - li { - padding: 18px $gl-padding $gl-padding; - - .container-fluid { - padding: 0; - } - } - - .title-col { - p { - margin: 0; - - &.title { - line-height: 19px; - font-size: 14px; - font-weight: $gl-font-weight-bold; - color: $gl-text-color; - } - - &.text { - color: $layout-link-gray; - - &.value-col { - color: $gl-text-color; - } - } - } - } - - .value-col { - text-align: right; - - span { - position: relative; - vertical-align: middle; - top: 3px; - } - } - } - .fa-spinner { font-size: 28px; position: relative; diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 60b07537799..7f037582ca0 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -652,14 +652,18 @@ } .diff-changed-file-name, - .diff-changed-file-path { + .diff-changed-blank-file-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .diff-changed-blank-file-name { + color: $gl-text-color-tertiary; + font-style: italic; + } + .diff-changed-file-path { - direction: rtl; color: $gl-text-color-tertiary; } diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index e1637618ab2..759719a72da 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -162,10 +162,6 @@ border: 0; } - span { - display: inline-block; - } - .select2-container span { margin-top: 0; } @@ -303,7 +299,6 @@ .gutter-toggle { margin-top: 7px; border-left: 1px solid $border-gray-normal; - padding-left: 0; text-align: center; } diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 6d4ccd53e12..4a528bc2bb1 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -391,11 +391,17 @@ .dropdown-toggle { float: right; - .toggle-icon { + i { color: $white-light; padding-right: 2px; margin-top: 2px; } + + &[disabled] { + i { + color: $gl-text-color-disabled; + } + } } .dropdown-menu { @@ -404,7 +410,6 @@ width: 298px; } - @media (max-width: $screen-xs-max) { display: flex; width: 100%; diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 05c1033c5f7..db88d4a16b7 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -48,7 +48,7 @@ } .dropdown-menu { - max-height: 250px; + max-height: $dropdown-max-height; overflow-y: auto; } @@ -69,13 +69,6 @@ border-color: $border-white-normal; } } - - .btn { - .icon-play { - height: 13px; - width: 12px; - } - } } .btn .text-center { @@ -235,7 +228,7 @@ .stage-cell { &.table-section { @media (min-width: $screen-md-min) { - min-width: 148px; + min-width: 160px; /* Hack alert: Without this the mini graph pipeline won't work properly*/ margin-right: -4px; } } @@ -798,7 +791,6 @@ button.mini-pipeline-graph-dropdown-toggle { // link to the build .mini-pipeline-graph-dropdown-item { - padding: 3px 7px 4px; align-items: center; clear: both; display: flex; @@ -993,3 +985,11 @@ button.mini-pipeline-graph-dropdown-toggle { font-weight: $gl-font-weight-normal; line-height: 1.5; } + +.legend-all { + color: $gl-text-color-secondary; +} + +.legend-success { + color: $green-500; +} diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 6f4c678c4b8..bf41005b6d5 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -322,13 +322,6 @@ } } -.project-repo-buttons { - .project-action-button .dropdown-menu { - max-height: 250px; - overflow-y: auto; - } -} - .split-one { display: inline-table; margin-right: 12px; @@ -902,17 +895,6 @@ pre.light-well { } } -.create-new-protected-branch-button, -.create-new-protected-tag-button { - @include dropdown-link; - - width: 100%; - background-color: transparent; - border: 0; - text-align: left; - text-overflow: ellipsis; -} - .protected-branches-list, .protected-tags-list { margin-bottom: 30px; diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss index 6cb32408a48..8265b8370f7 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/pages/repo.scss @@ -16,12 +16,6 @@ display: inline-block; } -@media (min-width: $screen-md-min) { - .blob-viewer[data-type="rich"] { - margin: 20px; - } -} - .ide-view { display: flex; height: calc(100vh - #{$header-height}); @@ -113,6 +107,11 @@ table.table tr td.multi-file-table-name { vertical-align: middle; margin-right: 2px; } + + .loading-container { + margin-right: 4px; + display: inline-block; + } } .multi-file-table-col-commit-message { @@ -253,7 +252,6 @@ table.table tr td.multi-file-table-name { display: flex; position: relative; flex-direction: column; - height: 100%; width: 290px; padding: 0; background-color: $gray-light; @@ -262,6 +260,11 @@ table.table tr td.multi-file-table-name { .projects-sidebar { display: flex; flex-direction: column; + + .context-header { + width: auto; + margin-right: 0; + } } .multi-file-commit-panel-inner { @@ -502,19 +505,70 @@ table.table tr td.multi-file-table-name { } } -.ide-flash-container.flash-container { - margin-top: $header-height; - margin-bottom: 0; +.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; + } + + .content { + margin-top: $header-height; + } + + .multi-file-commit-panel .multi-file-commit-panel-inner-scroll { + max-height: calc(100vh - #{$header-height + $context-header-height}); + } + + &.flash-shown { + .content { + 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}); + } + } } -.with-performance-bar { - .ide-flash-container.flash-container { - margin-top: $header-height + $performance-bar-height; +.with-performance-bar .ide.nav-only { + .flash-container { + margin-top: #{$header-height + $performance-bar-height}; + } + + .content { + margin-top: #{$header-height + $performance-bar-height}; } .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 { + 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}); + } + } } diff --git a/app/assets/stylesheets/pages/xterm.scss b/app/assets/stylesheets/pages/xterm.scss index c7297a34ad8..7d40c61da26 100644 --- a/app/assets/stylesheets/pages/xterm.scss +++ b/app/assets/stylesheets/pages/xterm.scss @@ -3,22 +3,21 @@ // see also: https://gist.github.com/jasonm23/2868981 $black: #000; - $red: #cd0000; - $green: #00cd00; - $yellow: #cdcd00; - $blue: #00e; // according to wikipedia, this is the xterm standard - //$blue: #1e90ff; // this is used by all the terminals I tried (when configured with the xterm color profile) - $magenta: #cd00cd; - $cyan: #00cdcd; - $white: #e5e5e5; + $red: #ea1010; + $green: #009900; + $yellow: #999900; + $blue: #0073e6; + $magenta: #d411d4; + $cyan: #009999; + $white: #ccc; $l-black: #373b41; - $l-red: #c66; - $l-green: #b5bd68; - $l-yellow: #f0c674; - $l-blue: #81a2be; - $l-magenta: #b294bb; - $l-cyan: #8abeb7; - $l-white: $gray-darkest; + $l-red: #ff6161; + $l-green: #00d600; + $l-yellow: #bdbd00; + $l-blue: #5797ff; + $l-magenta: #d96dd9; + $l-cyan: #00bdbd; + $l-white: #fff; /* * xterm colors |