detached 478146d

Better error/succes handling in JS according to what the backend is returning

- Added "js" to the list of included file extensions in .air.toml.
- Added new JavaScript files to the HTML templates.
- Improved error handling in custom.js for htmx requests.
commit
478146d00037065a11ddb537ebff7a85b1901691
author
bnema · · 2023-09-22 08:00 UTC
parent
ba19305
signature
unsigned
7 files · +221 -121
M .air.toml +1 -1
@@ -14,7 +14,7 @@ tmp_dir = "tmp"
1414 follow_symlink = false
1515 full_bin = "./tmp/main"
1616 include_dir = []
17- include_ext = ["go", "tpl", "tmpl", "html", "gohtml", "css"]
17+ include_ext = ["go", "tpl", "tmpl", "html", "gohtml", "css", "js"]
1818 include_file = []
1919 kill_delay = "0s"
2020 log = "build-errors.log"
@@ -14,7 +14,7 @@ tmp_dir = "tmp"
14 follow_symlink = false14 follow_symlink = false
15 full_bin = "./tmp/main"15 full_bin = "./tmp/main"
16 include_dir = []16 include_dir = []
17- include_ext = ["go", "tpl", "tmpl", "html", "gohtml", "css"]17+ include_ext = ["go", "tpl", "tmpl", "html", "gohtml", "css", "js"]
18 include_file = []18 include_file = []
19 kill_delay = "0s"19 kill_delay = "0s"
20 log = "build-errors.log"20 log = "build-errors.log"
M internal/gotemplate/models/html/fragments/header.gohtml +4 -3
@@ -4,8 +4,9 @@
44 <title>{{ .header.meta.title }}</title>
55 <link rel="stylesheet" href="/assets/css/tailwind.css" />
66 <link rel="stylesheet" href="/libs/iconoir.css" />
7- <script src="/libs/alpine.min.js" defer></script>
87 <script src="/libs/htmx.min.js"></script>
8+ <script src="/libs/htmx-response-targets.js"></script>
9+ <script src="/libs/htmx-debug.js"></script>
910 <script src="/libs/htmx-sse.js"></script>
10- <script src="/assets/js/custom.js" defer></script>
11-</head>⏎̸
11+ <script defer src="/assets/js/custom.js"></script>
12+</head>
@@ -4,8 +4,9 @@
4 <title>{{ .header.meta.title }}</title>4 <title>{{ .header.meta.title }}</title>
5 <link rel="stylesheet" href="/assets/css/tailwind.css" />5 <link rel="stylesheet" href="/assets/css/tailwind.css" />
6 <link rel="stylesheet" href="/libs/iconoir.css" />6 <link rel="stylesheet" href="/libs/iconoir.css" />
7- <script src="/libs/alpine.min.js" defer></script>
8 <script src="/libs/htmx.min.js"></script>7 <script src="/libs/htmx.min.js"></script>
8+ <script src="/libs/htmx-response-targets.js"></script>
9+ <script src="/libs/htmx-debug.js"></script>
9 <script src="/libs/htmx-sse.js"></script>10 <script src="/libs/htmx-sse.js"></script>
10- <script src="/assets/js/custom.js" defer></script>11+ <script defer src="/assets/js/custom.js"></script>
11-</head>⏎̸12+</head>
M internal/gotemplate/models/html/fragments/imagelist.gohtml +17 -41
@@ -1,4 +1,4 @@
1-<div class="flex flex-col p-4">
1+<div class="flex flex-col p-4" hx-ext="response-targets">
22 <h1 class="text-lg md:text-xl font-bold">Images</h1>
33 <div class="flex font-bold border-t-2 align-baseline py-2">
44 <div class="w-2/4">Name</div>
@@ -7,49 +7,25 @@
77 <div class="w-1/4">Action</div>
88 </div>
99 {{ range.Images }}
10- <div
11- x-data="{ success: false, loading: false, showError: false, errorMessage: '' }"
12- >
13- <div
14- id="img-{{.ShortImgID}}"
15- class="flex py-2 align-baseline justify-center"
16- hx-boost="true"
17- >
18- <div class="w-2/4">
19- <span title="{{.ImageSummary.ID}}">{{.Name}}</span>
20- </div>
21- <div class="w-2/4">{{.CreatedStr}}</div>
22- <div class="w-1/6">{{.SizeStr}}</div>
23-
24- <button
25- id="remove-button-img-{{.ShortImgID}}"
26- x-bind:class="{ 'bg-green-500': success, 'bg-red-500': !success, 'text-white': true, 'px-2': true, 'py-1': true, 'rounded': true }"
27- @click="loading = true"
28- hx-trigger="click"
29- hx-delete="/htmx/image-manager/delete/{{.ShortImgID}}"
30- hx-swap="outerHTML swap:outerHTML"
31- hx-target="#img-{{.ShortImgID}}"
32- hx-confirm="Are you sure you want to delete this image?"
33- hx-select="button"
34- hx-boost="true"
35- x-on:htmx:after-swap="success = true; loading = false; error = false"
36- x-on:htmx:responseError="showError = true; errorMessage = (event.detail && event.detail.xhr && event.detail.xhr.responseText) ? event.detail.xhr.responseText : 'An unknown error occurred.'"
37- >
38- <span x-show="!loading && !success">Remove</span>
39- <span x-show="loading">Loading...</span>
40- <span x-show="success">Success</span>
41- </button>
10+ <div id="img-{{.ShortImgID}}" class="flex py-2 align-baseline justify-center">
11+ <div class="w-2/4">
12+ <span title="{{.ImageSummary.ID}}">{{.Name}}</span>
4213 </div>
43-
44- <div
45- x-show="showError"
46- data-error-type="image"
47- class="error-display-div bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative"
14+ <div class="w-2/4">{{.CreatedStr}}</div>
15+ <div class="w-1/6">{{.SizeStr}}</div>
16+ <button
17+ id="remove-button-img-{{.ShortImgID}}"
18+ hx-delete="/htmx/image-manager/delete/{{.ShortImgID}}"
19+ hx-target="#img-{{.ShortImgID}}"
20+ hx-target-*="#error-message-img"
21+ hx-swap="innerHTML"
22+ hx-confirm="Are you sure you want to delete this container image?"
23+ class="w-1/4 bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded-md"
4824 >
49- <p x-text="errorMessage"></p>
50- </div>
25+ Delete
26+ </button>
5127 </div>
52-
28+ <div id="error-message-img" class="mt-2 text-red-500" display="none"></div>
5329 {{ else }}
5430 <div class="mt-2">No images found.</div>
5531 {{ end }}
@@ -1,4 +1,4 @@
1-<div class="flex flex-col p-4">1+<div class="flex flex-col p-4" hx-ext="response-targets">
2 <h1 class="text-lg md:text-xl font-bold">Images</h1>2 <h1 class="text-lg md:text-xl font-bold">Images</h1>
3 <div class="flex font-bold border-t-2 align-baseline py-2">3 <div class="flex font-bold border-t-2 align-baseline py-2">
4 <div class="w-2/4">Name</div>4 <div class="w-2/4">Name</div>
@@ -7,49 +7,25 @@
7 <div class="w-1/4">Action</div>7 <div class="w-1/4">Action</div>
8 </div>8 </div>
9 {{ range.Images }}9 {{ range.Images }}
10- <div10+ <div id="img-{{.ShortImgID}}" class="flex py-2 align-baseline justify-center">
11- x-data="{ success: false, loading: false, showError: false, errorMessage: '' }"11+ <div class="w-2/4">
12- >12+ <span title="{{.ImageSummary.ID}}">{{.Name}}</span>
13- <div
14- id="img-{{.ShortImgID}}"
15- class="flex py-2 align-baseline justify-center"
16- hx-boost="true"
17- >
18- <div class="w-2/4">
19- <span title="{{.ImageSummary.ID}}">{{.Name}}</span>
20- </div>
21- <div class="w-2/4">{{.CreatedStr}}</div>
22- <div class="w-1/6">{{.SizeStr}}</div>
23-
24- <button
25- id="remove-button-img-{{.ShortImgID}}"
26- x-bind:class="{ 'bg-green-500': success, 'bg-red-500': !success, 'text-white': true, 'px-2': true, 'py-1': true, 'rounded': true }"
27- @click="loading = true"
28- hx-trigger="click"
29- hx-delete="/htmx/image-manager/delete/{{.ShortImgID}}"
30- hx-swap="outerHTML swap:outerHTML"
31- hx-target="#img-{{.ShortImgID}}"
32- hx-confirm="Are you sure you want to delete this image?"
33- hx-select="button"
34- hx-boost="true"
35- x-on:htmx:after-swap="success = true; loading = false; error = false"
36- x-on:htmx:responseError="showError = true; errorMessage = (event.detail && event.detail.xhr && event.detail.xhr.responseText) ? event.detail.xhr.responseText : 'An unknown error occurred.'"
37- >
38- <span x-show="!loading && !success">Remove</span>
39- <span x-show="loading">Loading...</span>
40- <span x-show="success">Success</span>
41- </button>
42 </div>13 </div>
43-14+ <div class="w-2/4">{{.CreatedStr}}</div>
44- <div15+ <div class="w-1/6">{{.SizeStr}}</div>
45- x-show="showError"16+ <button
46- data-error-type="image"17+ id="remove-button-img-{{.ShortImgID}}"
47- class="error-display-div bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative"18+ hx-delete="/htmx/image-manager/delete/{{.ShortImgID}}"
19+ hx-target="#img-{{.ShortImgID}}"
20+ hx-target-*="#error-message-img"
21+ hx-swap="innerHTML"
22+ hx-confirm="Are you sure you want to delete this container image?"
23+ class="w-1/4 bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded-md"
48 >24 >
49- <p x-text="errorMessage"></p>25+ Delete
50- </div>26+ </button>
51 </div>27 </div>
52-28+ <div id="error-message-img" class="mt-2 text-red-500" display="none"></div>
53 {{ else }}29 {{ else }}
54 <div class="mt-2">No images found.</div>30 <div class="mt-2">No images found.</div>
55 {{ end }}31 {{ end }}
M internal/webui/public/assets/css/tailwind.css +39 -63
@@ -541,8 +541,38 @@ video {
541541 --tw-backdrop-sepia: ;
542542 }
543543
544-.relative {
545- position: relative;
544+.container {
545+ width: 100%;
546+}
547+
548+@media (min-width: 640px) {
549+ .container {
550+ max-width: 640px;
551+ }
552+}
553+
554+@media (min-width: 768px) {
555+ .container {
556+ max-width: 768px;
557+ }
558+}
559+
560+@media (min-width: 1024px) {
561+ .container {
562+ max-width: 1024px;
563+ }
564+}
565+
566+@media (min-width: 1280px) {
567+ .container {
568+ max-width: 1280px;
569+ }
570+}
571+
572+@media (min-width: 1536px) {
573+ .container {
574+ max-width: 1536px;
575+ }
546576 }
547577
548578 .my-2 {
@@ -643,22 +673,6 @@ video {
643673 flex-grow: 1;
644674 }
645675
646-.scale-100 {
647- --tw-scale-x: 1;
648- --tw-scale-y: 1;
649- transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
650-}
651-
652-.scale-90 {
653- --tw-scale-x: .9;
654- --tw-scale-y: .9;
655- transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
656-}
657-
658-.transform {
659- transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
660-}
661-
662676 .flex-row {
663677 flex-direction: row;
664678 }
@@ -732,11 +746,6 @@ video {
732746 border-color: rgb(209 213 219 / var(--tw-border-opacity));
733747 }
734748
735-.border-red-400 {
736- --tw-border-opacity: 1;
737- border-color: rgb(248 113 113 / var(--tw-border-opacity));
738-}
739-
740749 .bg-gordonmascot-hblue {
741750 --tw-bg-opacity: 1;
742751 background-color: rgb(1 30 92 / var(--tw-bg-opacity));
@@ -757,11 +766,6 @@ video {
757766 background-color: rgb(34 197 94 / var(--tw-bg-opacity));
758767 }
759768
760-.bg-red-100 {
761- --tw-bg-opacity: 1;
762- background-color: rgb(254 226 226 / var(--tw-bg-opacity));
763-}
764-
765769 .bg-red-500 {
766770 --tw-bg-opacity: 1;
767771 background-color: rgb(239 68 68 / var(--tw-bg-opacity));
@@ -813,11 +817,6 @@ video {
813817 padding-bottom: 0.5rem;
814818 }
815819
816-.py-3 {
817- padding-top: 0.75rem;
818- padding-bottom: 0.75rem;
819-}
820-
821820 .align-baseline {
822821 vertical-align: baseline;
823822 }
@@ -879,9 +878,9 @@ video {
879878 color: rgb(31 41 55 / var(--tw-text-opacity));
880879 }
881880
882-.text-red-700 {
881+.text-red-500 {
883882 --tw-text-opacity: 1;
884- color: rgb(185 28 28 / var(--tw-text-opacity));
883+ color: rgb(239 68 68 / var(--tw-text-opacity));
885884 }
886885
887886 .text-red-800 {
@@ -903,14 +902,6 @@ video {
903902 text-decoration-line: underline;
904903 }
905904
906-.opacity-0 {
907- opacity: 0;
908-}
909-
910-.opacity-100 {
911- opacity: 1;
912-}
913-
914905 .shadow {
915906 --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
916907 --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
@@ -922,26 +913,6 @@ video {
922913 outline-offset: 2px;
923914 }
924915
925-.transition {
926- transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;
927- transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
928- transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter;
929- transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
930- transition-duration: 150ms;
931-}
932-
933-.duration-300 {
934- transition-duration: 300ms;
935-}
936-
937-.ease-in {
938- transition-timing-function: cubic-bezier(0.4, 0, 1, 1);
939-}
940-
941-.ease-out {
942- transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
943-}
944-
945916 /* Import IBM Plex Fonts */
946917
947918 @font-face {
@@ -1062,6 +1033,11 @@ video {
10621033 margin-right: 8px;
10631034 }
10641035
1036+.hover\:bg-red-700:hover {
1037+ --tw-bg-opacity: 1;
1038+ background-color: rgb(185 28 28 / var(--tw-bg-opacity));
1039+}
1040+
10651041 @media (min-width: 640px) {
10661042 .sm\:h-24 {
10671043 height: 6rem;
@@ -541,8 +541,38 @@ video {
541 --tw-backdrop-sepia: ;541 --tw-backdrop-sepia: ;
542 }542 }
543 543
544-.relative {544+.container {
545- position: relative;545+ width: 100%;
546+}
547+
548+@media (min-width: 640px) {
549+ .container {
550+ max-width: 640px;
551+ }
552+}
553+
554+@media (min-width: 768px) {
555+ .container {
556+ max-width: 768px;
557+ }
558+}
559+
560+@media (min-width: 1024px) {
561+ .container {
562+ max-width: 1024px;
563+ }
564+}
565+
566+@media (min-width: 1280px) {
567+ .container {
568+ max-width: 1280px;
569+ }
570+}
571+
572+@media (min-width: 1536px) {
573+ .container {
574+ max-width: 1536px;
575+ }
546 }576 }
547 577
548 .my-2 {578 .my-2 {
@@ -643,22 +673,6 @@ video {
643 flex-grow: 1;673 flex-grow: 1;
644 }674 }
645 675
646-.scale-100 {
647- --tw-scale-x: 1;
648- --tw-scale-y: 1;
649- transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
650-}
651-
652-.scale-90 {
653- --tw-scale-x: .9;
654- --tw-scale-y: .9;
655- transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
656-}
657-
658-.transform {
659- transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
660-}
661-
662 .flex-row {676 .flex-row {
663 flex-direction: row;677 flex-direction: row;
664 }678 }
@@ -732,11 +746,6 @@ video {
732 border-color: rgb(209 213 219 / var(--tw-border-opacity));746 border-color: rgb(209 213 219 / var(--tw-border-opacity));
733 }747 }
734 748
735-.border-red-400 {
736- --tw-border-opacity: 1;
737- border-color: rgb(248 113 113 / var(--tw-border-opacity));
738-}
739-
740 .bg-gordonmascot-hblue {749 .bg-gordonmascot-hblue {
741 --tw-bg-opacity: 1;750 --tw-bg-opacity: 1;
742 background-color: rgb(1 30 92 / var(--tw-bg-opacity));751 background-color: rgb(1 30 92 / var(--tw-bg-opacity));
@@ -757,11 +766,6 @@ video {
757 background-color: rgb(34 197 94 / var(--tw-bg-opacity));766 background-color: rgb(34 197 94 / var(--tw-bg-opacity));
758 }767 }
759 768
760-.bg-red-100 {
761- --tw-bg-opacity: 1;
762- background-color: rgb(254 226 226 / var(--tw-bg-opacity));
763-}
764-
765 .bg-red-500 {769 .bg-red-500 {
766 --tw-bg-opacity: 1;770 --tw-bg-opacity: 1;
767 background-color: rgb(239 68 68 / var(--tw-bg-opacity));771 background-color: rgb(239 68 68 / var(--tw-bg-opacity));
@@ -813,11 +817,6 @@ video {
813 padding-bottom: 0.5rem;817 padding-bottom: 0.5rem;
814 }818 }
815 819
816-.py-3 {
817- padding-top: 0.75rem;
818- padding-bottom: 0.75rem;
819-}
820-
821 .align-baseline {820 .align-baseline {
822 vertical-align: baseline;821 vertical-align: baseline;
823 }822 }
@@ -879,9 +878,9 @@ video {
879 color: rgb(31 41 55 / var(--tw-text-opacity));878 color: rgb(31 41 55 / var(--tw-text-opacity));
880 }879 }
881 880
882-.text-red-700 {881+.text-red-500 {
883 --tw-text-opacity: 1;882 --tw-text-opacity: 1;
884- color: rgb(185 28 28 / var(--tw-text-opacity));883+ color: rgb(239 68 68 / var(--tw-text-opacity));
885 }884 }
886 885
887 .text-red-800 {886 .text-red-800 {
@@ -903,14 +902,6 @@ video {
903 text-decoration-line: underline;902 text-decoration-line: underline;
904 }903 }
905 904
906-.opacity-0 {
907- opacity: 0;
908-}
909-
910-.opacity-100 {
911- opacity: 1;
912-}
913-
914 .shadow {905 .shadow {
915 --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);906 --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
916 --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);907 --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
@@ -922,26 +913,6 @@ video {
922 outline-offset: 2px;913 outline-offset: 2px;
923 }914 }
924 915
925-.transition {
926- transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;
927- transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
928- transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter;
929- transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
930- transition-duration: 150ms;
931-}
932-
933-.duration-300 {
934- transition-duration: 300ms;
935-}
936-
937-.ease-in {
938- transition-timing-function: cubic-bezier(0.4, 0, 1, 1);
939-}
940-
941-.ease-out {
942- transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
943-}
944-
945 /* Import IBM Plex Fonts */916 /* Import IBM Plex Fonts */
946 917
947 @font-face {918 @font-face {
@@ -1062,6 +1033,11 @@ video {
1062 margin-right: 8px;1033 margin-right: 8px;
1063 }1034 }
1064 1035
1036+.hover\:bg-red-700:hover {
1037+ --tw-bg-opacity: 1;
1038+ background-color: rgb(185 28 28 / var(--tw-bg-opacity));
1039+}
1040+
1065 @media (min-width: 640px) {1041 @media (min-width: 640px) {
1066 .sm\:h-24 {1042 .sm\:h-24 {
1067 height: 6rem;1043 height: 6rem;
M internal/webui/public/assets/js/custom.js +27 -13
@@ -1,15 +1,29 @@
1-document.addEventListener('DOMContentLoaded', function() {
2- document.body.addEventListener('htmx:responseError', function(event) {
3- const errorDetail = event.detail;
4- const errorType = errorDetail && errorDetail.xhr ? errorDetail.xhr.getResponseHeader("X-Error-Type") : 'generic'; // Fallback to 'generic' if not set
5- const errorDiv = document.querySelector(`div[data-error-type="${errorType}"]`);
6- if (errorDiv) {
7- if (errorDetail && errorDetail.xhr && errorDetail.xhr.responseText) {
8- errorDiv.innerHTML = errorDetail.xhr.responseText;
9- } else {
10- errorDiv.textContent = 'An unknown error occurred.';
11- }
12- errorDiv.style.display = 'block'; // Make it visible
1+// htmx.logAll();
2+document.addEventListener('htmx:afterRequest', function(event) {
3+
4+ // Find the error message element
5+ var errorMessageElement = document.getElementById('error-message-img');
6+
7+ // Find the image div element by ID (you need to get this ID dynamically)
8+ var imageDivID = 'img-' + event.detail.elt.id.replace('img-', '');
9+ var imageDivElement = document.getElementById(imageDivID);
10+ console.log(imageDivElement);
11+
12+ // If it's an error, we display and hide the error message
13+ if (event.detail.xhr.status !== 200) {
14+ if (errorMessageElement && errorMessageElement.textContent.trim() !== "") {
15+ // Set a timeout to hide it
16+ setTimeout(function() {
17+ errorMessageElement.style.display = 'none'; // or set it to empty
18+ errorMessageElement.textContent = ''; // clear the text content
19+ }, 7000); // 3000 milliseconds = 3 seconds
1320 }
14- });
21+ } else {
22+ // If it's a success, we hide the image div
23+ if (imageDivElement) {
24+ setTimeout(function() {
25+ imageDivElement.style.display = 'none'; // Hide the div
26+ }, 3000); // 3000 milliseconds = 3 seconds
27+ }
28+ }
1529 });
@@ -1,15 +1,29 @@
1-document.addEventListener('DOMContentLoaded', function() {1+// htmx.logAll();
2- document.body.addEventListener('htmx:responseError', function(event) {2+document.addEventListener('htmx:afterRequest', function(event) {
3- const errorDetail = event.detail;3+
4- const errorType = errorDetail && errorDetail.xhr ? errorDetail.xhr.getResponseHeader("X-Error-Type") : 'generic'; // Fallback to 'generic' if not set4+ // Find the error message element
5- const errorDiv = document.querySelector(`div[data-error-type="${errorType}"]`);5+ var errorMessageElement = document.getElementById('error-message-img');
6- if (errorDiv) {6+
7- if (errorDetail && errorDetail.xhr && errorDetail.xhr.responseText) {7+ // Find the image div element by ID (you need to get this ID dynamically)
8- errorDiv.innerHTML = errorDetail.xhr.responseText;8+ var imageDivID = 'img-' + event.detail.elt.id.replace('img-', '');
9- } else {9+ var imageDivElement = document.getElementById(imageDivID);
10- errorDiv.textContent = 'An unknown error occurred.';10+ console.log(imageDivElement);
11- }11+
12- errorDiv.style.display = 'block'; // Make it visible12+ // If it's an error, we display and hide the error message
13+ if (event.detail.xhr.status !== 200) {
14+ if (errorMessageElement && errorMessageElement.textContent.trim() !== "") {
15+ // Set a timeout to hide it
16+ setTimeout(function() {
17+ errorMessageElement.style.display = 'none'; // or set it to empty
18+ errorMessageElement.textContent = ''; // clear the text content
19+ }, 7000); // 3000 milliseconds = 3 seconds
13 }20 }
14- });21+ } else {
22+ // If it's a success, we hide the image div
23+ if (imageDivElement) {
24+ setTimeout(function() {
25+ imageDivElement.style.display = 'none'; // Hide the div
26+ }, 3000); // 3000 milliseconds = 3 seconds
27+ }
28+ }
15 });29 });
A internal/webui/public/libs/htmx-debug.js +11 -0
@@ -0,0 +1,11 @@
1+htmx.defineExtension('debug', {
2+ onEvent: function (name, evt) {
3+ if (console.debug) {
4+ console.debug(name, evt);
5+ } else if (console) {
6+ console.log("DEBUG:", name, evt);
7+ } else {
8+ throw "NO CONSOLE SUPPORTED"
9+ }
10+ }
11+});
@@ -0,0 +1,11 @@
1+htmx.defineExtension('debug', {
2+ onEvent: function (name, evt) {
3+ if (console.debug) {
4+ console.debug(name, evt);
5+ } else if (console) {
6+ console.log("DEBUG:", name, evt);
7+ } else {
8+ throw "NO CONSOLE SUPPORTED"
9+ }
10+ }
11+});
A internal/webui/public/libs/htmx-response-targets.js +122 -0
@@ -0,0 +1,122 @@
1+(function(){
2+
3+ /** @type {import("../htmx").HtmxInternalApi} */
4+ var api;
5+
6+ var attrPrefix = 'hx-target-';
7+
8+ /**
9+ * @param {HTMLElement} elt
10+ * @param {number} respCode
11+ * @returns {HTMLElement | null}
12+ */
13+ function getRespCodeTarget(elt, respCodeNumber) {
14+ if (!elt || !respCodeNumber) return null;
15+
16+ var respCode = respCodeNumber.toString();
17+
18+ // '*' is the original syntax, as the obvious character for a wildcard.
19+ // The 'x' alternative was added for maximum compatibility with HTML
20+ // templating engines, due to ambiguity around which characters are
21+ // supported in HTML attributes.
22+ //
23+ // Start with the most specific possible attribute and generalize from
24+ // there.
25+ var attrPossibilities = [
26+ respCode,
27+
28+ respCode.substr(0, 2) + '*',
29+ respCode.substr(0, 2) + 'x',
30+
31+ respCode.substr(0, 1) + '*',
32+ respCode.substr(0, 1) + 'x',
33+ respCode.substr(0, 1) + '**',
34+ respCode.substr(0, 1) + 'xx',
35+
36+ '*',
37+ 'x',
38+ '***',
39+ 'xxx',
40+ ];
41+
42+ for (var i = 0; i < attrPossibilities.length; i++) {
43+ var attr = attrPrefix + attrPossibilities[i];
44+ var attrValue = api.getClosestAttributeValue(elt, attr);
45+ if (attrValue) {
46+ if (attrValue === "this") {
47+ return api.findThisElement(elt, attr);
48+ } else {
49+ return api.querySelectorExt(elt, attrValue);
50+ }
51+ }
52+ }
53+
54+ return null;
55+ }
56+
57+ /** @param {Event} evt */
58+ function handleErrorFlag(evt) {
59+ if (evt.detail.isError) {
60+ if (htmx.config.responseTargetUnsetsError) {
61+ evt.detail.isError = false;
62+ }
63+ } else if (htmx.config.responseTargetSetsError) {
64+ evt.detail.isError = true;
65+ }
66+ }
67+
68+ htmx.defineExtension('response-targets', {
69+
70+ /** @param {import("../htmx").HtmxInternalApi} apiRef */
71+ init: function (apiRef) {
72+ api = apiRef;
73+
74+ if (htmx.config.responseTargetUnsetsError === undefined) {
75+ htmx.config.responseTargetUnsetsError = true;
76+ }
77+ if (htmx.config.responseTargetSetsError === undefined) {
78+ htmx.config.responseTargetSetsError = false;
79+ }
80+ if (htmx.config.responseTargetPrefersExisting === undefined) {
81+ htmx.config.responseTargetPrefersExisting = false;
82+ }
83+ if (htmx.config.responseTargetPrefersRetargetHeader === undefined) {
84+ htmx.config.responseTargetPrefersRetargetHeader = true;
85+ }
86+ },
87+
88+ /**
89+ * @param {string} name
90+ * @param {Event} evt
91+ */
92+ onEvent: function (name, evt) {
93+ if (name === "htmx:beforeSwap" &&
94+ evt.detail.xhr &&
95+ evt.detail.xhr.status !== 200) {
96+ if (evt.detail.target) {
97+ if (htmx.config.responseTargetPrefersExisting) {
98+ evt.detail.shouldSwap = true;
99+ handleErrorFlag(evt);
100+ return true;
101+ }
102+ if (htmx.config.responseTargetPrefersRetargetHeader &&
103+ evt.detail.xhr.getAllResponseHeaders().match(/HX-Retarget:/i)) {
104+ evt.detail.shouldSwap = true;
105+ handleErrorFlag(evt);
106+ return true;
107+ }
108+ }
109+ if (!evt.detail.requestConfig) {
110+ return true;
111+ }
112+ var target = getRespCodeTarget(evt.detail.requestConfig.elt, evt.detail.xhr.status);
113+ if (target) {
114+ handleErrorFlag(evt);
115+ evt.detail.shouldSwap = true;
116+ evt.detail.target = target;
117+ }
118+ return true;
119+ }
120+ }
121+ });
122+})();
@@ -0,0 +1,122 @@
1+(function(){
2+
3+ /** @type {import("../htmx").HtmxInternalApi} */
4+ var api;
5+
6+ var attrPrefix = 'hx-target-';
7+
8+ /**
9+ * @param {HTMLElement} elt
10+ * @param {number} respCode
11+ * @returns {HTMLElement | null}
12+ */
13+ function getRespCodeTarget(elt, respCodeNumber) {
14+ if (!elt || !respCodeNumber) return null;
15+
16+ var respCode = respCodeNumber.toString();
17+
18+ // '*' is the original syntax, as the obvious character for a wildcard.
19+ // The 'x' alternative was added for maximum compatibility with HTML
20+ // templating engines, due to ambiguity around which characters are
21+ // supported in HTML attributes.
22+ //
23+ // Start with the most specific possible attribute and generalize from
24+ // there.
25+ var attrPossibilities = [
26+ respCode,
27+
28+ respCode.substr(0, 2) + '*',
29+ respCode.substr(0, 2) + 'x',
30+
31+ respCode.substr(0, 1) + '*',
32+ respCode.substr(0, 1) + 'x',
33+ respCode.substr(0, 1) + '**',
34+ respCode.substr(0, 1) + 'xx',
35+
36+ '*',
37+ 'x',
38+ '***',
39+ 'xxx',
40+ ];
41+
42+ for (var i = 0; i < attrPossibilities.length; i++) {
43+ var attr = attrPrefix + attrPossibilities[i];
44+ var attrValue = api.getClosestAttributeValue(elt, attr);
45+ if (attrValue) {
46+ if (attrValue === "this") {
47+ return api.findThisElement(elt, attr);
48+ } else {
49+ return api.querySelectorExt(elt, attrValue);
50+ }
51+ }
52+ }
53+
54+ return null;
55+ }
56+
57+ /** @param {Event} evt */
58+ function handleErrorFlag(evt) {
59+ if (evt.detail.isError) {
60+ if (htmx.config.responseTargetUnsetsError) {
61+ evt.detail.isError = false;
62+ }
63+ } else if (htmx.config.responseTargetSetsError) {
64+ evt.detail.isError = true;
65+ }
66+ }
67+
68+ htmx.defineExtension('response-targets', {
69+
70+ /** @param {import("../htmx").HtmxInternalApi} apiRef */
71+ init: function (apiRef) {
72+ api = apiRef;
73+
74+ if (htmx.config.responseTargetUnsetsError === undefined) {
75+ htmx.config.responseTargetUnsetsError = true;
76+ }
77+ if (htmx.config.responseTargetSetsError === undefined) {
78+ htmx.config.responseTargetSetsError = false;
79+ }
80+ if (htmx.config.responseTargetPrefersExisting === undefined) {
81+ htmx.config.responseTargetPrefersExisting = false;
82+ }
83+ if (htmx.config.responseTargetPrefersRetargetHeader === undefined) {
84+ htmx.config.responseTargetPrefersRetargetHeader = true;
85+ }
86+ },
87+
88+ /**
89+ * @param {string} name
90+ * @param {Event} evt
91+ */
92+ onEvent: function (name, evt) {
93+ if (name === "htmx:beforeSwap" &&
94+ evt.detail.xhr &&
95+ evt.detail.xhr.status !== 200) {
96+ if (evt.detail.target) {
97+ if (htmx.config.responseTargetPrefersExisting) {
98+ evt.detail.shouldSwap = true;
99+ handleErrorFlag(evt);
100+ return true;
101+ }
102+ if (htmx.config.responseTargetPrefersRetargetHeader &&
103+ evt.detail.xhr.getAllResponseHeaders().match(/HX-Retarget:/i)) {
104+ evt.detail.shouldSwap = true;
105+ handleErrorFlag(evt);
106+ return true;
107+ }
108+ }
109+ if (!evt.detail.requestConfig) {
110+ return true;
111+ }
112+ var target = getRespCodeTarget(evt.detail.requestConfig.elt, evt.detail.xhr.status);
113+ if (target) {
114+ handleErrorFlag(evt);
115+ evt.detail.shouldSwap = true;
116+ evt.detail.target = target;
117+ }
118+ return true;
119+ }
120+ }
121+ });
122+})();

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close