mirror of
				https://github.com/axllent/mailpit.git
				synced 2025-10-31 00:07:43 +02:00 
			
		
		
		
	UI: Add option to enable tag colors based on tag name hash
An experimental option to add tag colors (see #127). This will generate a random color for each unique tag
This commit is contained in:
		
							
								
								
									
										6
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -12,6 +12,7 @@ | |||||||
|         "bootstrap": "^5.2.0", |         "bootstrap": "^5.2.0", | ||||||
|         "bootstrap-icons": "^1.9.1", |         "bootstrap-icons": "^1.9.1", | ||||||
|         "bootstrap5-tags": "^1.4.41", |         "bootstrap5-tags": "^1.4.41", | ||||||
|  |         "color-hash": "^2.0.2", | ||||||
|         "moment": "^2.29.4", |         "moment": "^2.29.4", | ||||||
|         "prismjs": "^1.29.0", |         "prismjs": "^1.29.0", | ||||||
|         "rapidoc": "^9.3.4", |         "rapidoc": "^9.3.4", | ||||||
| @@ -1163,6 +1164,11 @@ | |||||||
|         "node": ">=0.10.0" |         "node": ">=0.10.0" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/color-hash": { | ||||||
|  |       "version": "2.0.2", | ||||||
|  |       "resolved": "https://registry.npmjs.org/color-hash/-/color-hash-2.0.2.tgz", | ||||||
|  |       "integrity": "sha512-6exeENAqBTuIR1wIo36mR8xVVBv6l1hSLd7Qmvf6158Ld1L15/dbahR9VUOiX7GmGJBCnQyS0EY+I8x+wa7egg==" | ||||||
|  |     }, | ||||||
|     "node_modules/combined-stream": { |     "node_modules/combined-stream": { | ||||||
|       "version": "1.0.8", |       "version": "1.0.8", | ||||||
|       "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", |       "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", | ||||||
|   | |||||||
| @@ -12,6 +12,7 @@ | |||||||
|     "bootstrap": "^5.2.0", |     "bootstrap": "^5.2.0", | ||||||
|     "bootstrap-icons": "^1.9.1", |     "bootstrap-icons": "^1.9.1", | ||||||
|     "bootstrap5-tags": "^1.4.41", |     "bootstrap5-tags": "^1.4.41", | ||||||
|  |     "color-hash": "^2.0.2", | ||||||
|     "moment": "^2.29.4", |     "moment": "^2.29.4", | ||||||
|     "prismjs": "^1.29.0", |     "prismjs": "^1.29.0", | ||||||
|     "rapidoc": "^9.3.4", |     "rapidoc": "^9.3.4", | ||||||
|   | |||||||
| @@ -1,11 +1,11 @@ | |||||||
| <script> | <script> | ||||||
| import commonMixins from './mixins.js'; | import commonMixins from './mixins.js' | ||||||
| import Message from './templates/Message.vue'; | import Message from './templates/Message.vue' | ||||||
| import MessageSummary from './templates/MessageSummary.vue'; | import MessageSummary from './templates/MessageSummary.vue' | ||||||
| import MessageRelease from './templates/MessageRelease.vue'; | import MessageRelease from './templates/MessageRelease.vue' | ||||||
| import MessageToast from './templates/MessageToast.vue'; | import MessageToast from './templates/MessageToast.vue' | ||||||
| import moment from 'moment'; | import moment from 'moment' | ||||||
| import Tinycon from 'tinycon'; | import Tinycon from 'tinycon' | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
| 	mixins: [commonMixins], | 	mixins: [commonMixins], | ||||||
| @@ -906,9 +906,23 @@ export default { | |||||||
| 			</div> | 			</div> | ||||||
|  |  | ||||||
| 			<template v-if="!selected.length && tags.length && !message"> | 			<template v-if="!selected.length && tags.length && !message"> | ||||||
| 				<h6 class="mt-4 text-muted"><small>Tags</small></h6> | 				<div class="mt-4 text-muted"> | ||||||
| 				<div class="list-group mt-2 mb-5"> | 					<button class="btn btn-sm dropdown-toggle ms-n1" data-bs-toggle="dropdown" aria-expanded="false"> | ||||||
| 					<button class="list-group-item list-group-item-action small" v-for="tag in tags" | 						Tags | ||||||
|  | 					</button> | ||||||
|  | 					<ul class="dropdown-menu dropdown-menu-end"> | ||||||
|  | 						<li> | ||||||
|  | 							<button class="dropdown-item" @click="toggleTagColors()"> | ||||||
|  | 								<template v-if="showTagColors">Hide</template> | ||||||
|  | 								<template v-else>Show</template> | ||||||
|  | 								tag colors | ||||||
|  | 							</button> | ||||||
|  | 						</li> | ||||||
|  | 					</ul> | ||||||
|  | 				</div> | ||||||
|  | 				<div class="list-group mt-1 mb-5"> | ||||||
|  | 					<button class="list-group-item list-group-item-action small px-2" v-for="tag in tags" | ||||||
|  | 						:style="showTagColors ? { borderLeftColor: colorHash(tag), borderLeftWidth: '4px' } : ''" | ||||||
| 						v-on:click="tagSearch($event, tag)" :class="inSearch(tag) ? 'active' : ''"> | 						v-on:click="tagSearch($event, tag)" :class="inSearch(tag) ? 'active' : ''"> | ||||||
| 						<i class="bi bi-tag-fill" v-if="inSearch(tag)"></i> | 						<i class="bi bi-tag-fill" v-if="inSearch(tag)"></i> | ||||||
| 						<i class="bi bi-tag" v-else></i> | 						<i class="bi bi-tag" v-else></i> | ||||||
| @@ -919,7 +933,7 @@ export default { | |||||||
|  |  | ||||||
| 			<MessageSummary v-if="message" :message="message"></MessageSummary> | 			<MessageSummary v-if="message" :message="message"></MessageSummary> | ||||||
|  |  | ||||||
| 			<div class="position-fixed bottom-0 bg-white py-2 text-muted w-100"> | 			<div class="position-fixed bottom-0 bg-white py-2 text-muted small w-100"> | ||||||
| 				<a href="#" class="text-muted" v-on:click="loadInfo"> | 				<a href="#" class="text-muted" v-on:click="loadInfo"> | ||||||
| 					<i class="bi bi-info-circle-fill"></i> | 					<i class="bi bi-info-circle-fill"></i> | ||||||
| 					About | 					About | ||||||
| @@ -927,7 +941,7 @@ export default { | |||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
|  |  | ||||||
| 		<div class="col-lg-10 col-md-9 mh-100 ps-0 pe-0"> | 		<div class="col-lg-10 col-md-9 mh-100 ps-0 ps-md-2 pe-0"> | ||||||
| 			<div class="mh-100" style="overflow-y: auto;" :class="message ? 'd-none' : ''" id="message-page"> | 			<div class="mh-100" style="overflow-y: auto;" :class="message ? 'd-none' : ''" id="message-page"> | ||||||
| 				<div class="list-group my-2" v-if="items.length"> | 				<div class="list-group my-2" v-if="items.length"> | ||||||
| 					<a v-for="message in items" :href="'#' + message.ID" | 					<a v-for="message in items" :href="'#' + message.ID" | ||||||
| @@ -962,7 +976,8 @@ export default { | |||||||
| 						<div class="col-lg-6 col-xxl-7 mt-2 mt-lg-0"> | 						<div class="col-lg-6 col-xxl-7 mt-2 mt-lg-0"> | ||||||
| 							<div><b>{{ message.Subject != "" ? message.Subject : "[ no subject ]" }}</b></div> | 							<div><b>{{ message.Subject != "" ? message.Subject : "[ no subject ]" }}</b></div> | ||||||
| 							<div> | 							<div> | ||||||
| 								<span class="badge text-bg-secondary me-1" v-for="t in message.Tags" | 								<span class="badge me-1" v-for="t in message.Tags" | ||||||
|  | 									:style="showTagColors ? { backgroundColor: colorHash(t) } : { backgroundColor: '#6c757d' }" | ||||||
| 									:title="'Filter messages tagged with ' + t" v-on:click="tagSearch($event, t)"> | 									:title="'Filter messages tagged with ' + t" v-on:click="tagSearch($event, t)"> | ||||||
| 									{{ t }} | 									{{ t }} | ||||||
| 								</span> | 								</span> | ||||||
|   | |||||||
| @@ -5,3 +5,4 @@ $font-family-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetic | |||||||
| $link-decoration: none; | $link-decoration: none; | ||||||
| $primary: #2c3e50; | $primary: #2c3e50; | ||||||
| $list-group-disabled-color: #adb5bd; | $list-group-disabled-color: #adb5bd; | ||||||
|  | $enable-negative-margins: true; | ||||||
|   | |||||||
| @@ -1,22 +1,32 @@ | |||||||
| import axios from 'axios'; | import axios from 'axios' | ||||||
| import { Modal } from 'bootstrap'; | import { Modal } from 'bootstrap' | ||||||
| import moment from 'moment'; | import moment from 'moment' | ||||||
|  | import ColorHash from 'color-hash' | ||||||
|  |  | ||||||
|  |  | ||||||
| // FakeModal is used to return a fake Bootstrap modal | // FakeModal is used to return a fake Bootstrap modal | ||||||
| // if the ID returns nothing | // if the ID returns nothing to prevent errors. | ||||||
| function FakeModal() { } | function FakeModal() { } | ||||||
| FakeModal.prototype.hide = function () { alert('close fake modal') } | FakeModal.prototype.hide = function () { } | ||||||
| FakeModal.prototype.show = function () { alert('open fake modal') } | FakeModal.prototype.show = function () { } | ||||||
|  |  | ||||||
|  | // Set up the color hash generator lightness and hue to ensure darker colors | ||||||
|  | const colorHash = new ColorHash({ lightness: 0.3, saturation: [0.35, 0.5, 0.65] }); | ||||||
|  |  | ||||||
| /* Common mixin functions used in apps */ | /* Common mixin functions used in apps */ | ||||||
| const commonMixins = { | const commonMixins = { | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
| 			loading: 0 | 			loading: 0, | ||||||
|  | 			tagColorCache: {}, | ||||||
|  | 			showTagColors: true | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
|  | 	mounted() { | ||||||
|  | 		this.showTagColors = localStorage.getItem('showTagsColors') | ||||||
|  | 	}, | ||||||
|  |  | ||||||
| 	methods: { | 	methods: { | ||||||
| 		getFileSize: function (bytes) { | 		getFileSize: function (bytes) { | ||||||
| 			var i = Math.floor(Math.log(bytes) / Math.log(1024)); | 			var i = Math.floor(Math.log(bytes) / Math.log(1024)); | ||||||
| @@ -201,6 +211,27 @@ const commonMixins = { | |||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			return 'bi-file-arrow-down-fill'; | 			return 'bi-file-arrow-down-fill'; | ||||||
|  | 		}, | ||||||
|  |  | ||||||
|  | 		// Returns a hex color based on a string. | ||||||
|  | 		// Values are stored in an array for faster lookup / processing. | ||||||
|  | 		colorHash: function (s) { | ||||||
|  | 			if (this.tagColorCache[s] != undefined) { | ||||||
|  | 				return this.tagColorCache[s] | ||||||
|  | 			} | ||||||
|  | 			this.tagColorCache[s] = colorHash.hex(s) | ||||||
|  |  | ||||||
|  | 			return this.tagColorCache[s] | ||||||
|  | 		}, | ||||||
|  |  | ||||||
|  | 		toggleTagColors: function () { | ||||||
|  | 			if (this.showTagColors) { | ||||||
|  | 				localStorage.removeItem('showTagsColors') | ||||||
|  | 				this.showTagColors = false | ||||||
|  | 			} else { | ||||||
|  | 				localStorage.setItem('showTagsColors', '1') | ||||||
|  | 				this.showTagColors = true | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user