mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-05 04:05:32 +02:00
feat: implement instance-wide informational banner
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
import {beforeEach, describe, expect, test, vi} from 'vitest';
|
||||
import {initAdminConfigs} from './config.ts';
|
||||
import {POST} from '../../modules/fetch.ts';
|
||||
|
||||
vi.mock('../../modules/fetch.ts', () => ({
|
||||
POST: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../modules/tippy.ts', () => ({
|
||||
showTemporaryTooltip: vi.fn(),
|
||||
}));
|
||||
|
||||
function createPreviewDOM() {
|
||||
document.body.innerHTML = `
|
||||
<div class="page-content admin config">
|
||||
<form class="ui form" action="/-/admin/config/instance_notice" method="post">
|
||||
<textarea name="message">Initial message</textarea>
|
||||
<select name="level">
|
||||
<option value="info" selected>Info</option>
|
||||
<option value="success">Success</option>
|
||||
<option value="warning">Warning</option>
|
||||
<option value="danger">Danger</option>
|
||||
</select>
|
||||
<input type="checkbox" name="show_icon" value="true" checked>
|
||||
</form>
|
||||
<div id="instance-notice-preview" class="ui info message">
|
||||
<div id="instance-notice-preview-icon"></div>
|
||||
<div id="instance-notice-preview-content"></div>
|
||||
</div>
|
||||
<div id="instance-notice-preview-icons" class="tw-hidden">
|
||||
<span data-level="info"><svg data-icon="info"></svg></span>
|
||||
<span data-level="success"><svg data-icon="success"></svg></span>
|
||||
<span data-level="warning"><svg data-icon="warning"></svg></span>
|
||||
<span data-level="danger"><svg data-icon="danger"></svg></span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
describe('Admin Instance Notice Preview', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
createPreviewDOM();
|
||||
});
|
||||
|
||||
test('renders markdown preview on input', async () => {
|
||||
vi.mocked(POST).mockResolvedValue({
|
||||
text: async () => '<p>Rendered message</p>',
|
||||
} as Response);
|
||||
|
||||
initAdminConfigs();
|
||||
|
||||
const messageInput = document.querySelector<HTMLTextAreaElement>('textarea[name="message"]')!;
|
||||
messageInput.value = 'Updated message';
|
||||
messageInput.dispatchEvent(new Event('input'));
|
||||
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
|
||||
expect(POST).toHaveBeenCalledWith('/-/markup', expect.objectContaining({
|
||||
data: expect.any(FormData),
|
||||
}));
|
||||
|
||||
const formData = vi.mocked(POST).mock.calls[0][1]?.data as FormData;
|
||||
expect(formData.get('mode')).toBe('comment');
|
||||
expect(formData.get('text')).toBe('Updated message');
|
||||
|
||||
const previewContent = document.querySelector('#instance-notice-preview-content')!;
|
||||
expect(previewContent.innerHTML).toContain('Rendered message');
|
||||
});
|
||||
|
||||
test('updates preview class and icon when level and icon toggle change', () => {
|
||||
initAdminConfigs();
|
||||
|
||||
const levelSelect = document.querySelector<HTMLSelectElement>('select[name="level"]')!;
|
||||
const showIcon = document.querySelector<HTMLInputElement>('input[name="show_icon"]')!;
|
||||
const preview = document.querySelector<HTMLDivElement>('#instance-notice-preview')!;
|
||||
const previewIcon = document.querySelector<HTMLDivElement>('#instance-notice-preview-icon')!;
|
||||
|
||||
levelSelect.value = 'danger';
|
||||
levelSelect.dispatchEvent(new Event('change'));
|
||||
expect(preview.classList.contains('negative')).toBe(true);
|
||||
expect(previewIcon.innerHTML).toContain('data-icon="danger"');
|
||||
|
||||
showIcon.checked = false;
|
||||
showIcon.dispatchEvent(new Event('change'));
|
||||
expect(previewIcon.classList.contains('tw-hidden')).toBe(true);
|
||||
});
|
||||
|
||||
test('queues a second render while first request is in flight and re-renders with latest text', async () => {
|
||||
let firstResolve: ((value: Response) => void) | undefined;
|
||||
const firstPending = new Promise<Response>((resolve) => {
|
||||
firstResolve = resolve;
|
||||
});
|
||||
|
||||
vi.mocked(POST)
|
||||
.mockImplementationOnce(async () => await firstPending)
|
||||
.mockResolvedValueOnce({
|
||||
text: async () => '<p>Second render</p>',
|
||||
} as Response);
|
||||
|
||||
initAdminConfigs();
|
||||
|
||||
const messageInput = document.querySelector<HTMLTextAreaElement>('textarea[name="message"]')!;
|
||||
|
||||
messageInput.value = 'First value';
|
||||
messageInput.dispatchEvent(new Event('input'));
|
||||
|
||||
await Promise.resolve();
|
||||
|
||||
messageInput.value = 'Latest value';
|
||||
messageInput.dispatchEvent(new Event('input'));
|
||||
|
||||
firstResolve?.({
|
||||
text: async () => '<p>First render</p>',
|
||||
} as Response);
|
||||
|
||||
for (let i = 0; i < 10 && vi.mocked(POST).mock.calls.length < 2; i++) {
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 0);
|
||||
});
|
||||
}
|
||||
|
||||
expect(POST).toHaveBeenCalledTimes(2);
|
||||
const secondData = vi.mocked(POST).mock.calls[1][1]?.data as FormData;
|
||||
expect(secondData.get('text')).toBe('Latest value');
|
||||
|
||||
const previewContent = document.querySelector('#instance-notice-preview-content')!;
|
||||
for (let i = 0; i < 10 && !previewContent.innerHTML.includes('Second render'); i++) {
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 0);
|
||||
});
|
||||
}
|
||||
expect(previewContent.innerHTML).toContain('Second render');
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,78 @@
|
||||
import {showTemporaryTooltip} from '../../modules/tippy.ts';
|
||||
import {POST} from '../../modules/fetch.ts';
|
||||
import {html, htmlRaw} from '../../utils/html.ts';
|
||||
|
||||
const {appSubUrl} = window.config;
|
||||
|
||||
function initInstanceNoticePreview(elAdminConfig: HTMLDivElement): void {
|
||||
const form = elAdminConfig.querySelector<HTMLFormElement>('form[action$="/-/admin/config/instance_notice"]');
|
||||
if (!form) return;
|
||||
|
||||
const inputMessage = form.querySelector<HTMLTextAreaElement>('textarea[name="message"]');
|
||||
const selectLevel = form.querySelector<HTMLSelectElement>('select[name="level"]');
|
||||
const inputShowIcon = form.querySelector<HTMLInputElement>('input[name="show_icon"]');
|
||||
const preview = elAdminConfig.querySelector<HTMLDivElement>('#instance-notice-preview');
|
||||
const previewIcon = elAdminConfig.querySelector<HTMLDivElement>('#instance-notice-preview-icon');
|
||||
const previewContent = elAdminConfig.querySelector<HTMLDivElement>('#instance-notice-preview-content');
|
||||
const iconContainer = elAdminConfig.querySelector<HTMLDivElement>('#instance-notice-preview-icons');
|
||||
if (!inputMessage || !selectLevel || !inputShowIcon || !preview || !previewIcon || !previewContent || !iconContainer) return;
|
||||
|
||||
const iconHTMLByLevel = new Map<string, string>();
|
||||
for (const el of iconContainer.querySelectorAll<HTMLElement>('[data-level]')) {
|
||||
iconHTMLByLevel.set(el.getAttribute('data-level')!, el.innerHTML);
|
||||
}
|
||||
|
||||
const classByLevel: Record<string, string> = {
|
||||
info: 'info',
|
||||
success: 'positive',
|
||||
warning: 'warning',
|
||||
danger: 'negative',
|
||||
};
|
||||
|
||||
const updateStyle = () => {
|
||||
preview.classList.remove('info', 'positive', 'warning', 'negative');
|
||||
preview.classList.add(classByLevel[selectLevel.value] || 'info');
|
||||
previewIcon.innerHTML = iconHTMLByLevel.get(selectLevel.value) || iconHTMLByLevel.get('info') || '';
|
||||
previewIcon.classList.toggle('tw-hidden', !inputShowIcon.checked);
|
||||
};
|
||||
|
||||
let renderRequesting = false;
|
||||
let pendingRender = false;
|
||||
const renderPreviewMarkdown = async () => {
|
||||
if (renderRequesting) {
|
||||
pendingRender = true;
|
||||
return;
|
||||
}
|
||||
renderRequesting = true;
|
||||
try {
|
||||
while (true) {
|
||||
pendingRender = false;
|
||||
const formData = new FormData();
|
||||
formData.append('mode', 'comment');
|
||||
formData.append('text', inputMessage.value);
|
||||
try {
|
||||
const response = await POST(`${appSubUrl}/-/markup`, {data: formData});
|
||||
const rendered = await response.text();
|
||||
previewContent.innerHTML = html`${htmlRaw(rendered)}`;
|
||||
} catch (error) {
|
||||
console.error('Error rendering instance notice preview:', error);
|
||||
}
|
||||
if (!pendingRender) break;
|
||||
}
|
||||
} finally {
|
||||
renderRequesting = false;
|
||||
}
|
||||
};
|
||||
|
||||
inputMessage.addEventListener('input', () => {
|
||||
renderPreviewMarkdown();
|
||||
});
|
||||
selectLevel.addEventListener('change', updateStyle);
|
||||
inputShowIcon.addEventListener('change', updateStyle);
|
||||
|
||||
updateStyle();
|
||||
}
|
||||
|
||||
export function initAdminConfigs(): void {
|
||||
const elAdminConfig = document.querySelector<HTMLDivElement>('.page-content.admin.config');
|
||||
if (!elAdminConfig) return;
|
||||
@@ -21,4 +91,6 @@ export function initAdminConfigs(): void {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initInstanceNoticePreview(elAdminConfig);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user