Handle invalid issues (#18111)

* Handle invalid issues

- When you hover over a issue reference, and the issue doesn't exist, it
will just hang on the loading animation.
- This patch fixes that by showing them the pop-up with a "Error
occured" message.

* Add I18N

* refactor

* fix comment for lint

* fix unit test for i18n

* fix unit test for i18n

* add comments

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
Gusted 2021-12-28 13:28:27 +00:00 committed by GitHub
parent d2fac636d1
commit e4e3df6c66
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 45 additions and 23 deletions

View file

@ -207,7 +207,7 @@ func TestAPIGetReleaseByTag(t *testing.T) {
var err *api.APIError var err *api.APIError
DecodeJSON(t, resp, &err) DecodeJSON(t, resp, &err)
assert.EqualValues(t, "Not Found", err.Message) assert.NotEmpty(t, err.Message)
} }
func TestAPIDeleteReleaseByTagName(t *testing.T) { func TestAPIDeleteReleaseByTagName(t *testing.T) {

View file

@ -141,7 +141,7 @@ func TestCreateBranchInvalidCSRF(t *testing.T) {
resp = session.MakeRequest(t, NewRequest(t, "GET", loc), http.StatusOK) resp = session.MakeRequest(t, NewRequest(t, "GET", loc), http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body) htmlDoc := NewHTMLParser(t, resp.Body)
assert.Equal(t, assert.Equal(t,
"Bad Request: Invalid CSRF token", "Bad Request: invalid CSRF token",
strings.TrimSpace(htmlDoc.doc.Find(".ui.message").Text()), strings.TrimSpace(htmlDoc.doc.Find(".ui.message").Text()),
) )
} }

View file

@ -30,6 +30,11 @@ type APIContext struct {
Org *APIOrganization Org *APIOrganization
} }
// Currently, we have the following common fields in error response:
// * message: the message for end users (it shouldn't be used for error type detection)
// if we need to indicate some errors, we should introduce some new fields like ErrorCode or ErrorType
// * url: the swagger document URL
// APIError is error format response // APIError is error format response
// swagger:response error // swagger:response error
type APIError struct { type APIError struct {
@ -47,8 +52,8 @@ type APIValidationError struct {
// APIInvalidTopicsError is error format response to invalid topics // APIInvalidTopicsError is error format response to invalid topics
// swagger:response invalidTopicsError // swagger:response invalidTopicsError
type APIInvalidTopicsError struct { type APIInvalidTopicsError struct {
Topics []string `json:"invalidTopics"`
Message string `json:"message"` Message string `json:"message"`
InvalidTopics []string `json:"invalidTopics"`
} }
//APIEmpty is an empty response //APIEmpty is an empty response
@ -122,9 +127,9 @@ func (ctx *APIContext) InternalServerError(err error) {
}) })
} }
var ( type apiContextKeyType struct{}
apiContextKey interface{} = "default_api_context"
) var apiContextKey = apiContextKeyType{}
// WithAPIContext set up api context in request // WithAPIContext set up api context in request
func WithAPIContext(req *http.Request, ctx *APIContext) *http.Request { func WithAPIContext(req *http.Request, ctx *APIContext) *http.Request {
@ -351,7 +356,7 @@ func ReferencesGitRepo(allowEmpty bool) func(http.Handler) http.Handler {
// NotFound handles 404s for APIContext // NotFound handles 404s for APIContext
// String will replace message, errors will be added to a slice // String will replace message, errors will be added to a slice
func (ctx *APIContext) NotFound(objs ...interface{}) { func (ctx *APIContext) NotFound(objs ...interface{}) {
var message = "Not Found" var message = ctx.Tr("error.not_found")
var errors []string var errors []string
for _, obj := range objs { for _, obj := range objs {
// Ignore nil // Ignore nil
@ -368,7 +373,7 @@ func (ctx *APIContext) NotFound(objs ...interface{}) {
ctx.JSON(http.StatusNotFound, map[string]interface{}{ ctx.JSON(http.StatusNotFound, map[string]interface{}{
"message": message, "message": message,
"documentation_url": setting.API.SwaggerURL, "url": setting.API.SwaggerURL,
"errors": errors, "errors": errors,
}) })
} }

View file

@ -104,10 +104,12 @@ error404 = The page you are trying to reach either <strong>does not exist</stron
never = Never never = Never
[error] [error]
occurred = An error has occurred occurred = An error occurred
report_message = If you are sure this is a Gitea bug, please search for issue on <a href="https://github.com/go-gitea/gitea/issues">GitHub</a> and open new issue if necessary. report_message = If you are sure this is a Gitea bug, please search for issues on <a href="https://github.com/go-gitea/gitea/issues" target="_blank">GitHub</a> or open a new issue if necessary.
missing_csrf = Bad Request: no CSRF token present missing_csrf = Bad Request: no CSRF token present
invalid_csrf = Bad Request: Invalid CSRF token invalid_csrf = Bad Request: invalid CSRF token
not_found = The target couldn't be found.
network_error = Network error
[startpage] [startpage]
app_desc = A painless, self-hosted Git service app_desc = A painless, self-hosted Git service

View file

@ -46,10 +46,13 @@
]).values()), ]).values()),
{{end}} {{end}}
mermaidMaxSourceCharacters: {{MermaidMaxSourceCharacters}}, mermaidMaxSourceCharacters: {{MermaidMaxSourceCharacters}},
{{/* this global i18n object should only contain gereral texts. for specalized texts, it should be provied inside the related modules by: (1) API response (2) HTML data-attribute (3) PageData */}}
i18n: { i18n: {
copy_success: '{{.i18n.Tr "copy_success"}}', copy_success: '{{.i18n.Tr "copy_success"}}',
copy_error: '{{.i18n.Tr "copy_error"}}', copy_error: '{{.i18n.Tr "copy_error"}}',
} error_occurred: '{{.i18n.Tr "error.occurred"}}',
network_error: '{{.i18n.Tr "error.network_error"}}',
},
}; };
{{/* in case some pages don't render the pageData, we make sure it is an object to prevent null access */}} {{/* in case some pages don't render the pageData, we make sure it is an object to prevent null access */}}
window.config.pageData = window.config.pageData || {}; window.config.pageData = window.config.pageData || {};

View file

@ -16,13 +16,17 @@
</div> </div>
</div> </div>
</div> </div>
<div v-if="!loading && issue === null">
<p><small>{{ i18nErrorOccurred }}</small></p>
<p>{{ i18nErrorMessage }}</p>
</div>
</div> </div>
</template> </template>
<script> <script>
import {SvgIcon} from '../svg.js'; import {SvgIcon} from '../svg.js';
const {appSubUrl} = window.config; const {appSubUrl, i18n} = window.config;
// NOTE: see models/issue_label.go for similar implementation // NOTE: see models/issue_label.go for similar implementation
const srgbToLinear = (color) => { const srgbToLinear = (color) => {
@ -49,7 +53,9 @@ export default {
data: () => ({ data: () => ({
loading: false, loading: false,
issue: null issue: null,
i18nErrorOccurred: i18n.error_occurred,
i18nErrorMessage: null,
}), }),
computed: { computed: {
@ -112,14 +118,20 @@ export default {
methods: { methods: {
load(data, callback) { load(data, callback) {
this.loading = true; this.loading = true;
$.get(`${appSubUrl}/api/v1/repos/${data.owner}/${data.repo}/issues/${data.index}`, (issue) => { this.i18nErrorMessage = null;
$.get(`${appSubUrl}/api/v1/repos/${data.owner}/${data.repo}/issues/${data.index}`).done((issue) => {
this.issue = issue; this.issue = issue;
this.loading = false; }).fail((jqXHR) => {
this.$nextTick(() => { if (jqXHR.responseJSON && jqXHR.responseJSON.message) {
if (callback) { this.i18nErrorMessage = jqXHR.responseJSON.message;
callback(); } else {
this.i18nErrorMessage = i18n.network_error;
}
}).always(() => {
this.loading = false;
if (callback) {
this.$nextTick(callback);
} }
});
}); });
} }
} }