From ac0e7163dd90274b7a76e52732833a9cdb08885f Mon Sep 17 00:00:00 2001 From: Ralph Slooten Date: Wed, 12 Jul 2023 17:21:51 +1200 Subject: [PATCH 1/2] UI: Pagination support for search, all results --- server/apiv1/api.go | 8 ++-- server/apiv1/structs.go | 10 ++++- server/server_test.go | 2 +- server/ui-src/App.vue | 52 ++++++++++++------------ server/ui-src/templates/Message.vue | 61 ++++++++++++++++++++--------- storage/database.go | 32 +++++++++++---- storage/database_test.go | 10 ++--- storage/search.go | 15 +------ 8 files changed, 115 insertions(+), 75 deletions(-) diff --git a/server/apiv1/api.go b/server/apiv1/api.go index ed1128f..01f2be3 100644 --- a/server/apiv1/api.go +++ b/server/apiv1/api.go @@ -62,10 +62,11 @@ func GetMessages(w http.ResponseWriter, r *http.Request) { res.Start = start res.Messages = messages - res.Count = len(messages) + res.Count = len(messages) // legacy - now undocumented in API specs res.Total = stats.Total res.Unread = stats.Unread res.Tags = stats.Tags + res.MessagesCount = stats.Total bytes, _ := json.Marshal(res) w.Header().Add("Content-Type", "application/json") @@ -109,7 +110,7 @@ func Search(w http.ResponseWriter, r *http.Request) { start, limit := getStartLimit(r) - messages, err := storage.Search(search, start, limit) + messages, results, err := storage.Search(search, start, limit) if err != nil { httpError(w, err.Error()) return @@ -121,8 +122,9 @@ func Search(w http.ResponseWriter, r *http.Request) { res.Start = start res.Messages = messages - res.Count = len(messages) + res.Count = results // legacy - now undocumented in API specs res.Total = stats.Total + res.MessagesCount = results res.Unread = stats.Unread res.Tags = stats.Tags diff --git a/server/apiv1/structs.go b/server/apiv1/structs.go index ec6f01e..cfe78a5 100644 --- a/server/apiv1/structs.go +++ b/server/apiv1/structs.go @@ -12,9 +12,17 @@ type MessagesSummary struct { // Total number of unread messages in mailbox Unread int `json:"unread"` - // Number of results returned + // Legacy - now undocumented in API specs but left for backwards compatibility. + // Removed from API documentation 2023-07-12 + // swagger:ignore Count int `json:"count"` + // Total number of messages matching current query + MessagesCount int `json:"messages_count"` + + // // Number of results returned on current page + // Count int `json:"count"` + // Pagination offset Start int `json:"start"` diff --git a/server/server_test.go b/server/server_test.go index a489efd..fc41dd1 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -195,7 +195,7 @@ func assertSearchEqual(t *testing.T, uri, query string, count int) { return } - assertEqual(t, count, m.Count, "wrong search results count") + assertEqual(t, count, m.MessagesCount, "wrong search results count") } func insertEmailData(t *testing.T) { diff --git a/server/ui-src/App.vue b/server/ui-src/App.vue index 93afc71..89e35ac 100644 --- a/server/ui-src/App.vue +++ b/server/ui-src/App.vue @@ -26,6 +26,7 @@ export default { limit: 50, total: 0, unread: 0, + messagesCount: 0, start: 0, count: 0, tags: [], @@ -75,7 +76,7 @@ export default { return this.start > 0 }, canNext: function () { - return this.total > (this.start + this.count) + return this.messagesCount > (this.start + this.count) }, unreadInSearch: function () { if (!this.searching) { @@ -146,11 +147,12 @@ export default { let uri = 'api/v1/messages' if (self.search) { self.searching = true - self.items = [] uri = 'api/v1/search' - self.start = 0 // search is displayed on one page params['query'] = self.search - params['limit'] = 200 + params['limit'] = self.limit + if (self.start > 0) { + params['start'] = self.start + } } else { self.searching = false params['limit'] = self.limit @@ -162,7 +164,8 @@ export default { self.get(uri, params, function (response) { self.total = response.data.total self.unread = response.data.unread - self.count = response.data.count + self.count = response.data.messages.length + self.messagesCount = response.data.messages_count self.start = response.data.start self.items = response.data.messages self.tags = response.data.tags @@ -194,6 +197,7 @@ export default { doSearch: function (e) { e.preventDefault() + this.start = 0 this.loadMessages() }, @@ -204,13 +208,14 @@ export default { } this.search = 'tag:' + tag window.location.hash = "" + this.start = 0 this.loadMessages() }, resetSearch: function (e) { e.preventDefault() this.search = '' - this.scrollInPlace = true + this.start = 0 this.loadMessages() }, @@ -459,9 +464,9 @@ export default { // websocket connect connect: function () { - let wsproto = location.protocol == 'https:' ? 'wss' : 'ws' + let proto = location.protocol == 'https:' ? 'wss' : 'ws' let ws = new WebSocket( - wsproto + "://" + document.location.host + document.location.pathname + "api/events" + proto + "://" + document.location.host + document.location.pathname + "api/events" ) let self = this ws.onmessage = function (e) { @@ -809,7 +814,7 @@ export default { - - - {{ formatNumber(items.length) }} result - - - - {{ formatNumber(start + 1) }}-{{ formatNumber(start + items.length) }} of - {{ formatNumber(total) }} - - - - + + {{ formatNumber(start + 1) }}-{{ formatNumber(start + items.length) }} of + {{ formatNumber(messagesCount) }} + + +
diff --git a/server/ui-src/templates/Message.vue b/server/ui-src/templates/Message.vue index be3a77a..467b637 100644 --- a/server/ui-src/templates/Message.vue +++ b/server/ui-src/templates/Message.vue @@ -23,11 +23,12 @@ export default { return { srcURI: false, iframes: [], // for resizing - showTags: false, // to force rerendering of component + showTags: false, // to force re-rendering of component + canSaveTags: false, // prevent auto-saving tags on render messageTags: [], allTags: [], loadHeaders: false, - showMobileBtns: false, + showMobileButtons: false, scaleHTMLPreview: 'display', // keys names match bootstrap icon names responsiveSizes: { @@ -39,20 +40,25 @@ export default { }, watch: { + // handle changes to the URL messageID message: { handler() { let self = this self.showTags = false + self.canSaveTags = false self.messageTags = self.message.Tags self.allTags = self.existingTags self.loadHeaders = false - self.scaleHTMLPreview = 'display';// default view + self.scaleHTMLPreview = 'display' // default view // delay to select first tab and add HTML highlighting (prev/next) self.$nextTick(function () { self.renderUI() self.showTags = true self.$nextTick(function () { Tags.init("select[multiple]") + window.setTimeout(function () { + self.canSaveTags = true + }, 200) }) }) }, @@ -60,8 +66,8 @@ export default { immediate: true }, messageTags() { - // save changed to tags - if (this.showTags) { + // save changes to tags + if (this.canSaveTags) { this.saveTags() } }, @@ -78,6 +84,7 @@ export default { mounted() { let self = this self.showTags = false + self.canSaveTags = false self.allTags = self.existingTags window.addEventListener("resize", self.resizeIframes) self.renderUI() @@ -95,7 +102,12 @@ export default { self.showTags = true self.$nextTick(function () { - Tags.init("select[multiple]") + self.$nextTick(function () { + Tags.init('select[multiple]') + window.setTimeout(function () { + self.canSaveTags = true + }, 200) + }) }) }, @@ -104,6 +116,10 @@ export default { }, methods: { + isHTMLTabSelected: function () { + this.showMobileButtons = this.$refs.navhtml + && this.$refs.navhtml.classList.contains('active') + }, renderUI: function () { let self = this // click the first non-disabled tab @@ -111,6 +127,14 @@ export default { document.activeElement.blur() // blur focus document.getElementById('message-view').scrollTop = 0 + self.isHTMLTabSelected() + + document.querySelectorAll('button[data-bs-toggle="tab"]').forEach(function (listObj) { + listObj.addEventListener('shown.bs.tab', function (event) { + self.isHTMLTabSelected() + }) + }) + // delay 0.2s until vue has rendered the iframe content window.setTimeout(function () { let p = document.getElementById('preview-html') @@ -311,29 +335,30 @@ export default {