0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-02-21 20:08:11 +01:00

feat: add dismiss functionality to instance notice banner

This commit is contained in:
Nicolas 2026-02-12 20:37:34 +01:00
parent 824d846f79
commit b92f648555
6 changed files with 129 additions and 5 deletions

View File

@ -3299,6 +3299,7 @@
"admin.config.instance_notice.delete": "Delete banner",
"admin.config.instance_notice.delete_success": "Instance banner deleted.",
"admin.config.instance_notice.edit_hint": "Edit this banner",
"admin.config.instance_notice.dismiss": "Dismiss",
"admin.config.session_config": "Session Configuration",
"admin.config.session_provider": "Session Provider",
"admin.config.provider_config": "Provider Config",

View File

@ -5,6 +5,8 @@ package common
import (
goctx "context"
"crypto/sha256"
"encoding/hex"
"errors"
"sync"
@ -78,7 +80,8 @@ type pageGlobalDataType struct {
}
type InstanceNoticeBannerTmplInfo struct {
Message string
Message string
DismissKey string
}
func getInstanceNoticeBanner(ctx *context.Context) *InstanceNoticeBannerTmplInfo {
@ -86,8 +89,11 @@ func getInstanceNoticeBanner(ctx *context.Context) *InstanceNoticeBannerTmplInfo
if !notice.IsActive(int64(timeutil.TimeStampNow())) {
return nil
}
h := sha256.Sum256([]byte(notice.Message))
dismissKey := hex.EncodeToString(h[:])[:16]
return &InstanceNoticeBannerTmplInfo{
Message: notice.Message,
Message: notice.Message,
DismissKey: dismissKey,
}
}

View File

@ -178,9 +178,12 @@
</nav>
{{if and .PageGlobalData .PageGlobalData.InstanceNoticeBanner}}
{{$banner := .PageGlobalData.InstanceNoticeBanner}}
<div class="ui info attached message tw-m-0 tw-rounded-none">
<div class="tw-flex tw-items-center tw-justify-center">
<div class="render-content markup tw-text-center">{{ctx.RenderUtils.MarkdownToHtml $banner.Message}}</div>
<div id="instance-notice-banner" class="ui info attached message tw-m-0 tw-rounded-none" data-dismiss-key="{{$banner.DismissKey}}">
<div class="tw-flex tw-items-center tw-justify-center tw-gap-3">
<div class="render-content markup tw-text-center tw-flex-1">{{ctx.RenderUtils.MarkdownToHtml $banner.Message}}</div>
<button type="button" class="ui mini icon button instance-notice-dismiss tw-shrink-0" aria-label="{{ctx.Locale.Tr "admin.config.instance_notice.dismiss"}}">
{{svg "octicon-x"}}
</button>
</div>
{{if .PageGlobalData.IsSiteAdmin}}
<div class="tw-mt-2 tw-text-center">

View File

@ -0,0 +1,84 @@
import {beforeEach, describe, expect, test, vi} from 'vitest';
import {initInstanceNotice} from './instance-notice.ts';
import {localUserSettings} from '../modules/user-settings.ts';
vi.mock('../modules/user-settings.ts', () => ({
localUserSettings: {
getString: vi.fn(),
setString: vi.fn(),
},
}));
function createBannerDOM(dismissKey: string) {
document.body.innerHTML = `
<div id="instance-notice-banner" class="ui info attached message" data-dismiss-key="${dismissKey}">
<div class="tw-flex tw-items-center tw-justify-center tw-gap-3">
<div class="render-content markup tw-text-center tw-flex-1">Maintenance in progress</div>
<button type="button" class="ui mini icon button instance-notice-dismiss">X</button>
</div>
</div>
`;
}
describe('Instance Notice Banner Dismiss', () => {
beforeEach(() => {
vi.clearAllMocks();
document.body.innerHTML = '';
});
test('no banner in DOM does nothing', () => {
initInstanceNotice();
expect(localUserSettings.getString).not.toHaveBeenCalled();
});
test('hides banner when dismiss key matches stored value', () => {
createBannerDOM('abc123');
vi.mocked(localUserSettings.getString).mockReturnValue('abc123');
initInstanceNotice();
const banner = document.querySelector<HTMLElement>('#instance-notice-banner')!;
expect(banner.style.display).toBe('none');
expect(localUserSettings.setString).not.toHaveBeenCalled();
});
test('does not hide banner when stored key differs', () => {
createBannerDOM('abc123');
vi.mocked(localUserSettings.getString).mockReturnValue('old-key');
initInstanceNotice();
const banner = document.querySelector<HTMLElement>('#instance-notice-banner')!;
expect(banner.style.display).not.toBe('none');
});
test('clicking dismiss button stores key and hides banner', () => {
createBannerDOM('abc123');
vi.mocked(localUserSettings.getString).mockReturnValue('');
initInstanceNotice();
const banner = document.querySelector<HTMLElement>('#instance-notice-banner')!;
expect(banner.style.display).not.toBe('none');
const dismissBtn = banner.querySelector<HTMLButtonElement>('.instance-notice-dismiss')!;
dismissBtn.click();
expect(localUserSettings.setString).toHaveBeenCalledWith('instance_notice_dismissed', 'abc123');
expect(banner.style.display).toBe('none');
});
test('banner without data-dismiss-key does nothing', () => {
document.body.innerHTML = `
<div id="instance-notice-banner" class="ui info attached message">
<div>Some message</div>
</div>
`;
initInstanceNotice();
const banner = document.querySelector<HTMLElement>('#instance-notice-banner')!;
expect(banner.style.display).not.toBe('none');
expect(localUserSettings.getString).not.toHaveBeenCalled();
});
});

View File

@ -0,0 +1,28 @@
import {localUserSettings} from '../modules/user-settings.ts';
const DISMISSED_KEY = 'instance_notice_dismissed';
function hideBanner(el: HTMLElement) {
el.style.display = 'none';
}
export function initInstanceNotice(): void {
const banner = document.querySelector<HTMLElement>('#instance-notice-banner');
if (!banner) return;
const dismissKey = banner.getAttribute('data-dismiss-key');
if (!dismissKey) return;
if (localUserSettings.getString(DISMISSED_KEY, '') === dismissKey) {
hideBanner(banner);
return;
}
const dismissBtn = banner.querySelector<HTMLButtonElement>('.instance-notice-dismiss');
if (dismissBtn) {
dismissBtn.addEventListener('click', () => {
localUserSettings.setString(DISMISSED_KEY, dismissKey);
hideBanner(banner);
});
}
}

View File

@ -64,6 +64,7 @@ import {initGlobalButtonClickOnEnter, initGlobalButtons, initGlobalDeleteButton}
import {initGlobalComboMarkdownEditor, initGlobalEnterQuickSubmit, initGlobalFormDirtyLeaveConfirm} from './features/common-form.ts';
import {callInitFunctions} from './modules/init.ts';
import {initRepoViewFileTree} from './features/repo-view-file-tree.ts';
import {initInstanceNotice} from './features/instance-notice.ts';
const initStartTime = performance.now();
const initPerformanceTracer = callInitFunctions([
@ -75,6 +76,7 @@ const initPerformanceTracer = callInitFunctions([
initGlobalDropdown,
initGlobalFetchAction,
initGlobalTooltips,
initInstanceNotice,
initGlobalButtonClickOnEnter,
initGlobalButtons,
initGlobalCopyToClipboardListener,