Commit ad80606a authored by IanShaw027's avatar IanShaw027
Browse files

feat(settings): 增加全局表格分页配置,支持自定义

parent d8fa38d5
...@@ -106,6 +106,8 @@ func (h *SettingHandler) GetSettings(c *gin.Context) { ...@@ -106,6 +106,8 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
HideCcsImportButton: settings.HideCcsImportButton, HideCcsImportButton: settings.HideCcsImportButton,
PurchaseSubscriptionEnabled: settings.PurchaseSubscriptionEnabled, PurchaseSubscriptionEnabled: settings.PurchaseSubscriptionEnabled,
PurchaseSubscriptionURL: settings.PurchaseSubscriptionURL, PurchaseSubscriptionURL: settings.PurchaseSubscriptionURL,
TableDefaultPageSize: settings.TableDefaultPageSize,
TablePageSizeOptions: settings.TablePageSizeOptions,
CustomMenuItems: dto.ParseCustomMenuItems(settings.CustomMenuItems), CustomMenuItems: dto.ParseCustomMenuItems(settings.CustomMenuItems),
CustomEndpoints: dto.ParseCustomEndpoints(settings.CustomEndpoints), CustomEndpoints: dto.ParseCustomEndpoints(settings.CustomEndpoints),
DefaultConcurrency: settings.DefaultConcurrency, DefaultConcurrency: settings.DefaultConcurrency,
...@@ -175,6 +177,8 @@ type UpdateSettingsRequest struct { ...@@ -175,6 +177,8 @@ type UpdateSettingsRequest struct {
HideCcsImportButton bool `json:"hide_ccs_import_button"` HideCcsImportButton bool `json:"hide_ccs_import_button"`
PurchaseSubscriptionEnabled *bool `json:"purchase_subscription_enabled"` PurchaseSubscriptionEnabled *bool `json:"purchase_subscription_enabled"`
PurchaseSubscriptionURL *string `json:"purchase_subscription_url"` PurchaseSubscriptionURL *string `json:"purchase_subscription_url"`
TableDefaultPageSize int `json:"table_default_page_size"`
TablePageSizeOptions []int `json:"table_page_size_options"`
CustomMenuItems *[]dto.CustomMenuItem `json:"custom_menu_items"` CustomMenuItems *[]dto.CustomMenuItem `json:"custom_menu_items"`
CustomEndpoints *[]dto.CustomEndpoint `json:"custom_endpoints"` CustomEndpoints *[]dto.CustomEndpoint `json:"custom_endpoints"`
...@@ -237,6 +241,13 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) { ...@@ -237,6 +241,13 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
if req.DefaultBalance < 0 { if req.DefaultBalance < 0 {
req.DefaultBalance = 0 req.DefaultBalance = 0
} }
// 通用表格配置:兼容旧客户端未传字段时保留当前值。
if req.TableDefaultPageSize <= 0 {
req.TableDefaultPageSize = previousSettings.TableDefaultPageSize
}
if req.TablePageSizeOptions == nil {
req.TablePageSizeOptions = previousSettings.TablePageSizeOptions
}
req.SMTPHost = strings.TrimSpace(req.SMTPHost) req.SMTPHost = strings.TrimSpace(req.SMTPHost)
req.SMTPUsername = strings.TrimSpace(req.SMTPUsername) req.SMTPUsername = strings.TrimSpace(req.SMTPUsername)
req.SMTPPassword = strings.TrimSpace(req.SMTPPassword) req.SMTPPassword = strings.TrimSpace(req.SMTPPassword)
...@@ -564,6 +575,8 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) { ...@@ -564,6 +575,8 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
HideCcsImportButton: req.HideCcsImportButton, HideCcsImportButton: req.HideCcsImportButton,
PurchaseSubscriptionEnabled: purchaseEnabled, PurchaseSubscriptionEnabled: purchaseEnabled,
PurchaseSubscriptionURL: purchaseURL, PurchaseSubscriptionURL: purchaseURL,
TableDefaultPageSize: req.TableDefaultPageSize,
TablePageSizeOptions: req.TablePageSizeOptions,
CustomMenuItems: customMenuJSON, CustomMenuItems: customMenuJSON,
CustomEndpoints: customEndpointsJSON, CustomEndpoints: customEndpointsJSON,
DefaultConcurrency: req.DefaultConcurrency, DefaultConcurrency: req.DefaultConcurrency,
...@@ -679,6 +692,8 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) { ...@@ -679,6 +692,8 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
HideCcsImportButton: updatedSettings.HideCcsImportButton, HideCcsImportButton: updatedSettings.HideCcsImportButton,
PurchaseSubscriptionEnabled: updatedSettings.PurchaseSubscriptionEnabled, PurchaseSubscriptionEnabled: updatedSettings.PurchaseSubscriptionEnabled,
PurchaseSubscriptionURL: updatedSettings.PurchaseSubscriptionURL, PurchaseSubscriptionURL: updatedSettings.PurchaseSubscriptionURL,
TableDefaultPageSize: updatedSettings.TableDefaultPageSize,
TablePageSizeOptions: updatedSettings.TablePageSizeOptions,
CustomMenuItems: dto.ParseCustomMenuItems(updatedSettings.CustomMenuItems), CustomMenuItems: dto.ParseCustomMenuItems(updatedSettings.CustomMenuItems),
CustomEndpoints: dto.ParseCustomEndpoints(updatedSettings.CustomEndpoints), CustomEndpoints: dto.ParseCustomEndpoints(updatedSettings.CustomEndpoints),
DefaultConcurrency: updatedSettings.DefaultConcurrency, DefaultConcurrency: updatedSettings.DefaultConcurrency,
...@@ -871,6 +886,12 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings, ...@@ -871,6 +886,12 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
if before.PurchaseSubscriptionURL != after.PurchaseSubscriptionURL { if before.PurchaseSubscriptionURL != after.PurchaseSubscriptionURL {
changed = append(changed, "purchase_subscription_url") changed = append(changed, "purchase_subscription_url")
} }
if before.TableDefaultPageSize != after.TableDefaultPageSize {
changed = append(changed, "table_default_page_size")
}
if !equalIntSlice(before.TablePageSizeOptions, after.TablePageSizeOptions) {
changed = append(changed, "table_page_size_options")
}
if before.CustomMenuItems != after.CustomMenuItems { if before.CustomMenuItems != after.CustomMenuItems {
changed = append(changed, "custom_menu_items") changed = append(changed, "custom_menu_items")
} }
...@@ -927,6 +948,18 @@ func equalDefaultSubscriptions(a, b []service.DefaultSubscriptionSetting) bool { ...@@ -927,6 +948,18 @@ func equalDefaultSubscriptions(a, b []service.DefaultSubscriptionSetting) bool {
return true return true
} }
func equalIntSlice(a, b []int) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
// TestSMTPRequest 测试SMTP连接请求 // TestSMTPRequest 测试SMTP连接请求
type TestSMTPRequest struct { type TestSMTPRequest struct {
SMTPHost string `json:"smtp_host"` SMTPHost string `json:"smtp_host"`
......
...@@ -61,6 +61,8 @@ type SystemSettings struct { ...@@ -61,6 +61,8 @@ type SystemSettings struct {
HideCcsImportButton bool `json:"hide_ccs_import_button"` HideCcsImportButton bool `json:"hide_ccs_import_button"`
PurchaseSubscriptionEnabled bool `json:"purchase_subscription_enabled"` PurchaseSubscriptionEnabled bool `json:"purchase_subscription_enabled"`
PurchaseSubscriptionURL string `json:"purchase_subscription_url"` PurchaseSubscriptionURL string `json:"purchase_subscription_url"`
TableDefaultPageSize int `json:"table_default_page_size"`
TablePageSizeOptions []int `json:"table_page_size_options"`
CustomMenuItems []CustomMenuItem `json:"custom_menu_items"` CustomMenuItems []CustomMenuItem `json:"custom_menu_items"`
CustomEndpoints []CustomEndpoint `json:"custom_endpoints"` CustomEndpoints []CustomEndpoint `json:"custom_endpoints"`
...@@ -125,6 +127,8 @@ type PublicSettings struct { ...@@ -125,6 +127,8 @@ type PublicSettings struct {
HideCcsImportButton bool `json:"hide_ccs_import_button"` HideCcsImportButton bool `json:"hide_ccs_import_button"`
PurchaseSubscriptionEnabled bool `json:"purchase_subscription_enabled"` PurchaseSubscriptionEnabled bool `json:"purchase_subscription_enabled"`
PurchaseSubscriptionURL string `json:"purchase_subscription_url"` PurchaseSubscriptionURL string `json:"purchase_subscription_url"`
TableDefaultPageSize int `json:"table_default_page_size"`
TablePageSizeOptions []int `json:"table_page_size_options"`
CustomMenuItems []CustomMenuItem `json:"custom_menu_items"` CustomMenuItems []CustomMenuItem `json:"custom_menu_items"`
CustomEndpoints []CustomEndpoint `json:"custom_endpoints"` CustomEndpoints []CustomEndpoint `json:"custom_endpoints"`
LinuxDoOAuthEnabled bool `json:"linuxdo_oauth_enabled"` LinuxDoOAuthEnabled bool `json:"linuxdo_oauth_enabled"`
......
...@@ -9,6 +9,7 @@ import ( ...@@ -9,6 +9,7 @@ import (
"fmt" "fmt"
"log/slog" "log/slog"
"net/url" "net/url"
"sort"
"strconv" "strconv"
"strings" "strings"
"sync/atomic" "sync/atomic"
...@@ -160,6 +161,8 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings ...@@ -160,6 +161,8 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
SettingKeyHideCcsImportButton, SettingKeyHideCcsImportButton,
SettingKeyPurchaseSubscriptionEnabled, SettingKeyPurchaseSubscriptionEnabled,
SettingKeyPurchaseSubscriptionURL, SettingKeyPurchaseSubscriptionURL,
SettingKeyTableDefaultPageSize,
SettingKeyTablePageSizeOptions,
SettingKeyCustomMenuItems, SettingKeyCustomMenuItems,
SettingKeyCustomEndpoints, SettingKeyCustomEndpoints,
SettingKeyLinuxDoConnectEnabled, SettingKeyLinuxDoConnectEnabled,
...@@ -184,6 +187,10 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings ...@@ -184,6 +187,10 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
registrationEmailSuffixWhitelist := ParseRegistrationEmailSuffixWhitelist( registrationEmailSuffixWhitelist := ParseRegistrationEmailSuffixWhitelist(
settings[SettingKeyRegistrationEmailSuffixWhitelist], settings[SettingKeyRegistrationEmailSuffixWhitelist],
) )
tableDefaultPageSize, tablePageSizeOptions := parseTablePreferences(
settings[SettingKeyTableDefaultPageSize],
settings[SettingKeyTablePageSizeOptions],
)
return &PublicSettings{ return &PublicSettings{
RegistrationEnabled: settings[SettingKeyRegistrationEnabled] == "true", RegistrationEnabled: settings[SettingKeyRegistrationEnabled] == "true",
...@@ -205,6 +212,8 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings ...@@ -205,6 +212,8 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
HideCcsImportButton: settings[SettingKeyHideCcsImportButton] == "true", HideCcsImportButton: settings[SettingKeyHideCcsImportButton] == "true",
PurchaseSubscriptionEnabled: settings[SettingKeyPurchaseSubscriptionEnabled] == "true", PurchaseSubscriptionEnabled: settings[SettingKeyPurchaseSubscriptionEnabled] == "true",
PurchaseSubscriptionURL: strings.TrimSpace(settings[SettingKeyPurchaseSubscriptionURL]), PurchaseSubscriptionURL: strings.TrimSpace(settings[SettingKeyPurchaseSubscriptionURL]),
TableDefaultPageSize: tableDefaultPageSize,
TablePageSizeOptions: tablePageSizeOptions,
CustomMenuItems: settings[SettingKeyCustomMenuItems], CustomMenuItems: settings[SettingKeyCustomMenuItems],
CustomEndpoints: settings[SettingKeyCustomEndpoints], CustomEndpoints: settings[SettingKeyCustomEndpoints],
LinuxDoOAuthEnabled: linuxDoEnabled, LinuxDoOAuthEnabled: linuxDoEnabled,
...@@ -252,6 +261,8 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any ...@@ -252,6 +261,8 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any
HideCcsImportButton bool `json:"hide_ccs_import_button"` HideCcsImportButton bool `json:"hide_ccs_import_button"`
PurchaseSubscriptionEnabled bool `json:"purchase_subscription_enabled"` PurchaseSubscriptionEnabled bool `json:"purchase_subscription_enabled"`
PurchaseSubscriptionURL string `json:"purchase_subscription_url,omitempty"` PurchaseSubscriptionURL string `json:"purchase_subscription_url,omitempty"`
TableDefaultPageSize int `json:"table_default_page_size"`
TablePageSizeOptions []int `json:"table_page_size_options"`
CustomMenuItems json.RawMessage `json:"custom_menu_items"` CustomMenuItems json.RawMessage `json:"custom_menu_items"`
CustomEndpoints json.RawMessage `json:"custom_endpoints"` CustomEndpoints json.RawMessage `json:"custom_endpoints"`
LinuxDoOAuthEnabled bool `json:"linuxdo_oauth_enabled"` LinuxDoOAuthEnabled bool `json:"linuxdo_oauth_enabled"`
...@@ -277,6 +288,8 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any ...@@ -277,6 +288,8 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any
HideCcsImportButton: settings.HideCcsImportButton, HideCcsImportButton: settings.HideCcsImportButton,
PurchaseSubscriptionEnabled: settings.PurchaseSubscriptionEnabled, PurchaseSubscriptionEnabled: settings.PurchaseSubscriptionEnabled,
PurchaseSubscriptionURL: settings.PurchaseSubscriptionURL, PurchaseSubscriptionURL: settings.PurchaseSubscriptionURL,
TableDefaultPageSize: settings.TableDefaultPageSize,
TablePageSizeOptions: settings.TablePageSizeOptions,
CustomMenuItems: filterUserVisibleMenuItems(settings.CustomMenuItems), CustomMenuItems: filterUserVisibleMenuItems(settings.CustomMenuItems),
CustomEndpoints: safeRawJSONArray(settings.CustomEndpoints), CustomEndpoints: safeRawJSONArray(settings.CustomEndpoints),
LinuxDoOAuthEnabled: settings.LinuxDoOAuthEnabled, LinuxDoOAuthEnabled: settings.LinuxDoOAuthEnabled,
...@@ -471,6 +484,16 @@ func (s *SettingService) UpdateSettings(ctx context.Context, settings *SystemSet ...@@ -471,6 +484,16 @@ func (s *SettingService) UpdateSettings(ctx context.Context, settings *SystemSet
updates[SettingKeyHideCcsImportButton] = strconv.FormatBool(settings.HideCcsImportButton) updates[SettingKeyHideCcsImportButton] = strconv.FormatBool(settings.HideCcsImportButton)
updates[SettingKeyPurchaseSubscriptionEnabled] = strconv.FormatBool(settings.PurchaseSubscriptionEnabled) updates[SettingKeyPurchaseSubscriptionEnabled] = strconv.FormatBool(settings.PurchaseSubscriptionEnabled)
updates[SettingKeyPurchaseSubscriptionURL] = strings.TrimSpace(settings.PurchaseSubscriptionURL) updates[SettingKeyPurchaseSubscriptionURL] = strings.TrimSpace(settings.PurchaseSubscriptionURL)
tableDefaultPageSize, tablePageSizeOptions := normalizeTablePreferences(
settings.TableDefaultPageSize,
settings.TablePageSizeOptions,
)
updates[SettingKeyTableDefaultPageSize] = strconv.Itoa(tableDefaultPageSize)
tablePageSizeOptionsJSON, err := json.Marshal(tablePageSizeOptions)
if err != nil {
return fmt.Errorf("marshal table page size options: %w", err)
}
updates[SettingKeyTablePageSizeOptions] = string(tablePageSizeOptionsJSON)
updates[SettingKeyCustomMenuItems] = settings.CustomMenuItems updates[SettingKeyCustomMenuItems] = settings.CustomMenuItems
updates[SettingKeyCustomEndpoints] = settings.CustomEndpoints updates[SettingKeyCustomEndpoints] = settings.CustomEndpoints
...@@ -824,6 +847,8 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error { ...@@ -824,6 +847,8 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error {
SettingKeySiteLogo: "", SettingKeySiteLogo: "",
SettingKeyPurchaseSubscriptionEnabled: "false", SettingKeyPurchaseSubscriptionEnabled: "false",
SettingKeyPurchaseSubscriptionURL: "", SettingKeyPurchaseSubscriptionURL: "",
SettingKeyTableDefaultPageSize: "20",
SettingKeyTablePageSizeOptions: "[10,20,50,100]",
SettingKeyCustomMenuItems: "[]", SettingKeyCustomMenuItems: "[]",
SettingKeyCustomEndpoints: "[]", SettingKeyCustomEndpoints: "[]",
SettingKeyDefaultConcurrency: strconv.Itoa(s.cfg.Default.UserConcurrency), SettingKeyDefaultConcurrency: strconv.Itoa(s.cfg.Default.UserConcurrency),
...@@ -893,6 +918,10 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin ...@@ -893,6 +918,10 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
CustomEndpoints: settings[SettingKeyCustomEndpoints], CustomEndpoints: settings[SettingKeyCustomEndpoints],
BackendModeEnabled: settings[SettingKeyBackendModeEnabled] == "true", BackendModeEnabled: settings[SettingKeyBackendModeEnabled] == "true",
} }
result.TableDefaultPageSize, result.TablePageSizeOptions = parseTablePreferences(
settings[SettingKeyTableDefaultPageSize],
settings[SettingKeyTablePageSizeOptions],
)
// 解析整数类型 // 解析整数类型
if port, err := strconv.Atoi(settings[SettingKeySMTPPort]); err == nil { if port, err := strconv.Atoi(settings[SettingKeySMTPPort]); err == nil {
...@@ -1036,6 +1065,59 @@ func parseDefaultSubscriptions(raw string) []DefaultSubscriptionSetting { ...@@ -1036,6 +1065,59 @@ func parseDefaultSubscriptions(raw string) []DefaultSubscriptionSetting {
return normalized return normalized
} }
func parseTablePreferences(defaultPageSizeRaw, optionsRaw string) (int, []int) {
defaultPageSize := 20
if v, err := strconv.Atoi(strings.TrimSpace(defaultPageSizeRaw)); err == nil {
defaultPageSize = v
}
var options []int
if strings.TrimSpace(optionsRaw) != "" {
_ = json.Unmarshal([]byte(optionsRaw), &options)
}
return normalizeTablePreferences(defaultPageSize, options)
}
func normalizeTablePreferences(defaultPageSize int, options []int) (int, []int) {
const minPageSize = 5
const maxPageSize = 1000
const fallbackPageSize = 20
seen := make(map[int]struct{}, len(options))
normalizedOptions := make([]int, 0, len(options))
for _, option := range options {
if option < minPageSize || option > maxPageSize {
continue
}
if _, ok := seen[option]; ok {
continue
}
seen[option] = struct{}{}
normalizedOptions = append(normalizedOptions, option)
}
sort.Ints(normalizedOptions)
if defaultPageSize < minPageSize || defaultPageSize > maxPageSize {
defaultPageSize = fallbackPageSize
}
if len(normalizedOptions) == 0 {
normalizedOptions = []int{10, 20, 50}
}
return defaultPageSize, normalizedOptions
}
func containsInt(values []int, target int) bool {
for _, value := range values {
if value == target {
return true
}
}
return false
}
// getStringOrDefault 获取字符串值或默认值 // getStringOrDefault 获取字符串值或默认值
func (s *SettingService) getStringOrDefault(settings map[string]string, key, defaultValue string) string { func (s *SettingService) getStringOrDefault(settings map[string]string, key, defaultValue string) string {
if value, ok := settings[key]; ok && value != "" { if value, ok := settings[key]; ok && value != "" {
......
...@@ -62,3 +62,18 @@ func TestSettingService_GetPublicSettings_ExposesRegistrationEmailSuffixWhitelis ...@@ -62,3 +62,18 @@ func TestSettingService_GetPublicSettings_ExposesRegistrationEmailSuffixWhitelis
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, []string{"@example.com", "@foo.bar"}, settings.RegistrationEmailSuffixWhitelist) require.Equal(t, []string{"@example.com", "@foo.bar"}, settings.RegistrationEmailSuffixWhitelist)
} }
func TestSettingService_GetPublicSettings_ExposesTablePreferences(t *testing.T) {
repo := &settingPublicRepoStub{
values: map[string]string{
SettingKeyTableDefaultPageSize: "50",
SettingKeyTablePageSizeOptions: "[20,50,100]",
},
}
svc := NewSettingService(repo, &config.Config{})
settings, err := svc.GetPublicSettings(context.Background())
require.NoError(t, err)
require.Equal(t, 50, settings.TableDefaultPageSize)
require.Equal(t, []int{20, 50, 100}, settings.TablePageSizeOptions)
}
...@@ -202,3 +202,24 @@ func TestParseDefaultSubscriptions_NormalizesValues(t *testing.T) { ...@@ -202,3 +202,24 @@ func TestParseDefaultSubscriptions_NormalizesValues(t *testing.T) {
{GroupID: 12, ValidityDays: MaxValidityDays}, {GroupID: 12, ValidityDays: MaxValidityDays},
}, got) }, got)
} }
func TestSettingService_UpdateSettings_TablePreferences(t *testing.T) {
repo := &settingUpdateRepoStub{}
svc := NewSettingService(repo, &config.Config{})
err := svc.UpdateSettings(context.Background(), &SystemSettings{
TableDefaultPageSize: 50,
TablePageSizeOptions: []int{20, 50, 100},
})
require.NoError(t, err)
require.Equal(t, "50", repo.updates[SettingKeyTableDefaultPageSize])
require.Equal(t, "[20,50,100]", repo.updates[SettingKeyTablePageSizeOptions])
err = svc.UpdateSettings(context.Background(), &SystemSettings{
TableDefaultPageSize: 1000,
TablePageSizeOptions: []int{20, 100},
})
require.NoError(t, err)
require.Equal(t, "1000", repo.updates[SettingKeyTableDefaultPageSize])
require.Equal(t, "[20,100]", repo.updates[SettingKeyTablePageSizeOptions])
}
...@@ -41,6 +41,8 @@ type SystemSettings struct { ...@@ -41,6 +41,8 @@ type SystemSettings struct {
HideCcsImportButton bool HideCcsImportButton bool
PurchaseSubscriptionEnabled bool PurchaseSubscriptionEnabled bool
PurchaseSubscriptionURL string PurchaseSubscriptionURL string
TableDefaultPageSize int
TablePageSizeOptions []int
CustomMenuItems string // JSON array of custom menu items CustomMenuItems string // JSON array of custom menu items
CustomEndpoints string // JSON array of custom endpoints CustomEndpoints string // JSON array of custom endpoints
...@@ -107,6 +109,8 @@ type PublicSettings struct { ...@@ -107,6 +109,8 @@ type PublicSettings struct {
PurchaseSubscriptionEnabled bool PurchaseSubscriptionEnabled bool
PurchaseSubscriptionURL string PurchaseSubscriptionURL string
TableDefaultPageSize int
TablePageSizeOptions []int
CustomMenuItems string // JSON array of custom menu items CustomMenuItems string // JSON array of custom menu items
CustomEndpoints string // JSON array of custom endpoints CustomEndpoints string // JSON array of custom endpoints
......
...@@ -40,6 +40,8 @@ export interface SystemSettings { ...@@ -40,6 +40,8 @@ export interface SystemSettings {
hide_ccs_import_button: boolean hide_ccs_import_button: boolean
purchase_subscription_enabled: boolean purchase_subscription_enabled: boolean
purchase_subscription_url: string purchase_subscription_url: string
table_default_page_size: number
table_page_size_options: number[]
backend_mode_enabled: boolean backend_mode_enabled: boolean
custom_menu_items: CustomMenuItem[] custom_menu_items: CustomMenuItem[]
custom_endpoints: CustomEndpoint[] custom_endpoints: CustomEndpoint[]
...@@ -114,6 +116,8 @@ export interface UpdateSettingsRequest { ...@@ -114,6 +116,8 @@ export interface UpdateSettingsRequest {
hide_ccs_import_button?: boolean hide_ccs_import_button?: boolean
purchase_subscription_enabled?: boolean purchase_subscription_enabled?: boolean
purchase_subscription_url?: string purchase_subscription_url?: string
table_default_page_size?: number
table_page_size_options?: number[]
backend_mode_enabled?: boolean backend_mode_enabled?: boolean
custom_menu_items?: CustomMenuItem[] custom_menu_items?: CustomMenuItem[]
custom_endpoints?: CustomEndpoint[] custom_endpoints?: CustomEndpoint[]
......
...@@ -123,6 +123,7 @@ import { useI18n } from 'vue-i18n' ...@@ -123,6 +123,7 @@ import { useI18n } from 'vue-i18n'
import Icon from '@/components/icons/Icon.vue' import Icon from '@/components/icons/Icon.vue'
import Select from './Select.vue' import Select from './Select.vue'
import { setPersistedPageSize } from '@/composables/usePersistedPageSize' import { setPersistedPageSize } from '@/composables/usePersistedPageSize'
import { getConfiguredTablePageSizeOptions, normalizeTablePageSize } from '@/utils/tablePreferences'
const { t } = useI18n() const { t } = useI18n()
...@@ -141,7 +142,7 @@ interface Emits { ...@@ -141,7 +142,7 @@ interface Emits {
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
pageSizeOptions: () => [10, 20, 50, 100], pageSizeOptions: () => getConfiguredTablePageSizeOptions(),
showPageSizeSelector: true, showPageSizeSelector: true,
showJump: false showJump: false
}) })
...@@ -161,7 +162,14 @@ const toItem = computed(() => { ...@@ -161,7 +162,14 @@ const toItem = computed(() => {
}) })
const pageSizeSelectOptions = computed(() => { const pageSizeSelectOptions = computed(() => {
return props.pageSizeOptions.map((size) => ({ const options = Array.from(
new Set([
...getConfiguredTablePageSizeOptions(),
normalizeTablePageSize(props.pageSize)
])
).sort((a, b) => a - b)
return options.map((size) => ({
value: size, value: size,
label: String(size) label: String(size)
})) }))
...@@ -216,7 +224,7 @@ const goToPage = (newPage: number) => { ...@@ -216,7 +224,7 @@ const goToPage = (newPage: number) => {
const handlePageSizeChange = (value: string | number | boolean | null) => { const handlePageSizeChange = (value: string | number | boolean | null) => {
if (value === null || typeof value === 'boolean') return if (value === null || typeof value === 'boolean') return
const newPageSize = typeof value === 'string' ? parseInt(value) : value const newPageSize = normalizeTablePageSize(typeof value === 'string' ? parseInt(value, 10) : value)
setPersistedPageSize(newPageSize) setPersistedPageSize(newPageSize)
emit('update:pageSize', newPageSize) emit('update:pageSize', newPageSize)
} }
......
import { getConfiguredTableDefaultPageSize, normalizeTablePageSize } from '@/utils/tablePreferences'
const STORAGE_KEY = 'table-page-size' const STORAGE_KEY = 'table-page-size'
const DEFAULT_PAGE_SIZE = 20 const SOURCE_KEY = 'table-page-size-source'
/** /**
* 从 localStorage 读取/写入 pageSize * 从 localStorage 读取/写入 pageSize
* 全局共享一个 key,所有表格统一偏好 * 全局共享一个 key,所有表格统一偏好
*/ */
export function getPersistedPageSize(fallback = DEFAULT_PAGE_SIZE): number { export function getPersistedPageSize(fallback = getConfiguredTableDefaultPageSize()): number {
try { try {
const stored = localStorage.getItem(STORAGE_KEY) const stored = localStorage.getItem(STORAGE_KEY)
if (stored) { if (stored) {
const parsed = Number(stored) return normalizeTablePageSize(stored)
if (Number.isFinite(parsed) && parsed > 0) return parsed
} }
} catch { } catch {
// localStorage 不可用(隐私模式等) // localStorage 不可用(隐私模式等)
} }
return fallback return normalizeTablePageSize(fallback)
} }
export function setPersistedPageSize(size: number): void { export function setPersistedPageSize(size: number): void {
try { try {
localStorage.setItem(STORAGE_KEY, String(size)) localStorage.setItem(STORAGE_KEY, String(normalizeTablePageSize(size)))
localStorage.setItem(SOURCE_KEY, 'user')
} catch {
// 静默失败
}
}
export function syncPersistedPageSizeWithSystemDefault(defaultSize = getConfiguredTableDefaultPageSize()): void {
try {
const normalizedDefault = normalizeTablePageSize(defaultSize)
const stored = localStorage.getItem(STORAGE_KEY)
const source = localStorage.getItem(SOURCE_KEY)
const normalizedStored = stored ? normalizeTablePageSize(stored) : null
if ((source === 'user' || (source === null && stored !== null)) && stored) {
localStorage.setItem(STORAGE_KEY, String(normalizedStored ?? normalizedDefault))
localStorage.setItem(SOURCE_KEY, 'user')
return
}
localStorage.setItem(STORAGE_KEY, String(normalizedDefault))
localStorage.setItem(SOURCE_KEY, 'system')
} catch { } catch {
// 静默失败 // 静默失败
} }
......
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { setActivePinia, createPinia } from 'pinia' import { setActivePinia, createPinia } from 'pinia'
import { useAppStore } from '@/stores/app' import { useAppStore } from '@/stores/app'
import { getPublicSettings } from '@/api/auth'
// Mock API 模块 // Mock API 模块
vi.mock('@/api/admin/system', () => ({ vi.mock('@/api/admin/system', () => ({
...@@ -15,12 +16,14 @@ describe('useAppStore', () => { ...@@ -15,12 +16,14 @@ describe('useAppStore', () => {
beforeEach(() => { beforeEach(() => {
setActivePinia(createPinia()) setActivePinia(createPinia())
vi.useFakeTimers() vi.useFakeTimers()
localStorage.clear()
// 清除 window.__APP_CONFIG__ // 清除 window.__APP_CONFIG__
delete (window as any).__APP_CONFIG__ delete (window as any).__APP_CONFIG__
}) })
afterEach(() => { afterEach(() => {
vi.useRealTimers() vi.useRealTimers()
localStorage.clear()
}) })
// --- Toast 消息管理 --- // --- Toast 消息管理 ---
...@@ -291,5 +294,120 @@ describe('useAppStore', () => { ...@@ -291,5 +294,120 @@ describe('useAppStore', () => {
expect(store.publicSettingsLoaded).toBe(false) expect(store.publicSettingsLoaded).toBe(false)
expect(store.cachedPublicSettings).toBeNull() expect(store.cachedPublicSettings).toBeNull()
}) })
it('fetchPublicSettings(force) 会同步更新运行时注入配置', async () => {
vi.mocked(getPublicSettings).mockResolvedValue({
registration_enabled: false,
email_verify_enabled: false,
registration_email_suffix_whitelist: [],
promo_code_enabled: true,
password_reset_enabled: false,
invitation_code_enabled: false,
turnstile_enabled: false,
turnstile_site_key: '',
site_name: 'Updated Site',
site_logo: '',
site_subtitle: '',
api_base_url: '',
contact_info: '',
doc_url: '',
home_content: '',
hide_ccs_import_button: false,
purchase_subscription_enabled: false,
purchase_subscription_url: '',
table_default_page_size: 1000,
table_page_size_options: [20, 100, 1000],
custom_menu_items: [],
custom_endpoints: [],
linuxdo_oauth_enabled: false,
backend_mode_enabled: false,
version: '1.0.0'
})
const store = useAppStore()
await store.fetchPublicSettings(true)
expect((window as any).__APP_CONFIG__.table_default_page_size).toBe(1000)
expect((window as any).__APP_CONFIG__.table_page_size_options).toEqual([20, 100, 1000])
expect(localStorage.getItem('table-page-size')).toBe('1000')
expect(localStorage.getItem('table-page-size-source')).toBe('system')
})
it('fetchPublicSettings(force) 保留用户显式选择的分页大小', async () => {
localStorage.setItem('table-page-size', '100')
localStorage.setItem('table-page-size-source', 'user')
vi.mocked(getPublicSettings).mockResolvedValue({
registration_enabled: false,
email_verify_enabled: false,
registration_email_suffix_whitelist: [],
promo_code_enabled: true,
password_reset_enabled: false,
invitation_code_enabled: false,
turnstile_enabled: false,
turnstile_site_key: '',
site_name: 'Updated Site',
site_logo: '',
site_subtitle: '',
api_base_url: '',
contact_info: '',
doc_url: '',
home_content: '',
hide_ccs_import_button: false,
purchase_subscription_enabled: false,
purchase_subscription_url: '',
table_default_page_size: 1000,
table_page_size_options: [20, 50, 1000],
custom_menu_items: [],
custom_endpoints: [],
linuxdo_oauth_enabled: false,
backend_mode_enabled: false,
version: '1.0.0'
})
const store = useAppStore()
await store.fetchPublicSettings(true)
expect(localStorage.getItem('table-page-size')).toBe('1000')
expect(localStorage.getItem('table-page-size-source')).toBe('user')
})
it('fetchPublicSettings(force) 保留旧版本未标记来源的分页偏好', async () => {
localStorage.setItem('table-page-size', '50')
vi.mocked(getPublicSettings).mockResolvedValue({
registration_enabled: false,
email_verify_enabled: false,
registration_email_suffix_whitelist: [],
promo_code_enabled: true,
password_reset_enabled: false,
invitation_code_enabled: false,
turnstile_enabled: false,
turnstile_site_key: '',
site_name: 'Updated Site',
site_logo: '',
site_subtitle: '',
api_base_url: '',
contact_info: '',
doc_url: '',
home_content: '',
hide_ccs_import_button: false,
purchase_subscription_enabled: false,
purchase_subscription_url: '',
table_default_page_size: 1000,
table_page_size_options: [20, 50, 1000],
custom_menu_items: [],
custom_endpoints: [],
linuxdo_oauth_enabled: false,
backend_mode_enabled: false,
version: '1.0.0'
})
const store = useAppStore()
await store.fetchPublicSettings(true)
expect(localStorage.getItem('table-page-size')).toBe('50')
expect(localStorage.getItem('table-page-size-source')).toBe('user')
})
}) })
}) })
...@@ -12,6 +12,7 @@ import { ...@@ -12,6 +12,7 @@ import {
type ReleaseInfo type ReleaseInfo
} from '@/api/admin/system' } from '@/api/admin/system'
import { getPublicSettings as fetchPublicSettingsAPI } from '@/api/auth' import { getPublicSettings as fetchPublicSettingsAPI } from '@/api/auth'
import { syncPersistedPageSizeWithSystemDefault } from '@/composables/usePersistedPageSize'
export const useAppStore = defineStore('app', () => { export const useAppStore = defineStore('app', () => {
// ==================== State ==================== // ==================== State ====================
...@@ -284,6 +285,10 @@ export const useAppStore = defineStore('app', () => { ...@@ -284,6 +285,10 @@ export const useAppStore = defineStore('app', () => {
* Apply settings to store state (internal helper to avoid code duplication) * Apply settings to store state (internal helper to avoid code duplication)
*/ */
function applySettings(config: PublicSettings): void { function applySettings(config: PublicSettings): void {
if (typeof window !== 'undefined') {
window.__APP_CONFIG__ = { ...config }
}
syncPersistedPageSizeWithSystemDefault(config.table_default_page_size)
cachedPublicSettings.value = config cachedPublicSettings.value = config
siteName.value = config.site_name || 'Sub2API' siteName.value = config.site_name || 'Sub2API'
siteLogo.value = config.site_logo || '' siteLogo.value = config.site_logo || ''
...@@ -329,6 +334,8 @@ export const useAppStore = defineStore('app', () => { ...@@ -329,6 +334,8 @@ export const useAppStore = defineStore('app', () => {
hide_ccs_import_button: false, hide_ccs_import_button: false,
purchase_subscription_enabled: false, purchase_subscription_enabled: false,
purchase_subscription_url: '', purchase_subscription_url: '',
table_default_page_size: 20,
table_page_size_options: [10, 20, 50],
custom_menu_items: [], custom_menu_items: [],
custom_endpoints: [], custom_endpoints: [],
linuxdo_oauth_enabled: false, linuxdo_oauth_enabled: false,
......
import { afterEach, describe, expect, it } from 'vitest'
import {
DEFAULT_TABLE_PAGE_SIZE,
DEFAULT_TABLE_PAGE_SIZE_OPTIONS,
getConfiguredTableDefaultPageSize,
getConfiguredTablePageSizeOptions,
normalizeTablePageSize
} from '@/utils/tablePreferences'
describe('tablePreferences', () => {
afterEach(() => {
delete window.__APP_CONFIG__
})
it('returns built-in defaults when app config is missing', () => {
expect(getConfiguredTableDefaultPageSize()).toBe(DEFAULT_TABLE_PAGE_SIZE)
expect(getConfiguredTablePageSizeOptions()).toEqual(DEFAULT_TABLE_PAGE_SIZE_OPTIONS)
})
it('uses configured defaults when app config is valid', () => {
window.__APP_CONFIG__ = {
table_default_page_size: 50,
table_page_size_options: [20, 50, 100]
} as any
expect(getConfiguredTableDefaultPageSize()).toBe(50)
expect(getConfiguredTablePageSizeOptions()).toEqual([20, 50, 100])
})
it('allows default page size outside selectable options', () => {
window.__APP_CONFIG__ = {
table_default_page_size: 1000,
table_page_size_options: [20, 50, 100]
} as any
expect(getConfiguredTableDefaultPageSize()).toBe(1000)
expect(getConfiguredTablePageSizeOptions()).toEqual([20, 50, 100])
expect(normalizeTablePageSize(1000)).toBe(100)
expect(normalizeTablePageSize(35)).toBe(50)
})
it('normalizes invalid options without rewriting the configured default itself', () => {
window.__APP_CONFIG__ = {
table_default_page_size: 35,
table_page_size_options: [1001, 50, 10, 10, 2, 0]
} as any
expect(getConfiguredTableDefaultPageSize()).toBe(35)
expect(getConfiguredTablePageSizeOptions()).toEqual([10, 50])
expect(normalizeTablePageSize(undefined)).toBe(50)
})
it('normalizes page size against configured options by rounding up', () => {
window.__APP_CONFIG__ = {
table_default_page_size: 20,
table_page_size_options: [20, 50, 1000]
} as any
expect(normalizeTablePageSize(20)).toBe(20)
expect(normalizeTablePageSize(30)).toBe(50)
expect(normalizeTablePageSize(100)).toBe(1000)
expect(normalizeTablePageSize(1500)).toBe(1000)
expect(normalizeTablePageSize(undefined)).toBe(20)
})
it('keeps built-in selectable defaults at 10, 20, 50', () => {
window.__APP_CONFIG__ = {
table_default_page_size: 1000
} as any
expect(getConfiguredTablePageSizeOptions()).toEqual([10, 20, 50])
})
})
const MIN_TABLE_PAGE_SIZE = 5
const MAX_TABLE_PAGE_SIZE = 1000
export const DEFAULT_TABLE_PAGE_SIZE = 20
export const DEFAULT_TABLE_PAGE_SIZE_OPTIONS = [10, 20, 50]
const sanitizePageSize = (value: unknown): number | null => {
const size = Number(value)
if (!Number.isInteger(size)) return null
if (size < MIN_TABLE_PAGE_SIZE || size > MAX_TABLE_PAGE_SIZE) return null
return size
}
const parsePageSizeForSelection = (value: unknown): number | null => {
const size = Number(value)
if (!Number.isInteger(size)) return null
if (size < MIN_TABLE_PAGE_SIZE) return null
return size
}
const getInjectedAppConfig = () => {
if (typeof window === 'undefined') return null
return window.__APP_CONFIG__ ?? null
}
const getSanitizedConfiguredOptions = (): number[] => {
const configured = getInjectedAppConfig()?.table_page_size_options
if (!Array.isArray(configured)) return []
return Array.from(
new Set(
configured
.map((value) => sanitizePageSize(value))
.filter((value): value is number => value !== null)
)
).sort((a, b) => a - b)
}
const normalizePageSizeToOptions = (value: number, options: number[]): number => {
for (const option of options) {
if (option >= value) {
return option
}
}
return options[options.length - 1]
}
export const getConfiguredTableDefaultPageSize = (): number => {
const configured = sanitizePageSize(getInjectedAppConfig()?.table_default_page_size)
if (configured === null) {
return DEFAULT_TABLE_PAGE_SIZE
}
return configured
}
export const getConfiguredTablePageSizeOptions = (): number[] => {
const unique = getSanitizedConfiguredOptions()
if (unique.length === 0) {
return [...DEFAULT_TABLE_PAGE_SIZE_OPTIONS]
}
return unique.length > 0 ? unique : [...DEFAULT_TABLE_PAGE_SIZE_OPTIONS]
}
export const normalizeTablePageSize = (value: unknown): number => {
const normalized = parsePageSizeForSelection(value)
const defaultSize = getConfiguredTableDefaultPageSize()
const options = getConfiguredTablePageSizeOptions()
if (normalized !== null) {
return normalizePageSizeToOptions(normalized, options)
}
return normalizePageSizeToOptions(defaultSize, options)
}
...@@ -1468,6 +1468,48 @@ ...@@ -1468,6 +1468,48 @@
</p> </p>
</div> </div>
<!-- Global Table Preferences -->
<div class="border-t border-gray-100 pt-4 dark:border-dark-700">
<h3 class="text-sm font-medium text-gray-900 dark:text-white">
{{ t('admin.settings.site.tablePreferencesTitle') }}
</h3>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.settings.site.tablePreferencesDescription') }}
</p>
<div class="mt-4 grid grid-cols-1 gap-6 md:grid-cols-2">
<div>
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ t('admin.settings.site.tableDefaultPageSize') }}
</label>
<input
v-model.number="form.table_default_page_size"
type="number"
min="5"
max="1000"
step="1"
class="input w-40"
/>
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.settings.site.tableDefaultPageSizeHint') }}
</p>
</div>
<div>
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ t('admin.settings.site.tablePageSizeOptions') }}
</label>
<input
v-model="tablePageSizeOptionsInput"
type="text"
class="input font-mono text-sm"
:placeholder="t('admin.settings.site.tablePageSizeOptionsPlaceholder')"
/>
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.settings.site.tablePageSizeOptionsHint') }}
</p>
</div>
</div>
</div>
<!-- Custom Endpoints --> <!-- Custom Endpoints -->
<div> <div>
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"> <label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
...@@ -2125,6 +2167,7 @@ const smtpPasswordManuallyEdited = ref(false) ...@@ -2125,6 +2167,7 @@ const smtpPasswordManuallyEdited = ref(false)
const testEmailAddress = ref('') const testEmailAddress = ref('')
const registrationEmailSuffixWhitelistTags = ref<string[]>([]) const registrationEmailSuffixWhitelistTags = ref<string[]>([])
const registrationEmailSuffixWhitelistDraft = ref('') const registrationEmailSuffixWhitelistDraft = ref('')
const tablePageSizeOptionsInput = ref('10, 20, 50')
// Admin API Key 状态 // Admin API Key 状态
const adminApiKeyLoading = ref(true) const adminApiKeyLoading = ref(true)
...@@ -2179,6 +2222,10 @@ const betaPolicyForm = reactive({ ...@@ -2179,6 +2222,10 @@ const betaPolicyForm = reactive({
}> }>
}) })
const tablePageSizeMin = 5
const tablePageSizeMax = 1000
const tablePageSizeDefault = 20
interface DefaultSubscriptionGroupOption { interface DefaultSubscriptionGroupOption {
value: number value: number
label: string label: string
...@@ -2218,6 +2265,8 @@ const form = reactive<SettingsForm>({ ...@@ -2218,6 +2265,8 @@ const form = reactive<SettingsForm>({
hide_ccs_import_button: false, hide_ccs_import_button: false,
purchase_subscription_enabled: false, purchase_subscription_enabled: false,
purchase_subscription_url: '', purchase_subscription_url: '',
table_default_page_size: tablePageSizeDefault,
table_page_size_options: [10, 20, 50],
custom_menu_items: [] as Array<{id: string; label: string; icon_svg: string; url: string; visibility: 'user' | 'admin'; sort_order: number}>, custom_menu_items: [] as Array<{id: string; label: string; icon_svg: string; url: string; visibility: 'user' | 'admin'; sort_order: number}>,
custom_endpoints: [] as Array<{name: string; endpoint: string; description: string}>, custom_endpoints: [] as Array<{name: string; endpoint: string; description: string}>,
frontend_url: '', frontend_url: '',
...@@ -2402,6 +2451,35 @@ function removeEndpoint(index: number) { ...@@ -2402,6 +2451,35 @@ function removeEndpoint(index: number) {
form.custom_endpoints.splice(index, 1) form.custom_endpoints.splice(index, 1)
} }
function formatTablePageSizeOptions(options: number[]): string {
return options.join(', ')
}
function parseTablePageSizeOptionsInput(raw: string): number[] | null {
const tokens = raw
.split(',')
.map((token) => token.trim())
.filter((token) => token.length > 0)
if (tokens.length === 0) {
return null
}
const parsed = tokens.map((token) => Number(token))
if (parsed.some((value) => !Number.isInteger(value))) {
return null
}
const deduped = Array.from(new Set(parsed)).sort((a, b) => a - b)
if (
deduped.some((value) => value < tablePageSizeMin || value > tablePageSizeMax)
) {
return null
}
return deduped
}
async function loadSettings() { async function loadSettings() {
loading.value = true loading.value = true
loadFailed.value = false loadFailed.value = false
...@@ -2420,6 +2498,9 @@ async function loadSettings() { ...@@ -2420,6 +2498,9 @@ async function loadSettings() {
registrationEmailSuffixWhitelistTags.value = normalizeRegistrationEmailSuffixDomains( registrationEmailSuffixWhitelistTags.value = normalizeRegistrationEmailSuffixDomains(
settings.registration_email_suffix_whitelist settings.registration_email_suffix_whitelist
) )
tablePageSizeOptionsInput.value = formatTablePageSizeOptions(
Array.isArray(settings.table_page_size_options) ? settings.table_page_size_options : [10, 20, 50]
)
registrationEmailSuffixWhitelistDraft.value = '' registrationEmailSuffixWhitelistDraft.value = ''
form.smtp_password = '' form.smtp_password = ''
smtpPasswordManuallyEdited.value = false smtpPasswordManuallyEdited.value = false
...@@ -2465,6 +2546,37 @@ function removeDefaultSubscription(index: number) { ...@@ -2465,6 +2546,37 @@ function removeDefaultSubscription(index: number) {
async function saveSettings() { async function saveSettings() {
saving.value = true saving.value = true
try { try {
const normalizedTableDefaultPageSize = Math.floor(Number(form.table_default_page_size))
if (
!Number.isInteger(normalizedTableDefaultPageSize) ||
normalizedTableDefaultPageSize < tablePageSizeMin ||
normalizedTableDefaultPageSize > tablePageSizeMax
) {
appStore.showError(
t('admin.settings.site.tableDefaultPageSizeRangeError', {
min: tablePageSizeMin,
max: tablePageSizeMax
})
)
return
}
const normalizedTablePageSizeOptions = parseTablePageSizeOptionsInput(
tablePageSizeOptionsInput.value
)
if (!normalizedTablePageSizeOptions) {
appStore.showError(
t('admin.settings.site.tablePageSizeOptionsFormatError', {
min: tablePageSizeMin,
max: tablePageSizeMax
})
)
return
}
form.table_default_page_size = normalizedTableDefaultPageSize
form.table_page_size_options = normalizedTablePageSizeOptions
const normalizedDefaultSubscriptions = form.default_subscriptions const normalizedDefaultSubscriptions = form.default_subscriptions
.filter((item) => item.group_id > 0 && item.validity_days > 0) .filter((item) => item.group_id > 0 && item.validity_days > 0)
.map((item: DefaultSubscriptionSetting) => ({ .map((item: DefaultSubscriptionSetting) => ({
...@@ -2542,6 +2654,8 @@ async function saveSettings() { ...@@ -2542,6 +2654,8 @@ async function saveSettings() {
hide_ccs_import_button: form.hide_ccs_import_button, hide_ccs_import_button: form.hide_ccs_import_button,
purchase_subscription_enabled: form.purchase_subscription_enabled, purchase_subscription_enabled: form.purchase_subscription_enabled,
purchase_subscription_url: form.purchase_subscription_url, purchase_subscription_url: form.purchase_subscription_url,
table_default_page_size: form.table_default_page_size,
table_page_size_options: form.table_page_size_options,
custom_menu_items: form.custom_menu_items, custom_menu_items: form.custom_menu_items,
custom_endpoints: form.custom_endpoints, custom_endpoints: form.custom_endpoints,
frontend_url: form.frontend_url, frontend_url: form.frontend_url,
...@@ -2578,6 +2692,9 @@ async function saveSettings() { ...@@ -2578,6 +2692,9 @@ async function saveSettings() {
registrationEmailSuffixWhitelistTags.value = normalizeRegistrationEmailSuffixDomains( registrationEmailSuffixWhitelistTags.value = normalizeRegistrationEmailSuffixDomains(
updated.registration_email_suffix_whitelist updated.registration_email_suffix_whitelist
) )
tablePageSizeOptionsInput.value = formatTablePageSizeOptions(
Array.isArray(updated.table_page_size_options) ? updated.table_page_size_options : [10, 20, 50]
)
registrationEmailSuffixWhitelistDraft.value = '' registrationEmailSuffixWhitelistDraft.value = ''
form.smtp_password = '' form.smtp_password = ''
smtpPasswordManuallyEdited.value = false smtpPasswordManuallyEdited.value = false
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment