mirror of
https://github.com/go-gitea/gitea.git
synced 2026-05-06 08:29:54 +02:00
fixup! feat: implement instance-wide informational banner
This commit is contained in:
parent
b53ba46a26
commit
dacd1941f1
@ -62,10 +62,9 @@ const (
|
||||
)
|
||||
|
||||
type InstanceNotice struct {
|
||||
Enabled bool
|
||||
Message string
|
||||
Level string
|
||||
ShowIcon bool
|
||||
Enabled bool
|
||||
Message string
|
||||
Level string
|
||||
|
||||
StartTime int64
|
||||
EndTime int64
|
||||
@ -73,8 +72,7 @@ type InstanceNotice struct {
|
||||
|
||||
func DefaultInstanceNotice() InstanceNotice {
|
||||
return InstanceNotice{
|
||||
Level: InstanceNoticeLevelInfo,
|
||||
ShowIcon: true,
|
||||
Level: InstanceNoticeLevelInfo,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -3288,12 +3288,11 @@
|
||||
"admin.config.instance_notice.level.success": "Success",
|
||||
"admin.config.instance_notice.level.warning": "Warning",
|
||||
"admin.config.instance_notice.level.danger": "Danger",
|
||||
"admin.config.instance_notice.show_icon": "Show icon",
|
||||
"admin.config.instance_notice.message_too_long": "Message must be at most %d characters.",
|
||||
"admin.config.instance_notice.start_time": "Start time",
|
||||
"admin.config.instance_notice.end_time": "End time",
|
||||
"admin.config.instance_notice.invalid_time": "Invalid date-time value. Use the picker format.",
|
||||
"admin.config.instance_notice.invalid_time_range": "End time must be after start time.",
|
||||
"admin.config.instance_notice.markdown_hint": "This banner is informational only and does not block access.",
|
||||
"admin.config.instance_notice.preview": "Preview",
|
||||
"admin.config.instance_notice.save": "Save banner",
|
||||
"admin.config.instance_notice.save_success": "Instance banner settings updated.",
|
||||
|
||||
@ -80,18 +80,17 @@ type pageGlobalDataType struct {
|
||||
type InstanceNoticeBannerTmplInfo struct {
|
||||
Message string
|
||||
Level string
|
||||
ShowIcon bool
|
||||
IconName string
|
||||
}
|
||||
|
||||
func instanceNoticeIconName(level string) string {
|
||||
switch level {
|
||||
case setting.InstanceNoticeLevelSuccess:
|
||||
return "octicon-check-circle"
|
||||
return "octicon-check"
|
||||
case setting.InstanceNoticeLevelWarning:
|
||||
return "octicon-alert"
|
||||
case setting.InstanceNoticeLevelDanger:
|
||||
return "octicon-stop"
|
||||
return "octicon-alert"
|
||||
default:
|
||||
return "octicon-info"
|
||||
}
|
||||
@ -105,7 +104,6 @@ func getInstanceNoticeBanner(ctx *context.Context) *InstanceNoticeBannerTmplInfo
|
||||
return &InstanceNoticeBannerTmplInfo{
|
||||
Message: notice.Message,
|
||||
Level: notice.Level,
|
||||
ShowIcon: notice.ShowIcon,
|
||||
IconName: instanceNoticeIconName(notice.Level),
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
system_model "code.gitea.io/gitea/models/system"
|
||||
"code.gitea.io/gitea/modules/cache"
|
||||
@ -29,6 +30,8 @@ import (
|
||||
const (
|
||||
tplConfig templates.TplName = "admin/config"
|
||||
tplConfigSettings templates.TplName = "admin/config_settings/config_settings"
|
||||
|
||||
instanceNoticeMessageMaxLength = 2000
|
||||
)
|
||||
|
||||
// SendTestMail send test mail to confirm mail service is OK
|
||||
@ -148,6 +151,7 @@ func Config(ctx *context.Context) {
|
||||
ctx.Data["Webhook"] = setting.Webhook
|
||||
instanceNotice := setting.GetInstanceNotice(ctx)
|
||||
ctx.Data["InstanceNotice"] = instanceNotice
|
||||
ctx.Data["InstanceNoticeMessageMaxLength"] = instanceNoticeMessageMaxLength
|
||||
if instanceNotice.StartTime > 0 {
|
||||
ctx.Data["InstanceNoticeStartTime"] = time.Unix(instanceNotice.StartTime, 0).In(setting.DefaultUILocation).Format("2006-01-02T15:04")
|
||||
}
|
||||
@ -237,7 +241,6 @@ func SetInstanceNotice(ctx *context.Context) {
|
||||
enabled := ctx.FormBool("enabled")
|
||||
message := strings.TrimSpace(ctx.FormString("message"))
|
||||
level := strings.TrimSpace(ctx.FormString("level"))
|
||||
showIcon := ctx.FormBool("show_icon")
|
||||
startTime, err := parseDatetimeLocalValue(strings.TrimSpace(ctx.FormString("start_time")))
|
||||
if err != nil {
|
||||
ctx.Flash.Error(ctx.Tr("admin.config.instance_notice.invalid_time"))
|
||||
@ -258,6 +261,11 @@ func SetInstanceNotice(ctx *context.Context) {
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/config#instance-notice")
|
||||
return
|
||||
}
|
||||
if utf8.RuneCountInString(message) > instanceNoticeMessageMaxLength {
|
||||
ctx.Flash.Error(ctx.Tr("admin.config.instance_notice.message_too_long", instanceNoticeMessageMaxLength))
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/config#instance-notice")
|
||||
return
|
||||
}
|
||||
if startTime > 0 && endTime > 0 && endTime < startTime {
|
||||
ctx.Flash.Error(ctx.Tr("admin.config.instance_notice.invalid_time_range"))
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/config#instance-notice")
|
||||
@ -268,7 +276,6 @@ func SetInstanceNotice(ctx *context.Context) {
|
||||
Enabled: enabled,
|
||||
Message: message,
|
||||
Level: level,
|
||||
ShowIcon: showIcon,
|
||||
StartTime: startTime,
|
||||
EndTime: endTime,
|
||||
}
|
||||
|
||||
@ -279,10 +279,10 @@
|
||||
<label>{{ctx.Locale.Tr "admin.config.instance_notice.enabled"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "admin.config.instance_notice.message"}}</label>
|
||||
<textarea name="message" rows="4" maxlength="2000" placeholder="{{ctx.Locale.Tr "admin.config.instance_notice.message_placeholder"}}">{{.InstanceNotice.Message}}</textarea>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "admin.config.instance_notice.message"}}</label>
|
||||
<textarea name="message" rows="4" maxlength="{{.InstanceNoticeMessageMaxLength}}" placeholder="{{ctx.Locale.Tr "admin.config.instance_notice.message_placeholder"}}">{{.InstanceNotice.Message}}</textarea>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "admin.config.instance_notice.level"}}</label>
|
||||
<select class="ui dropdown" name="level">
|
||||
@ -292,13 +292,7 @@
|
||||
<option value="danger" {{if eq .InstanceNotice.Level "danger"}}selected{{end}}>{{ctx.Locale.Tr "admin.config.instance_notice.level.danger"}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="show_icon" value="true" {{if .InstanceNotice.ShowIcon}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "admin.config.instance_notice.show_icon"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="two fields">
|
||||
<div class="two fields">
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "admin.config.instance_notice.start_time"}}</label>
|
||||
<input type="datetime-local" name="start_time" value="{{.InstanceNoticeStartTime}}">
|
||||
@ -308,25 +302,23 @@
|
||||
<input type="datetime-local" name="end_time" value="{{.InstanceNoticeEndTime}}">
|
||||
</div>
|
||||
</div>
|
||||
<p class="text tw-mb-3">{{ctx.Locale.Tr "admin.config.instance_notice.markdown_hint"}}</p>
|
||||
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "admin.config.instance_notice.preview"}}</label>
|
||||
<div id="instance-notice-preview" class="ui {{if eq .InstanceNotice.Level "success"}}positive{{else if eq .InstanceNotice.Level "warning"}}warning{{else if eq .InstanceNotice.Level "danger"}}negative{{else}}info{{end}} message tw-mt-2">
|
||||
<div class="tw-flex tw-items-start tw-justify-center tw-gap-2">
|
||||
<div id="instance-notice-preview-icon" class="{{if not .InstanceNotice.ShowIcon}}tw-hidden{{end}} tw-leading-6">
|
||||
{{if eq .InstanceNotice.Level "success"}}{{svg "octicon-check-circle"}}{{else if eq .InstanceNotice.Level "warning"}}{{svg "octicon-alert"}}{{else if eq .InstanceNotice.Level "danger"}}{{svg "octicon-stop"}}{{else}}{{svg "octicon-info"}}{{end}}
|
||||
<label>{{ctx.Locale.Tr "admin.config.instance_notice.preview"}}</label>
|
||||
<div id="instance-notice-preview" class="ui {{if eq .InstanceNotice.Level "success"}}positive{{else if eq .InstanceNotice.Level "warning"}}warning{{else if eq .InstanceNotice.Level "danger"}}negative{{else}}info{{end}} message tw-mt-2">
|
||||
<div class="tw-flex tw-items-start tw-justify-center tw-gap-2">
|
||||
<div id="instance-notice-preview-icon" class="tw-leading-6">
|
||||
{{if eq .InstanceNotice.Level "success"}}{{svg "octicon-check"}}{{else if eq .InstanceNotice.Level "warning"}}{{svg "octicon-alert"}}{{else if eq .InstanceNotice.Level "danger"}}{{svg "octicon-alert"}}{{else}}{{svg "octicon-info"}}{{end}}
|
||||
</div>
|
||||
<div id="instance-notice-preview-content" class="render-content markup tw-text-center">{{ctx.RenderUtils.MarkdownToHtml .InstanceNotice.Message}}</div>
|
||||
</div>
|
||||
<div id="instance-notice-preview-content" class="render-content markup tw-text-center">{{ctx.RenderUtils.MarkdownToHtml .InstanceNotice.Message}}</div>
|
||||
</div>
|
||||
<div id="instance-notice-preview-icons" class="tw-hidden">
|
||||
<span data-level="info">{{svg "octicon-info"}}</span>
|
||||
<span data-level="success">{{svg "octicon-check"}}</span>
|
||||
<span data-level="warning">{{svg "octicon-alert"}}</span>
|
||||
<span data-level="danger">{{svg "octicon-alert"}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="instance-notice-preview-icons" class="tw-hidden">
|
||||
<span data-level="info">{{svg "octicon-info"}}</span>
|
||||
<span data-level="success">{{svg "octicon-check-circle"}}</span>
|
||||
<span data-level="warning">{{svg "octicon-alert"}}</span>
|
||||
<span data-level="danger">{{svg "octicon-stop"}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="ui tiny primary button">{{ctx.Locale.Tr "admin.config.instance_notice.save"}}</button>
|
||||
</form>
|
||||
|
||||
@ -177,14 +177,12 @@
|
||||
{{end}}
|
||||
</nav>
|
||||
{{if and .PageGlobalData .PageGlobalData.InstanceNoticeBanner}}
|
||||
{{$banner := .PageGlobalData.InstanceNoticeBanner}}
|
||||
<div class="ui {{if eq $banner.Level "success"}}positive{{else if eq $banner.Level "warning"}}warning{{else if eq $banner.Level "danger"}}negative{{else}}info{{end}} attached message tw-m-0 tw-rounded-none">
|
||||
<div class="tw-flex tw-items-start tw-justify-center tw-gap-2">
|
||||
{{if $banner.ShowIcon}}
|
||||
{{$banner := .PageGlobalData.InstanceNoticeBanner}}
|
||||
<div class="ui {{if eq $banner.Level "success"}}positive{{else if eq $banner.Level "warning"}}warning{{else if eq $banner.Level "danger"}}negative{{else}}info{{end}} attached message tw-m-0 tw-rounded-none">
|
||||
<div class="tw-flex tw-items-start tw-justify-center tw-gap-2">
|
||||
<span class="tw-leading-6">{{svg $banner.IconName}}</span>
|
||||
{{end}}
|
||||
<div class="render-content markup tw-text-center">{{ctx.RenderUtils.MarkdownToHtml $banner.Message}}</div>
|
||||
</div>
|
||||
<div class="render-content markup tw-text-center">{{ctx.RenderUtils.MarkdownToHtml $banner.Message}}</div>
|
||||
</div>
|
||||
{{if .PageGlobalData.IsSiteAdmin}}
|
||||
<div class="tw-mt-2 tw-text-center">
|
||||
<a href="{{AppSubUrl}}/-/admin/config#instance-notice">{{ctx.Locale.Tr "admin.config.instance_notice.edit_hint"}}</a>
|
||||
|
||||
@ -5,6 +5,7 @@ package integration
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -23,10 +24,9 @@ func TestInstanceNoticeVisibility(t *testing.T) {
|
||||
setInstanceNoticeForTest(t, setting.DefaultInstanceNotice())
|
||||
|
||||
setInstanceNoticeForTest(t, setting.InstanceNotice{
|
||||
Enabled: true,
|
||||
Message: "Planned **upgrade** in progress.",
|
||||
Level: setting.InstanceNoticeLevelWarning,
|
||||
ShowIcon: true,
|
||||
Enabled: true,
|
||||
Message: "Planned **upgrade** in progress.",
|
||||
Level: setting.InstanceNoticeLevelWarning,
|
||||
})
|
||||
|
||||
t.Run("AnonymousUserSeesBanner", func(t *testing.T) {
|
||||
@ -86,16 +86,25 @@ func TestInstanceNoticeAdminCRUD(t *testing.T) {
|
||||
|
||||
adminSession := loginUser(t, "user1")
|
||||
req := NewRequestWithValues(t, "POST", "/-/admin/config/instance_notice", map[string]string{
|
||||
"enabled": "true",
|
||||
"message": "Admin set banner",
|
||||
"level": "danger",
|
||||
"show_icon": "true",
|
||||
"enabled": "true",
|
||||
"message": "Admin set banner",
|
||||
"level": "danger",
|
||||
})
|
||||
adminSession.MakeRequest(t, req, http.StatusSeeOther)
|
||||
|
||||
notice := setting.GetInstanceNotice(t.Context())
|
||||
assert.True(t, notice.Enabled)
|
||||
assert.True(t, notice.ShowIcon)
|
||||
assert.Equal(t, "Admin set banner", notice.Message)
|
||||
assert.Equal(t, setting.InstanceNoticeLevelDanger, notice.Level)
|
||||
|
||||
req = NewRequestWithValues(t, "POST", "/-/admin/config/instance_notice", map[string]string{
|
||||
"enabled": "true",
|
||||
"message": strings.Repeat("a", 2001),
|
||||
"level": "warning",
|
||||
})
|
||||
adminSession.MakeRequest(t, req, http.StatusSeeOther)
|
||||
|
||||
notice = setting.GetInstanceNotice(t.Context())
|
||||
assert.Equal(t, "Admin set banner", notice.Message)
|
||||
assert.Equal(t, setting.InstanceNoticeLevelDanger, notice.Level)
|
||||
|
||||
|
||||
@ -21,7 +21,6 @@ function createPreviewDOM() {
|
||||
<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>
|
||||
@ -31,7 +30,7 @@ function createPreviewDOM() {
|
||||
<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>
|
||||
<span data-level="danger"><svg data-icon="warning"></svg></span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@ -69,22 +68,17 @@ describe('Admin Instance Notice Preview', () => {
|
||||
expect(previewContent.innerHTML).toContain('Rendered message');
|
||||
});
|
||||
|
||||
test('updates preview class and icon when level and icon toggle change', () => {
|
||||
test('updates preview class and icon when level changes', () => {
|
||||
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);
|
||||
expect(previewIcon.innerHTML).toContain('data-icon="warning"');
|
||||
});
|
||||
|
||||
test('queues a second render while first request is in flight and re-renders with latest text', async () => {
|
||||
|
||||
@ -10,12 +10,11 @@ function initInstanceNoticePreview(elAdminConfig: HTMLDivElement): void {
|
||||
|
||||
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;
|
||||
if (!inputMessage || !selectLevel || !preview || !previewIcon || !previewContent || !iconContainer) return;
|
||||
|
||||
const iconHTMLByLevel = new Map<string, string>();
|
||||
for (const el of iconContainer.querySelectorAll<HTMLElement>('[data-level]')) {
|
||||
@ -33,7 +32,6 @@ function initInstanceNoticePreview(elAdminConfig: HTMLDivElement): void {
|
||||
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;
|
||||
@ -68,7 +66,6 @@ function initInstanceNoticePreview(elAdminConfig: HTMLDivElement): void {
|
||||
renderPreviewMarkdown();
|
||||
});
|
||||
selectLevel.addEventListener('change', updateStyle);
|
||||
inputShowIcon.addEventListener('change', updateStyle);
|
||||
|
||||
updateStyle();
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user