Skip to content

Commit a43ea22

Browse files
HesterGwxiaoguang
andauthored
Change form actions to fetch for submit review box (#25219)
Co-author: @wxiaoguang Close #25096 The way to fix it in this PR is to change form submit to fetch using formData, and add flags to avoid post repeatedly. Should be able to apply to more forms that have the same issue after this PR. In the demo below, 'approve' is clicked several times, and then 'comment' is clicked several time after 'request changes' clicked. After: https://github.com/go-gitea/gitea/assets/17645053/beabeb1d-fe66-4b76-b048-4f022b4e83a0 Update: screenshots from /devtest > ![image](https://user-images.githubusercontent.com/2114189/245680011-ee4231e0-a53d-4c2a-a9c2-71ccd98005cc.png) > > ![image](https://user-images.githubusercontent.com/2114189/245680057-9215d348-63d8-406d-8828-17e171163aaa.png) > > ![image](https://user-images.githubusercontent.com/2114189/245680148-89d7b3d1-d7b6-442f-b69e-eadaee112482.png) --------- Co-authored-by: wxiaoguang <[email protected]>
1 parent 6348823 commit a43ea22

File tree

14 files changed

+208
-36
lines changed

14 files changed

+208
-36
lines changed

modules/context/base.go

+4
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,10 @@ func (b *Base) JSON(status int, content interface{}) {
132132
}
133133
}
134134

135+
func (b *Base) JSONRedirect(redirect string) {
136+
b.JSON(http.StatusOK, map[string]any{"redirect": redirect})
137+
}
138+
135139
// RemoteAddr returns the client machine ip address
136140
func (b *Base) RemoteAddr() string {
137141
return b.Req.RemoteAddr

routers/web/devtest/devtest.go

+10
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,16 @@ func List(ctx *context.Context) {
3232
ctx.HTML(http.StatusOK, "devtest/list")
3333
}
3434

35+
func FetchActionTest(ctx *context.Context) {
36+
_ = ctx.Req.ParseForm()
37+
ctx.Flash.Info(ctx.Req.Method + " " + ctx.Req.RequestURI + "<br>" +
38+
"Form: " + ctx.Req.Form.Encode() + "<br>" +
39+
"PostForm: " + ctx.Req.PostForm.Encode(),
40+
)
41+
time.Sleep(2 * time.Second)
42+
ctx.JSONRedirect("")
43+
}
44+
3545
func Tmpl(ctx *context.Context) {
3646
now := time.Now()
3747
ctx.Data["TimeNow"] = now

routers/web/repo/pull_review.go

+4-5
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ func SubmitReview(ctx *context.Context) {
193193
}
194194
if ctx.HasError() {
195195
ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
196-
ctx.Redirect(fmt.Sprintf("%s/pulls/%d/files", ctx.Repo.RepoLink, issue.Index))
196+
ctx.JSONRedirect(fmt.Sprintf("%s/pulls/%d/files", ctx.Repo.RepoLink, issue.Index))
197197
return
198198
}
199199

@@ -214,7 +214,7 @@ func SubmitReview(ctx *context.Context) {
214214
}
215215

216216
ctx.Flash.Error(translated)
217-
ctx.Redirect(fmt.Sprintf("%s/pulls/%d/files", ctx.Repo.RepoLink, issue.Index))
217+
ctx.JSONRedirect(fmt.Sprintf("%s/pulls/%d/files", ctx.Repo.RepoLink, issue.Index))
218218
return
219219
}
220220
}
@@ -228,14 +228,13 @@ func SubmitReview(ctx *context.Context) {
228228
if err != nil {
229229
if issues_model.IsContentEmptyErr(err) {
230230
ctx.Flash.Error(ctx.Tr("repo.issues.review.content.empty"))
231-
ctx.Redirect(fmt.Sprintf("%s/pulls/%d/files", ctx.Repo.RepoLink, issue.Index))
231+
ctx.JSONRedirect(fmt.Sprintf("%s/pulls/%d/files", ctx.Repo.RepoLink, issue.Index))
232232
} else {
233233
ctx.ServerError("SubmitReview", err)
234234
}
235235
return
236236
}
237-
238-
ctx.Redirect(fmt.Sprintf("%s/pulls/%d#%s", ctx.Repo.RepoLink, issue.Index, comm.HashTag()))
237+
ctx.JSONRedirect(fmt.Sprintf("%s/pulls/%d#%s", ctx.Repo.RepoLink, issue.Index, comm.HashTag()))
239238
}
240239

241240
// DismissReview dismissing stale review by repo admin

routers/web/web.go

+1
Original file line numberDiff line numberDiff line change
@@ -1411,6 +1411,7 @@ func registerRoutes(m *web.Route) {
14111411

14121412
if !setting.IsProd {
14131413
m.Any("/devtest", devtest.List)
1414+
m.Any("/devtest/fetch-action-test", devtest.FetchActionTest)
14141415
m.Any("/devtest/{sub}", devtest.Tmpl)
14151416
}
14161417

templates/devtest/fetch-action.tmpl

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{{template "base/head" .}}
2+
<div class="page-content devtest ui container">
3+
{{template "base/alert" .}}
4+
<div>
5+
<h1>link-action</h1>
6+
<div>
7+
Use "window.fetch" to send a request to backend, the request is defined in an "A" or "BUTTON" element.
8+
It might be renamed to "link-fetch-action" to match the "form-fetch-action".
9+
</div>
10+
<div>
11+
<button class="link-action" data-url="fetch-action-test?k=1">test</button>
12+
</div>
13+
</div>
14+
<div>
15+
<h1>form-fetch-action</h1>
16+
<div>Use "window.fetch" to send a form request to backend</div>
17+
<div>
18+
<form method="get" action="fetch-action-test?k=1" class="form-fetch-action">
19+
<button name="btn">submit get</button>
20+
</form>
21+
<form method="post" action="fetch-action-test?k=1" class="form-fetch-action">
22+
<div><textarea name="text" rows="3" class="js-quick-submit"></textarea></div>
23+
<div><label><input name="check" type="checkbox"> check</label></div>
24+
<div><button name="btn">submit post</button></div>
25+
</form>
26+
<form method="post" action="/no-such-uri" class="form-fetch-action">
27+
<div class="gt-py-5">bad action url</div>
28+
<div><button name="btn">submit test</button></div>
29+
</form>
30+
</div>
31+
</div>
32+
</div>
33+
<style>
34+
.ui.message.flash-message {
35+
text-align: left;
36+
}
37+
.form-fetch-action {
38+
margin-bottom: 1em;
39+
border: 1px red dashed; /* show the border for demo purpose */
40+
}
41+
</style>
42+
{{template "base/footer" .}}

templates/devtest/gitea-ui.tmpl

+11
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,17 @@
8989
<div><span data-tooltip-content="test tooltip" data-tooltip-interactive="true">text with interactive tooltip</span></div>
9090
</div>
9191

92+
<div>
93+
<h1>Loading</h1>
94+
<div class="is-loading small-loading-icon gt-border-secondary gt-py-2"><span>loading ...</span></div>
95+
<div class="is-loading gt-border-secondary gt-py-4">
96+
<p>loading ...</p>
97+
<p>loading ...</p>
98+
<p>loading ...</p>
99+
<p>loading ...</p>
100+
</div>
101+
</div>
102+
92103
<div>
93104
<h1>GiteaOriginUrl</h1>
94105
<div><gitea-origin-url data-url="test/url"></gitea-origin-url></div>

templates/devtest/list.tmpl

+10-7
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1-
<style>
2-
@media (prefers-color-scheme: dark) {
3-
:root {
4-
color-scheme: dark;
5-
}
6-
}
7-
</style>
1+
{{template "base/head" .}}
2+
83
<ul>
94
{{range .SubNames}}
105
<li><a href="{{AppSubUrl}}/devtest/{{.}}">{{.}}</a></li>
116
{{end}}
127
</ul>
8+
9+
<style>
10+
ul {
11+
line-height: 2em;
12+
}
13+
</style>
14+
15+
{{template "base/footer" .}}

templates/repo/diff/new_review.tmpl

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
</button>
77
<div class="review-box-panel tippy-target">
88
<div class="ui segment">
9-
<form class="ui form" action="{{.Link}}/reviews/submit" method="post">
9+
<form class="ui form form-fetch-action" action="{{.Link}}/reviews/submit" method="post">
1010
{{.CsrfTokenHtml}}
1111
<input type="hidden" name="commit_id" value="{{.AfterCommitID}}">
1212
<div class="field gt-df gt-ac">

web_src/css/modules/animations.css

+12-9
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,22 @@
44
}
55

66
.is-loading {
7-
background: transparent !important;
8-
color: transparent !important;
9-
border: transparent !important;
107
pointer-events: none !important;
118
position: relative !important;
129
overflow: hidden !important;
1310
}
1411

12+
.is-loading > * {
13+
opacity: 0.3;
14+
}
15+
1516
.is-loading::after {
1617
content: "";
1718
position: absolute;
1819
display: block;
19-
width: 4rem;
2020
height: 4rem;
21+
max-height: 50%;
22+
aspect-ratio: 1 / 1;
2123
left: 50%;
2224
top: 50%;
2325
transform: translate(-50%, -50%);
@@ -28,30 +30,31 @@
2830
border-radius: 100%;
2931
}
3032

33+
.is-loading.small-loading-icon::after {
34+
border-width: 2px;
35+
}
36+
3137
.markup pre.is-loading,
3238
.editor-loading.is-loading,
3339
.pdf-content.is-loading {
3440
height: var(--height-loading);
3541
}
3642

43+
/* TODO: not needed, use "is-loading small-loading-icon" instead */
3744
.btn-octicon.is-loading::after {
3845
border-width: 2px;
3946
height: 1.25rem;
4047
width: 1.25rem;
4148
}
4249

50+
/* TODO: not needed, use "is-loading small-loading-icon" instead */
4351
code.language-math.is-loading::after {
4452
padding: 0;
4553
border-width: 2px;
4654
width: 1.25rem;
4755
height: 1.25rem;
4856
}
4957

50-
#oauth2-login-navigator.is-loading::after {
51-
width: 40px;
52-
height: 40px;
53-
}
54-
5558
@keyframes fadein {
5659
0% {
5760
opacity: 0;

web_src/css/modules/tippy.css

+6
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@
2929
color: var(--color-text);
3030
}
3131

32+
.tippy-box[data-theme="form-fetch-error"] {
33+
border-color: var(--color-error-border);
34+
background-color: var(--color-error-bg);
35+
color: var(--color-error-text);
36+
}
37+
3238
.tippy-content {
3339
position: relative;
3440
padding: 1rem;

web_src/js/features/common-global.js

+81-3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {handleGlobalEnterQuickSubmit} from './comp/QuickSubmit.js';
77
import {svg} from '../svg.js';
88
import {hideElem, showElem, toggleElem} from '../utils/dom.js';
99
import {htmlEscape} from 'escape-goat';
10+
import {createTippy} from '../modules/tippy.js';
1011

1112
const {appUrl, csrfToken, i18n} = window.config;
1213

@@ -60,6 +61,81 @@ export function initGlobalButtonClickOnEnter() {
6061
});
6162
}
6263

64+
async function formFetchAction(e) {
65+
if (!e.target.classList.contains('form-fetch-action')) return;
66+
67+
e.preventDefault();
68+
const formEl = e.target;
69+
if (formEl.classList.contains('is-loading')) return;
70+
71+
formEl.classList.add('is-loading');
72+
if (formEl.clientHeight < 50) {
73+
formEl.classList.add('small-loading-icon');
74+
}
75+
76+
const formMethod = formEl.getAttribute('method') || 'get';
77+
const formActionUrl = formEl.getAttribute('action');
78+
const formData = new FormData(formEl);
79+
const [submitterName, submitterValue] = [e.submitter?.getAttribute('name'), e.submitter?.getAttribute('value')];
80+
if (submitterName) {
81+
formData.append(submitterName, submitterValue || '');
82+
}
83+
84+
let reqUrl = formActionUrl;
85+
const reqOpt = {method: formMethod.toUpperCase(), headers: {'X-Csrf-Token': csrfToken}};
86+
if (formMethod.toLowerCase() === 'get') {
87+
const params = new URLSearchParams();
88+
for (const [key, value] of formData) {
89+
params.append(key, value.toString());
90+
}
91+
const pos = reqUrl.indexOf('?');
92+
if (pos !== -1) {
93+
reqUrl = reqUrl.slice(0, pos);
94+
}
95+
reqUrl += `?${params.toString()}`;
96+
} else {
97+
reqOpt.body = formData;
98+
}
99+
100+
let errorTippy;
101+
const onError = (msg) => {
102+
formEl.classList.remove('is-loading', 'small-loading-icon');
103+
if (errorTippy) errorTippy.destroy();
104+
errorTippy = createTippy(formEl, {
105+
content: msg,
106+
interactive: true,
107+
showOnCreate: true,
108+
hideOnClick: true,
109+
role: 'alert',
110+
theme: 'form-fetch-error',
111+
trigger: 'manual',
112+
arrow: false,
113+
});
114+
};
115+
116+
const doRequest = async () => {
117+
try {
118+
const resp = await fetch(reqUrl, reqOpt);
119+
if (resp.status === 200) {
120+
const {redirect} = await resp.json();
121+
formEl.classList.remove('dirty'); // remove the areYouSure check before reloading
122+
if (redirect) {
123+
window.location.href = redirect;
124+
} else {
125+
window.location.reload();
126+
}
127+
} else {
128+
onError(`server error: ${resp.status}`);
129+
}
130+
} catch (e) {
131+
onError(e.error);
132+
}
133+
};
134+
135+
// TODO: add "confirm" support like "link-action" in the future
136+
await doRequest();
137+
}
138+
63139
export function initGlobalCommon() {
64140
// Semantic UI modules.
65141
const $uiDropdowns = $('.ui.dropdown');
@@ -114,6 +190,8 @@ export function initGlobalCommon() {
114190
if (btn.classList.contains('loading')) return e.preventDefault();
115191
btn.classList.add('loading');
116192
});
193+
194+
document.addEventListener('submit', formFetchAction);
117195
}
118196

119197
export function initGlobalDropzone() {
@@ -182,7 +260,7 @@ function linkAction(e) {
182260
const $this = $(e.target);
183261
const redirect = $this.attr('data-redirect');
184262

185-
const request = () => {
263+
const doRequest = () => {
186264
$this.prop('disabled', true);
187265
$.post($this.attr('data-url'), {
188266
_csrf: csrfToken
@@ -201,7 +279,7 @@ function linkAction(e) {
201279

202280
const modalConfirmHtml = htmlEscape($this.attr('data-modal-confirm') || '');
203281
if (!modalConfirmHtml) {
204-
request();
282+
doRequest();
205283
return;
206284
}
207285

@@ -220,7 +298,7 @@ function linkAction(e) {
220298
$modal.appendTo(document.body);
221299
$modal.modal({
222300
onApprove() {
223-
request();
301+
doRequest();
224302
},
225303
onHidden() {
226304
$modal.remove();
+14-7
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,24 @@
11
import $ from 'jquery';
22

33
export function handleGlobalEnterQuickSubmit(target) {
4-
const $target = $(target);
5-
const $form = $(target).closest('form');
6-
if ($form.length) {
4+
const form = target.closest('form');
5+
if (form) {
6+
if (!form.checkValidity()) {
7+
form.reportValidity();
8+
return;
9+
}
10+
11+
if (form.classList.contains('form-fetch-action')) {
12+
form.dispatchEvent(new SubmitEvent('submit', {bubbles: true, cancelable: true}));
13+
return;
14+
}
15+
716
// here use the event to trigger the submit event (instead of calling `submit()` method directly)
817
// otherwise the `areYouSure` handler won't be executed, then there will be an annoying "confirm to leave" dialog
9-
if ($form[0].checkValidity()) {
10-
$form.trigger('submit');
11-
}
18+
$(form).trigger('submit');
1219
} else {
1320
// if no form, then the editor is for an AJAX request, dispatch an event to the target, let the target's event handler to do the AJAX request.
1421
// the 'ce-' prefix means this is a CustomEvent
15-
$target.trigger('ce-quick-submit');
22+
target.dispatchEvent(new CustomEvent('ce-quick-submit', {bubbles: true}));
1623
}
1724
}

0 commit comments

Comments
 (0)