mirror of
https://github.com/go-gitea/gitea.git
synced 2026-05-20 20:04:50 +02:00
feat: add dismiss functionality to instance notice banner
This commit is contained in:
parent
824d846f79
commit
b92f648555
@ -3299,6 +3299,7 @@
|
|||||||
"admin.config.instance_notice.delete": "Delete banner",
|
"admin.config.instance_notice.delete": "Delete banner",
|
||||||
"admin.config.instance_notice.delete_success": "Instance banner deleted.",
|
"admin.config.instance_notice.delete_success": "Instance banner deleted.",
|
||||||
"admin.config.instance_notice.edit_hint": "Edit this banner",
|
"admin.config.instance_notice.edit_hint": "Edit this banner",
|
||||||
|
"admin.config.instance_notice.dismiss": "Dismiss",
|
||||||
"admin.config.session_config": "Session Configuration",
|
"admin.config.session_config": "Session Configuration",
|
||||||
"admin.config.session_provider": "Session Provider",
|
"admin.config.session_provider": "Session Provider",
|
||||||
"admin.config.provider_config": "Provider Config",
|
"admin.config.provider_config": "Provider Config",
|
||||||
|
|||||||
@ -5,6 +5,8 @@ package common
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
goctx "context"
|
goctx "context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
@ -78,7 +80,8 @@ type pageGlobalDataType struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type InstanceNoticeBannerTmplInfo struct {
|
type InstanceNoticeBannerTmplInfo struct {
|
||||||
Message string
|
Message string
|
||||||
|
DismissKey string
|
||||||
}
|
}
|
||||||
|
|
||||||
func getInstanceNoticeBanner(ctx *context.Context) *InstanceNoticeBannerTmplInfo {
|
func getInstanceNoticeBanner(ctx *context.Context) *InstanceNoticeBannerTmplInfo {
|
||||||
@ -86,8 +89,11 @@ func getInstanceNoticeBanner(ctx *context.Context) *InstanceNoticeBannerTmplInfo
|
|||||||
if !notice.IsActive(int64(timeutil.TimeStampNow())) {
|
if !notice.IsActive(int64(timeutil.TimeStampNow())) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
h := sha256.Sum256([]byte(notice.Message))
|
||||||
|
dismissKey := hex.EncodeToString(h[:])[:16]
|
||||||
return &InstanceNoticeBannerTmplInfo{
|
return &InstanceNoticeBannerTmplInfo{
|
||||||
Message: notice.Message,
|
Message: notice.Message,
|
||||||
|
DismissKey: dismissKey,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -178,9 +178,12 @@
|
|||||||
</nav>
|
</nav>
|
||||||
{{if and .PageGlobalData .PageGlobalData.InstanceNoticeBanner}}
|
{{if and .PageGlobalData .PageGlobalData.InstanceNoticeBanner}}
|
||||||
{{$banner := .PageGlobalData.InstanceNoticeBanner}}
|
{{$banner := .PageGlobalData.InstanceNoticeBanner}}
|
||||||
<div class="ui info attached message tw-m-0 tw-rounded-none">
|
<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">
|
<div class="tw-flex tw-items-center tw-justify-center tw-gap-3">
|
||||||
<div class="render-content markup tw-text-center">{{ctx.RenderUtils.MarkdownToHtml $banner.Message}}</div>
|
<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>
|
</div>
|
||||||
{{if .PageGlobalData.IsSiteAdmin}}
|
{{if .PageGlobalData.IsSiteAdmin}}
|
||||||
<div class="tw-mt-2 tw-text-center">
|
<div class="tw-mt-2 tw-text-center">
|
||||||
|
|||||||
84
web_src/js/features/instance-notice.test.ts
Normal file
84
web_src/js/features/instance-notice.test.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
28
web_src/js/features/instance-notice.ts
Normal file
28
web_src/js/features/instance-notice.ts
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -64,6 +64,7 @@ import {initGlobalButtonClickOnEnter, initGlobalButtons, initGlobalDeleteButton}
|
|||||||
import {initGlobalComboMarkdownEditor, initGlobalEnterQuickSubmit, initGlobalFormDirtyLeaveConfirm} from './features/common-form.ts';
|
import {initGlobalComboMarkdownEditor, initGlobalEnterQuickSubmit, initGlobalFormDirtyLeaveConfirm} from './features/common-form.ts';
|
||||||
import {callInitFunctions} from './modules/init.ts';
|
import {callInitFunctions} from './modules/init.ts';
|
||||||
import {initRepoViewFileTree} from './features/repo-view-file-tree.ts';
|
import {initRepoViewFileTree} from './features/repo-view-file-tree.ts';
|
||||||
|
import {initInstanceNotice} from './features/instance-notice.ts';
|
||||||
|
|
||||||
const initStartTime = performance.now();
|
const initStartTime = performance.now();
|
||||||
const initPerformanceTracer = callInitFunctions([
|
const initPerformanceTracer = callInitFunctions([
|
||||||
@ -75,6 +76,7 @@ const initPerformanceTracer = callInitFunctions([
|
|||||||
initGlobalDropdown,
|
initGlobalDropdown,
|
||||||
initGlobalFetchAction,
|
initGlobalFetchAction,
|
||||||
initGlobalTooltips,
|
initGlobalTooltips,
|
||||||
|
initInstanceNotice,
|
||||||
initGlobalButtonClickOnEnter,
|
initGlobalButtonClickOnEnter,
|
||||||
initGlobalButtons,
|
initGlobalButtons,
|
||||||
initGlobalCopyToClipboardListener,
|
initGlobalCopyToClipboardListener,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user