loading up the forgejo repo on tangled to test page performance
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Improve AJAX link and modal confirm dialog (#25210)

Clarify the "link-action" behavior:

> // A "link-action" can post AJAX request to its "data-url"
> // Then the browser is redirect to: the "redirect" in response, or
"data-redirect" attribute, or current URL by reloading.

And enhance the "link-action" to support showing a modal dialog for
confirm. A similar general approach could also help PRs like
https://github.com/go-gitea/gitea/pull/22344#discussion_r1062883436

> // If the "link-action" has "data-modal-confirm(-html)" attribute, a
confirm modal dialog will be shown before taking action.


And a lot of duplicate code can be removed now. A good framework design
can help to avoid code copying&pasting.

---------

Co-authored-by: silverwind <me@silverwind.io>

authored by

wxiaoguang
silverwind
and committed by
GitHub
6bbccdd1 a51b115b

+86 -157
+1 -1
templates/admin/user/edit.tmpl
··· 186 186 187 187 <div class="field"> 188 188 <button class="ui green button">{{$.locale.Tr "settings.update_avatar"}}</button> 189 - <a class="ui red button delete-post" data-request-url="{{.Link}}/avatar/delete" data-done-url="{{.Link}}">{{$.locale.Tr "settings.delete_current_avatar"}}</a>{{/* TODO: Convert links without href to buttons for a11y */}} 189 + <button class="ui red button link-action" data-url="{{.Link}}/avatar/delete" data-redirect="{{.Link}}">{{$.locale.Tr "settings.delete_current_avatar"}}</button> 190 190 </div> 191 191 </form> 192 192 </div>
+7 -5
templates/base/head_script.tmpl
··· 32 32 mermaidMaxSourceCharacters: {{MermaidMaxSourceCharacters}}, 33 33 {{/* this global i18n object should only contain general texts. for specialized texts, it should be provided inside the related modules by: (1) API response (2) HTML data-attribute (3) PageData */}} 34 34 i18n: { 35 - copy_success: '{{.locale.Tr "copy_success"}}', 36 - copy_error: '{{.locale.Tr "copy_error"}}', 37 - error_occurred: '{{.locale.Tr "error.occurred"}}', 38 - network_error: '{{.locale.Tr "error.network_error"}}', 39 - remove_label_str: '{{.locale.Tr "remove_label_str"}}', 35 + copy_success: {{.locale.Tr "copy_success"}}, 36 + copy_error: {{.locale.Tr "copy_error"}}, 37 + error_occurred: {{.locale.Tr "error.occurred"}}, 38 + network_error: {{.locale.Tr "error.network_error"}}, 39 + remove_label_str: {{.locale.Tr "remove_label_str"}}, 40 + modal_confirm: {{.locale.Tr "modal.confirm"}}, 41 + modal_cancel: {{.locale.Tr "modal.cancel"}}, 40 42 }, 41 43 }; 42 44 {{/* in case some pages don't render the pageData, we make sure it is an object to prevent null access */}}
+1 -1
templates/org/settings/options.tmpl
··· 90 90 91 91 <div class="field"> 92 92 <button class="ui green button">{{$.locale.Tr "settings.update_avatar"}}</button> 93 - <a class="ui red button delete-post" data-request-url="{{.Link}}/avatar/delete" data-done-url="{{.Link}}">{{$.locale.Tr "settings.delete_current_avatar"}}</a> 93 + <button class="ui red button link-action" data-url="{{.Link}}/avatar/delete" data-redirect="{{.Link}}">{{$.locale.Tr "settings.delete_current_avatar"}}</button> 94 94 </div> 95 95 </form> 96 96 </div>
+4 -6
templates/org/team/members.tmpl
··· 9 9 {{template "org/team/navbar" .}} 10 10 {{if .IsOrganizationOwner}} 11 11 <div class="ui attached segment"> 12 - <form class="ui form ignore-dirty" id="add-member-form" action="{{$.OrgLink}}/teams/{{$.Team.LowerName | PathEscape}}/action/add" method="post"> 12 + <form class="ui form ignore-dirty gt-df gt-fw gt-gap-3" action="{{$.OrgLink}}/teams/{{$.Team.LowerName | PathEscape}}/action/add" method="post"> 13 13 {{.CsrfTokenHtml}} 14 14 <input type="hidden" name="uid" value="{{.SignedUser.ID}}"> 15 - <div class="inline field ui left"> 16 - <div id="search-user-box" class="ui search"{{if .IsEmailInviteEnabled}} data-allow-email="true" data-allow-email-description="{{.locale.Tr "org.teams.invite_team_member" $.Team.Name}}"{{end}}> 17 - <div class="ui input"> 18 - <input class="prompt" name="uname" placeholder="{{.locale.Tr "repo.settings.search_user_placeholder"}}" autocomplete="off" required> 19 - </div> 15 + <div id="search-user-box" class="ui search gt-mr-3"{{if .IsEmailInviteEnabled}} data-allow-email="true" data-allow-email-description="{{.locale.Tr "org.teams.invite_team_member" $.Team.Name}}"{{end}}> 16 + <div class="ui input"> 17 + <input class="prompt" name="uname" placeholder="{{.locale.Tr "repo.settings.search_user_placeholder"}}" autocomplete="off" required> 20 18 </div> 21 19 </div> 22 20 <button class="ui green button">{{.locale.Tr "org.teams.add_team_member"}}</button>
+12 -40
templates/org/team/repositories.tmpl
··· 9 9 {{template "org/team/navbar" .}} 10 10 {{$canAddRemove := and $.IsOrganizationOwner (not $.Team.IncludesAllRepositories)}} 11 11 {{if $canAddRemove}} 12 - <div class="ui attached segment" id="repo-top-segment"> 13 - <div class="inline ui field left"> 14 - <form class="ui form ignore-dirty" id="add-repo-form" action="{{$.OrgLink}}/teams/{{$.Team.LowerName | PathEscape}}/action/repo/add" method="post"> 15 - {{.CsrfTokenHtml}} 16 - <div class="inline field ui left"> 17 - <div id="search-repo-box" data-uid="{{.Org.ID}}" class="ui search"> 18 - <div class="ui input"> 19 - <input class="prompt" name="repo_name" placeholder="{{.locale.Tr "org.teams.search_repo_placeholder"}}" autocomplete="off" required> 20 - </div> 21 - </div> 12 + <div class="ui attached segment gt-df gt-fw gt-gap-3"> 13 + <form class="ui form ignore-dirty gt-f1 gt-dif" action="{{$.OrgLink}}/teams/{{$.Team.LowerName | PathEscape}}/action/repo/add" method="post"> 14 + {{.CsrfTokenHtml}} 15 + <div id="search-repo-box" data-uid="{{.Org.ID}}" class="ui search"> 16 + <div class="ui input"> 17 + <input class="prompt" name="repo_name" placeholder="{{.locale.Tr "org.teams.search_repo_placeholder"}}" autocomplete="off" required> 22 18 </div> 23 - <button class="ui green button">{{.locale.Tr "add"}}</button> 24 - </form> 25 - </div> 26 - <div class="inline ui field right"> 27 - <form class="ui form" id="repo-multiple-form" action="{{$.OrgLink}}/teams/{{$.Team.LowerName | PathEscape}}/repositories" method="post"> 28 - <button class="ui green button add-all-button" data-modal-id="org-team-add-all-repo" data-url="{{$.OrgLink}}/teams/{{$.Team.LowerName | PathEscape}}/action/repo/addall">{{.locale.Tr "add_all"}}</button> 29 - <button class="ui red button delete-button" data-modal-id="org-team-remove-all-repo" data-url="{{$.OrgLink}}/teams/{{$.Team.LowerName | PathEscape}}/action/repo/removeall">{{.locale.Tr "remove_all"}}</button> 30 - </form> 19 + </div> 20 + <button class="ui green button gt-ml-3">{{.locale.Tr "add"}}</button> 21 + </form> 22 + <div class="gt-dib"> 23 + <button class="ui green button link-action" data-modal-confirm="{{.locale.Tr "org.teams.add_all_repos_desc"}}" data-url="{{$.OrgLink}}/teams/{{$.Team.LowerName | PathEscape}}/action/repo/addall">{{.locale.Tr "add_all"}}</button> 24 + <button class="ui red button link-action" data-modal-confirm="{{.locale.Tr "org.teams.remove_all_repos_desc"}}" data-url="{{$.OrgLink}}/teams/{{$.Team.LowerName | PathEscape}}/action/repo/removeall">{{.locale.Tr "remove_all"}}</button> 31 25 </div> 32 26 </div> 33 27 {{end}} ··· 62 56 </div> 63 57 </div> 64 58 </div> 65 - </div> 66 - 67 - <div class="ui g-modal-confirm delete modal" id="org-team-remove-all-repo"> 68 - <div class="header"> 69 - {{svg "octicon-trash"}} 70 - {{.locale.Tr "org.teams.remove_all_repos_title"}} 71 - </div> 72 - <div class="content"> 73 - <p>{{.locale.Tr "org.teams.remove_all_repos_desc"}}</p> 74 - </div> 75 - {{template "base/modal_actions_confirm" .}} 76 - </div> 77 - 78 - <div class="ui g-modal-confirm addall modal" id="org-team-add-all-repo"> 79 - <div class="header"> 80 - {{svg "octicon-globe"}} 81 - {{.locale.Tr "org.teams.add_all_repos_title"}} 82 - </div> 83 - <div class="content"> 84 - <p>{{.locale.Tr "org.teams.add_all_repos_desc"}}</p> 85 - </div> 86 - {{template "base/modal_actions_confirm" .}} 87 59 </div> 88 60 89 61 {{template "base/footer" .}}
+1 -1
templates/repo/branch/list.tmpl
··· 165 165 {{end}} 166 166 {{if and $.IsWriter (not $.IsMirror) (not $.Repository.IsArchived) (not .IsProtected)}} 167 167 {{if .IsDeleted}} 168 - <button class="btn interact-bg gt-p-3 undo-button" data-url="{{$.Link}}/restore?branch_id={{.DeletedBranch.ID}}&name={{.DeletedBranch.Name}}&page={{$.Page.Paginater.Current}}" data-tooltip-content="{{$.locale.Tr "repo.branch.restore" (.Name)}}"> 168 + <button class="btn interact-bg gt-p-3 link-action restore-branch-button" data-url="{{$.Link}}/restore?branch_id={{.DeletedBranch.ID}}&name={{.DeletedBranch.Name}}&page={{$.Page.Paginater.Current}}" data-tooltip-content="{{$.locale.Tr "repo.branch.restore" (.Name)}}"> 169 169 <span class="text blue"> 170 170 {{svg "octicon-reply"}} 171 171 </span>
+1 -1
templates/repo/settings/options.tmpl
··· 61 61 62 62 <div class="field"> 63 63 <button class="ui green button">{{$.locale.Tr "settings.update_avatar"}}</button> 64 - <a class="ui red button delete-post" data-request-url="{{.Link}}/avatar/delete" data-done-url="{{.Link}}">{{$.locale.Tr "settings.delete_current_avatar"}}</a> 64 + <button class="ui red button link-action" data-url="{{.Link}}/avatar/delete" data-redirect="{{.Link}}">{{$.locale.Tr "settings.delete_current_avatar"}}</button> 65 65 </div> 66 66 </form> 67 67
+1 -1
templates/user/settings/profile.tmpl
··· 125 125 126 126 <div class="field"> 127 127 <button class="ui green button">{{$.locale.Tr "settings.update_avatar"}}</button> 128 - <button class="ui red button delete-post" data-request-url="{{.Link}}/avatar/delete" data-done-url="{{.Link}}">{{$.locale.Tr "settings.delete_current_avatar"}}</button> 128 + <button class="ui red button link-action" data-url="{{.Link}}/avatar/delete" data-redirect="{{.Link}}">{{$.locale.Tr "settings.delete_current_avatar"}}</button> 129 129 </div> 130 130 </form> 131 131 </div>
+1 -1
tests/integration/branches_test.go
··· 34 34 func TestUndoDeleteBranch(t *testing.T) { 35 35 onGiteaRun(t, func(t *testing.T, u *url.URL) { 36 36 deleteBranch(t) 37 - htmlDoc, name := branchAction(t, ".undo-button") 37 + htmlDoc, name := branchAction(t, ".restore-branch-button") 38 38 assert.Contains(t, 39 39 htmlDoc.doc.Find(".ui.positive.message").Text(), 40 40 translation.NewLocale("en-US").Tr("repo.branch.restore_success", name),
-23
web_src/css/org.css
··· 205 205 margin: 0; 206 206 } 207 207 208 - .organization.teams #add-repo-form input, 209 - .organization.teams #repo-multiple-form input, 210 - .organization.teams #add-member-form input { 211 - margin-left: 0; 212 - } 213 - 214 - .organization.teams #add-repo-form .ui.button, 215 - .organization.teams #repo-multiple-form .ui.button, 216 - .organization.teams #add-member-form .ui.button { 217 - margin-left: 5px; 218 - margin-top: -3px; 219 - } 220 - 221 - .organization.teams #repo-top-segment { 222 - height: 60px; 223 - } 224 - 225 - @media (max-width: 767.98px) { 226 - .organization.teams #repo-top-segment { 227 - height: 100px; 228 - } 229 - } 230 - 231 208 .org-team-navbar .active.item { 232 209 background: var(--color-box-body) !important; 233 210 }
+57 -77
web_src/js/features/common-global.js
··· 8 8 import {hideElem, showElem, toggleElem} from '../utils/dom.js'; 9 9 import {htmlEscape} from 'escape-goat'; 10 10 11 - const {appUrl, csrfToken} = window.config; 11 + const {appUrl, csrfToken, i18n} = window.config; 12 12 13 13 export function initGlobalFormDirtyLeaveConfirm() { 14 14 // Warn users that try to leave a page after entering data into a form. ··· 172 172 } 173 173 } 174 174 175 + function linkAction(e) { 176 + e.preventDefault(); 177 + 178 + // A "link-action" can post AJAX request to its "data-url" 179 + // Then the browser is redirect to: the "redirect" in response, or "data-redirect" attribute, or current URL by reloading. 180 + // If the "link-action" has "data-modal-confirm(-html)" attribute, a confirm modal dialog will be shown before taking action. 181 + 182 + const $this = $(e.target); 183 + const redirect = $this.attr('data-redirect'); 184 + 185 + const request = () => { 186 + $this.prop('disabled', true); 187 + $.post($this.attr('data-url'), { 188 + _csrf: csrfToken 189 + }).done((data) => { 190 + if (data && data.redirect) { 191 + window.location.href = data.redirect; 192 + } else if (redirect) { 193 + window.location.href = redirect; 194 + } else { 195 + window.location.reload(); 196 + } 197 + }).always(() => { 198 + $this.prop('disabled', false); 199 + }); 200 + }; 201 + 202 + const modalConfirmHtml = htmlEscape($this.attr('data-modal-confirm') || ''); 203 + if (!modalConfirmHtml) { 204 + request(); 205 + return; 206 + } 207 + 208 + const okButtonColor = $this.hasClass('red') || $this.hasClass('yellow') || $this.hasClass('orange') || $this.hasClass('negative') ? 'orange' : 'green'; 209 + 210 + const $modal = $(` 211 + <div class="ui g-modal-confirm modal"> 212 + <div class="content">${modalConfirmHtml}</div> 213 + <div class="actions"> 214 + <button class="ui basic cancel button">${svg('octicon-x')} ${i18n.modal_cancel}</button> 215 + <button class="ui ${okButtonColor} ok button">${svg('octicon-check')} ${i18n.modal_confirm}</button> 216 + </div> 217 + </div> 218 + `); 219 + 220 + $modal.appendTo(document.body); 221 + $modal.modal({ 222 + onApprove() { 223 + request(); 224 + }, 225 + onHidden() { 226 + $modal.remove(); 227 + }, 228 + }).modal('show'); 229 + } 230 + 175 231 export function initGlobalLinkActions() { 176 232 function showDeletePopup(e) { 177 233 e.preventDefault(); ··· 217 273 }).modal('show'); 218 274 } 219 275 220 - function showAddAllPopup(e) { 221 - e.preventDefault(); 222 - const $this = $(this); 223 - let filter = ''; 224 - if ($this.attr('data-modal-id')) { 225 - filter += `#${$this.attr('data-modal-id')}`; 226 - } 227 - 228 - const dialog = $(`.addall.modal${filter}`); 229 - dialog.find('.name').text($this.data('name')); 230 - 231 - dialog.modal({ 232 - closable: false, 233 - onApprove() { 234 - if ($this.data('type') === 'form') { 235 - $($this.data('form')).trigger('submit'); 236 - return; 237 - } 238 - 239 - $.post($this.data('url'), { 240 - _csrf: csrfToken, 241 - id: $this.data('id') 242 - }).done((data) => { 243 - window.location.href = data.redirect; 244 - }); 245 - } 246 - }).modal('show'); 247 - } 248 - 249 - function linkAction(e) { 250 - e.preventDefault(); 251 - const $this = $(this); 252 - const redirect = $this.data('redirect'); 253 - $this.prop('disabled', true); 254 - $.post($this.data('url'), { 255 - _csrf: csrfToken 256 - }).done((data) => { 257 - if (data.redirect) { 258 - window.location.href = data.redirect; 259 - } else if (redirect) { 260 - window.location.href = redirect; 261 - } else { 262 - window.location.reload(); 263 - } 264 - }).always(() => { 265 - $this.prop('disabled', false); 266 - }); 267 - } 268 - 269 276 // Helpers. 270 277 $('.delete-button').on('click', showDeletePopup); 271 278 $('.link-action').on('click', linkAction); 272 - 273 - // FIXME: this function is only used once, and not common, not well designed. should be refactored later 274 - $('.add-all-button').on('click', showAddAllPopup); 275 - 276 - // FIXME: this is only used once, and should be replace with `link-action` instead 277 - $('.undo-button').on('click', function () { 278 - const $this = $(this); 279 - $this.prop('disabled', true); 280 - $.post($this.data('url'), { 281 - _csrf: csrfToken, 282 - id: $this.data('id') 283 - }).done((data) => { 284 - window.location.href = data.redirect; 285 - }).always(() => { 286 - $this.prop('disabled', false); 287 - }); 288 - }); 289 279 } 290 280 291 281 export function initGlobalButtons() { ··· 345 335 if (colorPickers.length > 0) { 346 336 initCompColorPicker(); 347 337 } 348 - }); 349 - 350 - $('.delete-post.button').on('click', function (e) { 351 - e.preventDefault(); 352 - const $this = $(this); 353 - $.post($this.attr('data-request-url'), { 354 - _csrf: csrfToken 355 - }).done(() => { 356 - window.location.href = $this.attr('data-done-url'); 357 - }); 358 338 }); 359 339 } 360 340