+ {{$statusUnread := 1}}{{$statusRead := 2}}{{$statusPinned := 3}} {{$notificationUnreadCount := call .PageGlobalData.GetNotificationUnreadCount}} -
+ {{$pageTypeIsRead := eq $.PageType "read"}} +
- {{if and (eq .Status 1)}} + {{if and (not $pageTypeIsRead) $notificationUnreadCount}}
{{$.CsrfTokenHtml}} -
- -
+
{{end}}
-
-
- {{if not .Notifications}} -
- {{svg "octicon-inbox" 56 "tw-mb-4"}} - {{if eq .Status 1}} - {{ctx.Locale.Tr "notification.no_unread"}} +
+ {{range $one := .Notifications}} +
+
+ {{if $one.Issue}} + {{template "shared/issueicon" $one.Issue}} {{else}} - {{ctx.Locale.Tr "notification.no_read"}} + {{svg "octicon-repo" 16 "text grey"}} {{end}}
- {{else}} - {{range $notification := .Notifications}} -
-
- {{if .Issue}} - {{template "shared/issueicon" .Issue}} - {{else}} - {{svg "octicon-repo" 16 "text grey"}} - {{end}} -
- -
- {{.Repository.FullName}} {{if .Issue}}#{{.Issue.Index}}{{end}} - {{if eq .Status 3}} - {{svg "octicon-pin" 13 "text blue tw-mt-0.5 tw-ml-1"}} - {{end}} -
-
- - {{if .Issue}} - {{.Issue.Title | ctx.RenderUtils.RenderIssueSimpleTitle}} - {{else}} - {{.Repository.FullName}} - {{end}} - -
-
-
- {{if .Issue}} - {{DateUtils.TimeSince .Issue.UpdatedUnix}} - {{else}} - {{DateUtils.TimeSince .UpdatedUnix}} - {{end}} -
-
- {{if ne .Status 3}} -
- {{$.CsrfTokenHtml}} - - - -
- {{end}} - {{if or (eq .Status 1) (eq .Status 3)}} -
- {{$.CsrfTokenHtml}} - - - - -
- {{else if eq .Status 2}} -
- {{$.CsrfTokenHtml}} - - - - -
- {{end}} -
+
+
+ {{$one.Repository.FullName}} {{if $one.Issue}}#{{$one.Issue.Index}}{{end}} + {{if eq $one.Status $statusPinned}} + {{svg "octicon-pin" 13 "text blue"}} + {{end}}
+
+ {{if $one.Issue}} + {{$one.Issue.Title | ctx.RenderUtils.RenderIssueSimpleTitle}} + {{else}} + {{$one.Repository.FullName}} + {{end}} +
+
+
+ {{if $one.Issue}} + {{DateUtils.TimeSince $one.Issue.UpdatedUnix}} + {{else}} + {{DateUtils.TimeSince $one.UpdatedUnix}} + {{end}} +
+
+ {{$.CsrfTokenHtml}} + + {{if ne $one.Status $statusPinned}} + + {{end}} + {{if or (eq $one.Status $statusUnread) (eq $one.Status $statusPinned)}} + + {{else if eq $one.Status $statusRead}} + + {{end}} +
+
+ {{else}} +
+ {{svg "octicon-inbox" 56 "tw-mb-4"}} + {{if $pageTypeIsRead}} + {{ctx.Locale.Tr "notification.no_read"}} + {{else}} + {{ctx.Locale.Tr "notification.no_unread"}} {{end}} - {{end}} -
+
+ {{end}}
{{template "base/paginate" .}}
diff --git a/web_src/css/base.css b/web_src/css/base.css index 2b7a47edf1..b415a70cb8 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -185,10 +185,6 @@ details summary { cursor: pointer; } -details summary > * { - display: inline; -} - progress { background: var(--color-secondary-dark-1); border-radius: var(--border-radius); @@ -474,15 +470,6 @@ a.label, color: var(--color-text-light-2); } -.ui.comments .comment .actions a { - color: var(--color-text-light); -} - -.ui.comments .comment .actions a.active, -.ui.comments .comment .actions a:hover { - color: var(--color-primary); -} - img.ui.avatar, .ui.avatar img, .ui.avatar svg { diff --git a/web_src/css/modules/label.css b/web_src/css/modules/label.css index f5d0decdf6..cf850e4c5a 100644 --- a/web_src/css/modules/label.css +++ b/web_src/css/modules/label.css @@ -93,7 +93,6 @@ a.ui.label:hover { background: var(--color-button); border: 1px solid var(--color-light-border); color: var(--color-text-light); - padding: calc(0.5833em - 1px) calc(0.833em - 1px); } a.ui.basic.label:hover { text-decoration: none; @@ -254,6 +253,7 @@ a.ui.ui.ui.basic.grey.label:hover { color: var(--color-label-hover-bg); } +/* "horizontal label" is actually "fat label" which has enough padding spaces to be used standalone in headers */ .ui.horizontal.label { margin: 0 0.5em 0 0; padding: 0.4em 0.833em; diff --git a/web_src/css/repo.css b/web_src/css/repo.css index a72709c382..8729212f10 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -1420,13 +1420,15 @@ td .commit-summary { .comment-header { background: var(--color-box-header); border-bottom: 1px solid var(--color-secondary); - padding: 0 1rem; + padding: 0.5em 1rem; position: relative; color: var(--color-text); min-height: 41px; display: flex; justify-content: space-between; align-items: center; + flex-wrap: wrap; + gap: 0.25em; } .comment-header::before, @@ -1468,17 +1470,16 @@ td .commit-summary { left: 7px; } -.comment-header .actions a:not(.label) { - padding: 0.5rem !important; -} - -.comment-header .actions .label { - margin: 0 !important; -} - .comment-header-left, .comment-header-right { - gap: 4px; + display: flex; + align-items: center; + gap: 0.5em; +} + +.comment-header-right { + flex: 1; + justify-content: end; } .comment-body { @@ -2014,15 +2015,6 @@ tbody.commit-list { .commit-table th.sha { display: none !important; } - .comment-header { - flex-wrap: wrap; - } - .comment-header .comment-header-left { - flex-wrap: wrap; - } - .comment-header .comment-header-right { - margin-left: auto; - } } .commit-status-header { diff --git a/web_src/css/user.css b/web_src/css/user.css index caabf1834c..d42e8688fb 100644 --- a/web_src/css/user.css +++ b/web_src/css/user.css @@ -114,6 +114,14 @@ border-radius: var(--border-radius); } +.notifications-item { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 0.5em; + padding: 0.5em 1em; +} + .notifications-item:hover { background: var(--color-hover); } @@ -129,6 +137,9 @@ .notifications-item:hover .notifications-buttons { display: flex; + align-items: center; + justify-content: end; + gap: 0.25em; } .notifications-item:hover .notifications-updated { diff --git a/web_src/js/features/file-view.ts b/web_src/js/features/file-view.ts index d803f53c0d..67f4381468 100644 --- a/web_src/js/features/file-view.ts +++ b/web_src/js/features/file-view.ts @@ -2,7 +2,7 @@ import type {FileRenderPlugin} from '../render/plugin.ts'; import {newRenderPlugin3DViewer} from '../render/plugins/3d-viewer.ts'; import {newRenderPluginPdfViewer} from '../render/plugins/pdf-viewer.ts'; import {registerGlobalInitFunc} from '../modules/observer.ts'; -import {createElementFromHTML, showElem, toggleClass} from '../utils/dom.ts'; +import {createElementFromHTML, showElem, toggleElemClass} from '../utils/dom.ts'; import {html} from '../utils/html.ts'; import {basename} from '../utils.ts'; @@ -21,8 +21,8 @@ function showRenderRawFileButton(elFileView: HTMLElement, renderContainer: HTMLE const toggleButtons = elFileView.querySelector('.file-view-toggle-buttons'); showElem(toggleButtons); const displayingRendered = Boolean(renderContainer); - toggleClass(toggleButtons.querySelectorAll('.file-view-toggle-source'), 'active', !displayingRendered); // it may not exist - toggleClass(toggleButtons.querySelector('.file-view-toggle-rendered'), 'active', displayingRendered); + toggleElemClass(toggleButtons.querySelectorAll('.file-view-toggle-source'), 'active', !displayingRendered); // it may not exist + toggleElemClass(toggleButtons.querySelector('.file-view-toggle-rendered'), 'active', displayingRendered); // TODO: if there is only one button, hide it? } diff --git a/web_src/js/features/notification.ts b/web_src/js/features/notification.ts index dc0acb0244..4a1aa3ede9 100644 --- a/web_src/js/features/notification.ts +++ b/web_src/js/features/notification.ts @@ -1,40 +1,13 @@ import {GET} from '../modules/fetch.ts'; -import {toggleElem, type DOMEvent, createElementFromHTML} from '../utils/dom.ts'; +import {toggleElem, createElementFromHTML} from '../utils/dom.ts'; import {logoutFromWorker} from '../modules/worker.ts'; const {appSubUrl, notificationSettings, assetVersionEncoded} = window.config; let notificationSequenceNumber = 0; -export function initNotificationsTable() { - const table = document.querySelector('#notification_table'); - if (!table) return; - - // when page restores from bfcache, delete previously clicked items - window.addEventListener('pageshow', (e) => { - if (e.persisted) { // page was restored from bfcache - const table = document.querySelector('#notification_table'); - const unreadCountEl = document.querySelector('.notifications-unread-count'); - let unreadCount = parseInt(unreadCountEl.textContent); - for (const item of table.querySelectorAll('.notifications-item[data-remove="true"]')) { - item.remove(); - unreadCount -= 1; - } - unreadCountEl.textContent = String(unreadCount); - } - }); - - // mark clicked unread links for deletion on bfcache restore - for (const link of table.querySelectorAll('.notifications-item[data-status="1"] .notifications-link')) { - link.addEventListener('click', (e: DOMEvent) => { - e.target.closest('.notifications-item').setAttribute('data-remove', 'true'); - }); - } -} - -async function receiveUpdateCount(event: MessageEvent) { +async function receiveUpdateCount(event: MessageEvent<{type: string, data: string}>) { try { - const data = JSON.parse(event.data); - + const data = JSON.parse(event.data.data); for (const count of document.querySelectorAll('.notification_count')) { count.classList.toggle('tw-hidden', data.Count === 0); count.textContent = `${data.Count}`; @@ -71,7 +44,7 @@ export function initNotificationCount() { type: 'start', url: `${window.location.origin}${appSubUrl}/user/events`, }); - worker.port.addEventListener('message', (event: MessageEvent) => { + worker.port.addEventListener('message', (event: MessageEvent<{type: string, data: string}>) => { if (!event.data || !event.data.type) { console.error('unknown worker message event', event); return; @@ -144,11 +117,11 @@ async function updateNotificationCountWithCallback(callback: (timeout: number, n } async function updateNotificationTable() { - const notificationDiv = document.querySelector('#notification_div'); + let notificationDiv = document.querySelector('#notification_div'); if (notificationDiv) { try { const params = new URLSearchParams(window.location.search); - params.set('div-only', String(true)); + params.set('div-only', 'true'); params.set('sequence-number', String(++notificationSequenceNumber)); const response = await GET(`${appSubUrl}/notifications?${params.toString()}`); @@ -160,7 +133,8 @@ async function updateNotificationTable() { const el = createElementFromHTML(data); if (parseInt(el.getAttribute('data-sequence-number')) === notificationSequenceNumber) { notificationDiv.outerHTML = data; - initNotificationsTable(); + notificationDiv = document.querySelector('#notification_div'); + window.htmx.process(notificationDiv); // when using htmx, we must always remember to process the new content changed by us } } catch (error) { console.error(error); diff --git a/web_src/js/features/repo-diff.ts b/web_src/js/features/repo-diff.ts index ad1da5c2fa..bde7ec0324 100644 --- a/web_src/js/features/repo-diff.ts +++ b/web_src/js/features/repo-diff.ts @@ -138,7 +138,14 @@ function initDiffHeaderPopup() { btn.setAttribute('data-header-popup-initialized', ''); const popup = btn.nextElementSibling; if (!popup?.matches('.tippy-target')) throw new Error('Popup element not found'); - createTippy(btn, {content: popup, theme: 'menu', placement: 'bottom', trigger: 'click', interactive: true, hideOnClick: true}); + createTippy(btn, { + content: popup, + theme: 'menu', + placement: 'bottom-end', + trigger: 'click', + interactive: true, + hideOnClick: true, + }); } } diff --git a/web_src/js/features/repo-graph.ts b/web_src/js/features/repo-graph.ts index 036a55f715..ebca6e212a 100644 --- a/web_src/js/features/repo-graph.ts +++ b/web_src/js/features/repo-graph.ts @@ -1,4 +1,4 @@ -import {toggleClass} from '../utils/dom.ts'; +import {toggleElemClass} from '../utils/dom.ts'; import {GET} from '../modules/fetch.ts'; import {fomanticQuery} from '../modules/fomantic/base.ts'; @@ -9,11 +9,11 @@ export function initRepoGraphGit() { const elColorMonochrome = document.querySelector('#flow-color-monochrome'); const elColorColored = document.querySelector('#flow-color-colored'); const toggleColorMode = (mode: 'monochrome' | 'colored') => { - toggleClass(graphContainer, 'monochrome', mode === 'monochrome'); - toggleClass(graphContainer, 'colored', mode === 'colored'); + toggleElemClass(graphContainer, 'monochrome', mode === 'monochrome'); + toggleElemClass(graphContainer, 'colored', mode === 'colored'); - toggleClass(elColorMonochrome, 'active', mode === 'monochrome'); - toggleClass(elColorColored, 'active', mode === 'colored'); + toggleElemClass(elColorMonochrome, 'active', mode === 'monochrome'); + toggleElemClass(elColorColored, 'active', mode === 'colored'); const params = new URLSearchParams(window.location.search); params.set('mode', mode); diff --git a/web_src/js/features/repo-settings.ts b/web_src/js/features/repo-settings.ts index be1821664f..5c81cf5ecd 100644 --- a/web_src/js/features/repo-settings.ts +++ b/web_src/js/features/repo-settings.ts @@ -1,6 +1,6 @@ import {minimatch} from 'minimatch'; import {createMonaco} from './codeeditor.ts'; -import {onInputDebounce, queryElems, toggleClass, toggleElem} from '../utils/dom.ts'; +import {onInputDebounce, queryElems, toggleElem} from '../utils/dom.ts'; import {POST} from '../modules/fetch.ts'; import {initRepoSettingsBranchesDrag} from './repo-settings-branches.ts'; import {fomanticQuery} from '../modules/fomantic/base.ts'; @@ -124,14 +124,18 @@ function initRepoSettingsOptions() { const pageContent = document.querySelector('.page-content.repository.settings.options'); if (!pageContent) return; - // Enable or select internal/external wiki system and issue tracker. + // toggle related panels for the checkbox/radio inputs, the "selector" may not exist + const toggleTargetContextPanel = (selector: string, enabled: boolean) => { + if (!selector) return; + queryElems(document, selector, (el) => el.classList.toggle('disabled', !enabled)); + }; queryElems(pageContent, '.enable-system', (el) => el.addEventListener('change', () => { - toggleClass(el.getAttribute('data-target'), 'disabled', !el.checked); - toggleClass(el.getAttribute('data-context'), 'disabled', el.checked); + toggleTargetContextPanel(el.getAttribute('data-target'), el.checked); + toggleTargetContextPanel(el.getAttribute('data-context'), !el.checked); })); queryElems(pageContent, '.enable-system-radio', (el) => el.addEventListener('change', () => { - toggleClass(el.getAttribute('data-target'), 'disabled', el.value === 'false'); - toggleClass(el.getAttribute('data-context'), 'disabled', el.value === 'true'); + toggleTargetContextPanel(el.getAttribute('data-target'), el.value === 'true'); + toggleTargetContextPanel(el.getAttribute('data-context'), el.value === 'false'); })); queryElems(pageContent, '.js-tracker-issue-style', (el) => el.addEventListener('change', () => { diff --git a/web_src/js/index-domready.ts b/web_src/js/index-domready.ts index ca18d1e828..770c7fc00c 100644 --- a/web_src/js/index-domready.ts +++ b/web_src/js/index-domready.ts @@ -15,7 +15,7 @@ import {initTableSort} from './features/tablesort.ts'; import {initAdminUserListSearchForm} from './features/admin/users.ts'; import {initAdminConfigs} from './features/admin/config.ts'; import {initMarkupAnchors} from './markup/anchors.ts'; -import {initNotificationCount, initNotificationsTable} from './features/notification.ts'; +import {initNotificationCount} from './features/notification.ts'; import {initRepoIssueContentHistory} from './features/repo-issue-content.ts'; import {initStopwatch} from './features/stopwatch.ts'; import {initFindFileInRepo} from './features/repo-findfile.ts'; @@ -117,7 +117,6 @@ const initPerformanceTracer = callInitFunctions([ initDashboardRepoList, initNotificationCount, - initNotificationsTable, initOrgTeam, diff --git a/web_src/js/index.ts b/web_src/js/index.ts index e78b3cb64f..af53cc488c 100644 --- a/web_src/js/index.ts +++ b/web_src/js/index.ts @@ -9,5 +9,15 @@ import {onDomReady} from './utils/dom.ts'; import 'htmx.org'; onDomReady(async () => { - await import(/* webpackChunkName: "index-domready" */'./index-domready.ts'); + // when navigate before the import complete, there will be an error from webpack chunk loader: + // JavaScript promise rejection: Loading chunk index-domready failed. + try { + await import(/* webpackChunkName: "index-domready" */'./index-domready.ts'); + } catch (e) { + if (e.name === 'ChunkLoadError') { + console.error('Error loading index-domready:', e); + } else { + throw e; + } + } }); diff --git a/web_src/js/modules/toast.ts b/web_src/js/modules/toast.ts index ed807a4977..087103cbd8 100644 --- a/web_src/js/modules/toast.ts +++ b/web_src/js/modules/toast.ts @@ -44,7 +44,7 @@ type ToastifyElement = HTMLElement & {_giteaToastifyInstance?: Toast }; // See https://github.com/apvarun/toastify-js#api for options function showToast(message: string, level: Intent, {gravity, position, duration, useHtmlBody, preventDuplicates = true, ...other}: ToastOpts = {}): Toast { - const body = useHtmlBody ? String(message) : htmlEscape(message); + const body = useHtmlBody ? message : htmlEscape(message); const parent = document.querySelector('.ui.dimmer.active') ?? document.body; const duplicateKey = preventDuplicates ? (preventDuplicates === true ? `${level}-${body}` : preventDuplicates) : ''; diff --git a/web_src/js/utils.ts b/web_src/js/utils.ts index e33b1413e8..f396a8e4f6 100644 --- a/web_src/js/utils.ts +++ b/web_src/js/utils.ts @@ -1,6 +1,6 @@ import {decode, encode} from 'uint8-to-base64'; import type {IssuePageInfo, IssuePathInfo, RepoOwnerPathInfo} from './types.ts'; -import {toggleClass, toggleElem} from './utils/dom.ts'; +import {toggleElemClass, toggleElem} from './utils/dom.ts'; // transform /path/to/file.ext to /path/to export function dirname(path: string): string { @@ -194,7 +194,7 @@ export function toggleFullScreen(fullscreenElementsSelector: string, isFullScree const fullScreenEl = document.querySelector(fullscreenElementsSelector); const outerEl = document.querySelector('.full.height'); - toggleClass(fullscreenElementsSelector, 'fullscreen', isFullScreen); + toggleElemClass(fullscreenElementsSelector, 'fullscreen', isFullScreen); if (isFullScreen) { outerEl.append(fullScreenEl); } else { diff --git a/web_src/js/utils/dom.ts b/web_src/js/utils/dom.ts index 3b14b9bcea..6d6a3735da 100644 --- a/web_src/js/utils/dom.ts +++ b/web_src/js/utils/dom.ts @@ -25,7 +25,7 @@ function elementsCall(el: ElementArg, func: ElementsCallbackWithArgs, ...args: a throw new Error('invalid argument to be shown/hidden'); } -export function toggleClass(el: ElementArg, className: string, force?: boolean): ArrayLikeIterable { +export function toggleElemClass(el: ElementArg, className: string, force?: boolean): ArrayLikeIterable { return elementsCall(el, (e: Element) => { if (force === true) { e.classList.add(className); @@ -44,7 +44,7 @@ export function toggleClass(el: ElementArg, className: string, force?: boolean): * @param force force=true to show or force=false to hide, undefined to toggle */ export function toggleElem(el: ElementArg, force?: boolean): ArrayLikeIterable { - return toggleClass(el, 'tw-hidden', force === undefined ? force : !force); + return toggleElemClass(el, 'tw-hidden', force === undefined ? force : !force); } export function showElem(el: ElementArg): ArrayLikeIterable { @@ -283,7 +283,7 @@ export function isElemVisible(el: HTMLElement): boolean { // This function DOESN'T account for all possible visibility scenarios, its behavior is covered by the tests of "querySingleVisibleElem" if (!el) return false; // checking el.style.display is not necessary for browsers, but it is required by some tests with happy-dom because happy-dom doesn't really do layout - return !el.classList.contains('tw-hidden') && Boolean((el.offsetWidth || el.offsetHeight || el.getClientRects().length) && el.style.display !== 'none'); + return !el.classList.contains('tw-hidden') && (el.offsetWidth || el.offsetHeight || el.getClientRects().length) && el.style.display !== 'none'; } // replace selected text in a textarea while preserving editor history, e.g. CTRL-Z works after this