Refactor Settings Component and Introduce New Settings Schema
- Removed unused imports and streamlined component structure for better readability. - Added a new settings schema to manage user and app settings, enhancing configurability. - Updated state management for settings, including draft and saved settings handling. - Improved appearance application logic to dynamically adjust theme and density based on user preferences. - Introduced utility functions for validating and picking settings, ensuring robust settings management.
This commit is contained in:
parent
da8fe8de2d
commit
9a601245b6
@ -1,10 +1,8 @@
|
|||||||
import { useContext, useEffect, useMemo, useState } from 'react'
|
import { useContext, useEffect, useMemo, useState } from 'react'
|
||||||
import { Descriptions, Flex, Select, Space, Spin, Typography } from 'antd'
|
import { Flex, Space } from 'antd'
|
||||||
import { LoadingOutlined, SettingOutlined } from '@ant-design/icons'
|
|
||||||
import { useThemeContext } from '../context/ThemeContext'
|
import { useThemeContext } from '../context/ThemeContext'
|
||||||
import { ApiServerContext } from '../context/ApiServerContext'
|
import { ApiServerContext } from '../context/ApiServerContext'
|
||||||
import { ElectronContext } from '../context/ElectronContext'
|
import { ElectronContext } from '../context/ElectronContext'
|
||||||
import { AuthContext } from '../context/AuthContext'
|
|
||||||
import {
|
import {
|
||||||
normalizeAppUpdateEngine,
|
normalizeAppUpdateEngine,
|
||||||
useAppUpdateContext
|
useAppUpdateContext
|
||||||
@ -12,20 +10,29 @@ import {
|
|||||||
import { useMessageContext } from '../context/MessageContext'
|
import { useMessageContext } from '../context/MessageContext'
|
||||||
import useCollapseState from '../hooks/useCollapseState'
|
import useCollapseState from '../hooks/useCollapseState'
|
||||||
import InfoCollapse from '../common/InfoCollapse'
|
import InfoCollapse from '../common/InfoCollapse'
|
||||||
|
import ObjectInfo from '../common/ObjectInfo'
|
||||||
import ViewButton from '../common/ViewButton'
|
import ViewButton from '../common/ViewButton'
|
||||||
import EditButtons from '../common/EditButtons'
|
import EditButtons from '../common/EditButtons'
|
||||||
|
import ScrollBox from '../common/ScrollBox'
|
||||||
|
import settingsSchema, {
|
||||||
|
APP_SETTING_NAMES,
|
||||||
|
USER_SETTING_NAMES,
|
||||||
|
areSettingsValid,
|
||||||
|
getEffectiveAppearance,
|
||||||
|
getSettingsDefaults,
|
||||||
|
getVisibleSettingsSections,
|
||||||
|
pickSettings
|
||||||
|
} from '../../../database/Settings'
|
||||||
|
|
||||||
const { Text } = Typography
|
|
||||||
const { Option } = Select
|
|
||||||
const DEFAULT_UPDATE_BRANCH = 'main'
|
const DEFAULT_UPDATE_BRANCH = 'main'
|
||||||
const DEFAULT_UPDATE_ENGINE = 'native'
|
const DEFAULT_UPDATE_ENGINE = 'native'
|
||||||
|
const LEGACY_ELECTRON_USER_KEYS = [
|
||||||
const engineLabel = (engine) => {
|
'theme',
|
||||||
const normalized = normalizeAppUpdateEngine(engine)
|
'density',
|
||||||
if (normalized === 'chromium') return 'Chromium'
|
'showNavigationLabels',
|
||||||
if (normalized === 'native') return 'Native'
|
'dateTimeFormat',
|
||||||
return 'Not configured'
|
'timezone'
|
||||||
}
|
]
|
||||||
|
|
||||||
const Settings = () => {
|
const Settings = () => {
|
||||||
const {
|
const {
|
||||||
@ -37,145 +44,183 @@ const Settings = () => {
|
|||||||
showNavigationLabels,
|
showNavigationLabels,
|
||||||
setShowNavigationLabels
|
setShowNavigationLabels
|
||||||
} = useThemeContext()
|
} = useThemeContext()
|
||||||
const { fetchAppUpdateBranches } = useContext(ApiServerContext)
|
const {
|
||||||
|
connected,
|
||||||
|
fetchAppUpdateBranches,
|
||||||
|
updateUserSettings,
|
||||||
|
userSettings,
|
||||||
|
userSettingsLoaded
|
||||||
|
} = useContext(ApiServerContext)
|
||||||
const { isElectron, getAppSettings, setAppSettings, getAppEngine } =
|
const { isElectron, getAppSettings, setAppSettings, getAppEngine } =
|
||||||
useContext(ElectronContext)
|
useContext(ElectronContext)
|
||||||
const { recheckForUpdates } = useAppUpdateContext()
|
const { recheckForUpdates } = useAppUpdateContext()
|
||||||
const { userProfile, setUserProfile } = useContext(AuthContext)
|
|
||||||
const { showSuccess, showError } = useMessageContext()
|
const { showSuccess, showError } = useMessageContext()
|
||||||
const [collapseState, updateCollapseState] = useCollapseState('Settings', {
|
const visibleSections = useMemo(
|
||||||
appearance: true,
|
() => getVisibleSettingsSections({ isElectron }),
|
||||||
appUpdates: true
|
[isElectron]
|
||||||
})
|
)
|
||||||
|
const [collapseState, updateCollapseState] = useCollapseState(
|
||||||
|
'Settings',
|
||||||
|
Object.fromEntries(
|
||||||
|
settingsSchema.sections.map((section) => [section.key, true])
|
||||||
|
)
|
||||||
|
)
|
||||||
const [isEditing, setIsEditing] = useState(false)
|
const [isEditing, setIsEditing] = useState(false)
|
||||||
const [settingsLoading, setSettingsLoading] = useState(true)
|
const [settingsLoading, setSettingsLoading] = useState(true)
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
const [appSettings, setAppSettingsState] = useState({})
|
const [savedSettings, setSavedSettings] = useState({})
|
||||||
const [draftSettings, setDraftSettings] = useState({})
|
const [draftSettings, setDraftSettings] = useState({})
|
||||||
|
const [electronSettings, setElectronSettings] = useState({})
|
||||||
const [branches, setBranches] = useState([])
|
const [branches, setBranches] = useState([])
|
||||||
const [branchLoading, setBranchLoading] = useState(false)
|
|
||||||
const [runningEngine, setRunningEngine] = useState(DEFAULT_UPDATE_ENGINE)
|
const currentThemeValue = isSystem ? 'system' : isDarkMode ? 'dark' : 'light'
|
||||||
|
const currentDensityValue = isCompact ? 'compact' : 'comfortable'
|
||||||
|
const userAppearance = userSettings?.appearance || {}
|
||||||
|
|
||||||
|
const resolvedSettings = useMemo(() => {
|
||||||
|
const defaults = getSettingsDefaults({ isElectron })
|
||||||
|
const stored = isEditing ? draftSettings : savedSettings
|
||||||
|
const defaultBranch = branches.includes(DEFAULT_UPDATE_BRANCH)
|
||||||
|
? DEFAULT_UPDATE_BRANCH
|
||||||
|
: branches[0]
|
||||||
|
|
||||||
|
return {
|
||||||
|
...defaults,
|
||||||
|
theme: currentThemeValue,
|
||||||
|
density: currentDensityValue,
|
||||||
|
showNavigationLabels: showNavigationLabels ?? false,
|
||||||
|
...(isElectron
|
||||||
|
? {
|
||||||
|
appTheme: stored.appTheme || currentThemeValue,
|
||||||
|
appShowNavigationLabels:
|
||||||
|
stored.appShowNavigationLabels ?? showNavigationLabels ?? false,
|
||||||
|
appUpdateBranch: defaultBranch,
|
||||||
|
appUpdateEngine:
|
||||||
|
normalizeAppUpdateEngine(stored.appUpdateEngine) ||
|
||||||
|
DEFAULT_UPDATE_ENGINE
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
...stored
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
branches,
|
||||||
|
currentDensityValue,
|
||||||
|
currentThemeValue,
|
||||||
|
draftSettings,
|
||||||
|
isEditing,
|
||||||
|
isElectron,
|
||||||
|
savedSettings,
|
||||||
|
showNavigationLabels
|
||||||
|
])
|
||||||
|
|
||||||
|
const applyAppearance = (nextSettings, nextElectronSettings = electronSettings) => {
|
||||||
|
const effective = getEffectiveAppearance({
|
||||||
|
isElectron,
|
||||||
|
userAppearance: nextSettings,
|
||||||
|
electronSettings: {
|
||||||
|
...nextElectronSettings,
|
||||||
|
appTheme: nextSettings.appTheme,
|
||||||
|
appShowNavigationLabels: nextSettings.appShowNavigationLabels,
|
||||||
|
overrideTheme: nextSettings.theme === 'app'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (effective.theme) setThemeMode(effective.theme)
|
||||||
|
if (effective.density) setDensityMode(effective.density)
|
||||||
|
if (effective.showNavigationLabels !== undefined) {
|
||||||
|
setShowNavigationLabels(effective.showNavigationLabels)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!userSettingsLoaded || isEditing) return
|
||||||
|
|
||||||
const loadSettings = async () => {
|
const loadSettings = async () => {
|
||||||
setSettingsLoading(true)
|
setSettingsLoading(true)
|
||||||
const [storedSettings, detectedEngine] = await Promise.all([
|
const [storedElectronSettings, detectedEngine] = await Promise.all([
|
||||||
isElectron ? getAppSettings() : Promise.resolve(userProfile?.settings || {}),
|
isElectron ? getAppSettings() : Promise.resolve({}),
|
||||||
isElectron ? getAppEngine() : Promise.resolve(DEFAULT_UPDATE_ENGINE)
|
isElectron ? getAppEngine() : Promise.resolve(DEFAULT_UPDATE_ENGINE)
|
||||||
])
|
])
|
||||||
const nextEngine =
|
const nextEngine =
|
||||||
normalizeAppUpdateEngine(detectedEngine) || DEFAULT_UPDATE_ENGINE
|
normalizeAppUpdateEngine(detectedEngine) || DEFAULT_UPDATE_ENGINE
|
||||||
setRunningEngine(nextEngine)
|
const nextElectronSettings = { ...(storedElectronSettings || {}) }
|
||||||
|
if (
|
||||||
const nextSettings = { ...(storedSettings || {}) }
|
isElectron &&
|
||||||
if (isElectron && !normalizeAppUpdateEngine(nextSettings.appUpdateEngine)) {
|
!normalizeAppUpdateEngine(nextElectronSettings.appUpdateEngine)
|
||||||
nextSettings.appUpdateEngine = nextEngine
|
) {
|
||||||
|
nextElectronSettings.appUpdateEngine = nextEngine
|
||||||
}
|
}
|
||||||
|
|
||||||
setAppSettingsState(nextSettings)
|
const nextSettings = {
|
||||||
|
...getSettingsDefaults({ isElectron }),
|
||||||
|
...pickSettings(userAppearance, USER_SETTING_NAMES),
|
||||||
|
...(isElectron
|
||||||
|
? {
|
||||||
|
...pickSettings(nextElectronSettings, APP_SETTING_NAMES),
|
||||||
|
appTheme:
|
||||||
|
nextElectronSettings.appTheme || nextElectronSettings.theme,
|
||||||
|
appShowNavigationLabels:
|
||||||
|
nextElectronSettings.appShowNavigationLabels ??
|
||||||
|
nextElectronSettings.showNavigationLabels,
|
||||||
|
theme: nextElectronSettings.overrideTheme
|
||||||
|
? 'app'
|
||||||
|
: userAppearance.theme
|
||||||
|
}
|
||||||
|
: {})
|
||||||
|
}
|
||||||
|
|
||||||
|
setElectronSettings(nextElectronSettings)
|
||||||
|
setSavedSettings(nextSettings)
|
||||||
setDraftSettings(nextSettings)
|
setDraftSettings(nextSettings)
|
||||||
|
applyAppearance(nextSettings, nextElectronSettings)
|
||||||
setSettingsLoading(false)
|
setSettingsLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
loadSettings()
|
loadSettings()
|
||||||
}, [getAppEngine, getAppSettings, isElectron, userProfile?.settings])
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- reload when stored appearance/app settings change
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (settingsLoading || isEditing) return
|
|
||||||
if (appSettings.theme) setThemeMode(appSettings.theme)
|
|
||||||
if (appSettings.density) setDensityMode(appSettings.density)
|
|
||||||
if (appSettings.showNavigationLabels !== undefined) {
|
|
||||||
setShowNavigationLabels(appSettings.showNavigationLabels)
|
|
||||||
}
|
|
||||||
}, [
|
}, [
|
||||||
appSettings.density,
|
getAppEngine,
|
||||||
appSettings.showNavigationLabels,
|
getAppSettings,
|
||||||
appSettings.theme,
|
|
||||||
isEditing,
|
isEditing,
|
||||||
setDensityMode,
|
isElectron,
|
||||||
setShowNavigationLabels,
|
userAppearance.dateTimeFormat,
|
||||||
setThemeMode,
|
userAppearance.density,
|
||||||
settingsLoading
|
userAppearance.showNavigationLabels,
|
||||||
|
userAppearance.theme,
|
||||||
|
userAppearance.timezone,
|
||||||
|
userSettingsLoaded
|
||||||
])
|
])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isElectron) {
|
if (!isElectron) {
|
||||||
setBranches([])
|
setBranches([])
|
||||||
setBranchLoading(false)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadBranches = async () => {
|
const loadBranches = async () => {
|
||||||
setBranchLoading(true)
|
|
||||||
const availableBranches = await fetchAppUpdateBranches()
|
const availableBranches = await fetchAppUpdateBranches()
|
||||||
setBranches(availableBranches)
|
setBranches(availableBranches)
|
||||||
|
|
||||||
setDraftSettings((previous) => {
|
setDraftSettings((previous) => {
|
||||||
if (previous.appUpdateBranch) return previous
|
if (previous.appUpdateBranch) return previous
|
||||||
|
|
||||||
const defaultBranch = availableBranches.includes(DEFAULT_UPDATE_BRANCH)
|
const defaultBranch = availableBranches.includes(DEFAULT_UPDATE_BRANCH)
|
||||||
? DEFAULT_UPDATE_BRANCH
|
? DEFAULT_UPDATE_BRANCH
|
||||||
: availableBranches[0]
|
: availableBranches[0]
|
||||||
|
|
||||||
return defaultBranch
|
return defaultBranch
|
||||||
? { ...previous, appUpdateBranch: defaultBranch }
|
? { ...previous, appUpdateBranch: defaultBranch }
|
||||||
: previous
|
: previous
|
||||||
})
|
})
|
||||||
|
|
||||||
setBranchLoading(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
loadBranches()
|
loadBranches()
|
||||||
}, [fetchAppUpdateBranches, isElectron])
|
}, [fetchAppUpdateBranches, isElectron])
|
||||||
|
|
||||||
const branchOptions = useMemo(
|
|
||||||
() =>
|
|
||||||
branches.map((branch) => (
|
|
||||||
<Option key={branch} value={branch}>
|
|
||||||
{branch}
|
|
||||||
</Option>
|
|
||||||
)),
|
|
||||||
[branches]
|
|
||||||
)
|
|
||||||
const viewItems = [
|
|
||||||
{ key: 'appearance', label: 'Appearance Settings' },
|
|
||||||
...(isElectron ? [{ key: 'appUpdates', label: 'App Update Settings' }] : [])
|
|
||||||
]
|
|
||||||
|
|
||||||
const getCurrentThemeValue = () => {
|
|
||||||
if (isSystem) return 'system'
|
|
||||||
return isDarkMode ? 'dark' : 'light'
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentThemeValue = getCurrentThemeValue()
|
|
||||||
const currentDensityValue = isCompact ? 'compact' : 'comfortable'
|
|
||||||
const currentShowNavigationLabels =
|
|
||||||
appSettings.showNavigationLabels ?? showNavigationLabels ?? false
|
|
||||||
const currentBranch =
|
|
||||||
appSettings.appUpdateBranch ||
|
|
||||||
(branches.includes(DEFAULT_UPDATE_BRANCH) ? DEFAULT_UPDATE_BRANCH : null) ||
|
|
||||||
branches[0] ||
|
|
||||||
'Not configured'
|
|
||||||
const currentEngine =
|
|
||||||
normalizeAppUpdateEngine(appSettings.appUpdateEngine) ||
|
|
||||||
normalizeAppUpdateEngine(runningEngine) ||
|
|
||||||
DEFAULT_UPDATE_ENGINE
|
|
||||||
|
|
||||||
const startEditing = () => {
|
const startEditing = () => {
|
||||||
setDraftSettings({
|
setDraftSettings(resolvedSettings)
|
||||||
...appSettings,
|
|
||||||
appUpdateBranch:
|
|
||||||
currentBranch === 'Not configured' ? undefined : currentBranch,
|
|
||||||
appUpdateEngine: currentEngine,
|
|
||||||
theme: currentThemeValue,
|
|
||||||
density: currentDensityValue,
|
|
||||||
showNavigationLabels: currentShowNavigationLabels
|
|
||||||
})
|
|
||||||
setIsEditing(true)
|
setIsEditing(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const cancelEditing = () => {
|
const cancelEditing = () => {
|
||||||
setDraftSettings(appSettings)
|
setDraftSettings(savedSettings)
|
||||||
setIsEditing(false)
|
setIsEditing(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,55 +228,67 @@ const Settings = () => {
|
|||||||
setSaving(true)
|
setSaving(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const useAppTheme = isElectron && draftSettings.theme === 'app'
|
||||||
const nextEngine =
|
const nextEngine =
|
||||||
normalizeAppUpdateEngine(draftSettings.appUpdateEngine) ||
|
normalizeAppUpdateEngine(draftSettings.appUpdateEngine) ||
|
||||||
currentEngine
|
DEFAULT_UPDATE_ENGINE
|
||||||
const nextSettings = {
|
const nextSettings = {
|
||||||
...appSettings,
|
...getSettingsDefaults({ isElectron }),
|
||||||
theme: draftSettings.theme,
|
...savedSettings,
|
||||||
density: draftSettings.density,
|
...draftSettings,
|
||||||
showNavigationLabels: Boolean(draftSettings.showNavigationLabels),
|
showNavigationLabels: Boolean(draftSettings.showNavigationLabels),
|
||||||
...(isElectron
|
...(isElectron
|
||||||
? {
|
? {
|
||||||
|
appShowNavigationLabels: Boolean(
|
||||||
|
draftSettings.appShowNavigationLabels
|
||||||
|
),
|
||||||
appUpdateBranch: draftSettings.appUpdateBranch,
|
appUpdateBranch: draftSettings.appUpdateBranch,
|
||||||
appUpdateEngine: nextEngine
|
appUpdateEngine: nextEngine
|
||||||
}
|
}
|
||||||
: {})
|
: {})
|
||||||
}
|
}
|
||||||
const saved = isElectron
|
|
||||||
? await setAppSettings(nextSettings)
|
|
||||||
: Boolean(userProfile)
|
|
||||||
|
|
||||||
if (!saved) {
|
const userUpdates = USER_SETTING_NAMES.filter(
|
||||||
|
(name) => !(name === 'theme' && useAppTheme)
|
||||||
|
).map((name) => updateUserSettings('appearance', name, nextSettings[name]))
|
||||||
|
|
||||||
|
const userResults = await Promise.all(userUpdates)
|
||||||
|
if (connected && userResults.some((result) => result == null)) {
|
||||||
showError('Unable to save settings.')
|
showError('Unable to save settings.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isElectron) {
|
let nextElectronSettings = electronSettings
|
||||||
setUserProfile((previous) => ({
|
if (isElectron) {
|
||||||
...previous,
|
nextElectronSettings = { ...electronSettings }
|
||||||
settings: {
|
for (const key of LEGACY_ELECTRON_USER_KEYS) {
|
||||||
...(previous?.settings || {}),
|
delete nextElectronSettings[key]
|
||||||
theme: draftSettings.theme,
|
}
|
||||||
density: draftSettings.density,
|
nextElectronSettings = {
|
||||||
showNavigationLabels: Boolean(draftSettings.showNavigationLabels)
|
...nextElectronSettings,
|
||||||
}
|
...pickSettings(nextSettings, APP_SETTING_NAMES),
|
||||||
}))
|
overrideTheme: useAppTheme
|
||||||
|
}
|
||||||
|
|
||||||
|
const saved = await setAppSettings(nextElectronSettings)
|
||||||
|
if (!saved) {
|
||||||
|
showError('Unable to save settings.')
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setThemeMode(draftSettings.theme)
|
applyAppearance(nextSettings, nextElectronSettings)
|
||||||
setDensityMode(draftSettings.density)
|
setElectronSettings(nextElectronSettings)
|
||||||
setShowNavigationLabels(draftSettings.showNavigationLabels)
|
setSavedSettings(nextSettings)
|
||||||
setAppSettingsState(nextSettings)
|
|
||||||
setDraftSettings(nextSettings)
|
setDraftSettings(nextSettings)
|
||||||
setIsEditing(false)
|
setIsEditing(false)
|
||||||
showSuccess('Settings saved.')
|
showSuccess('Settings saved.')
|
||||||
|
|
||||||
const appUpdateSettingsChanged =
|
const appUpdateSettingsChanged =
|
||||||
isElectron &&
|
isElectron &&
|
||||||
(nextSettings.appUpdateBranch !== appSettings.appUpdateBranch ||
|
(nextSettings.appUpdateBranch !== savedSettings.appUpdateBranch ||
|
||||||
nextSettings.appUpdateEngine !==
|
nextSettings.appUpdateEngine !==
|
||||||
normalizeAppUpdateEngine(appSettings.appUpdateEngine))
|
normalizeAppUpdateEngine(savedSettings.appUpdateEngine))
|
||||||
|
|
||||||
if (appUpdateSettingsChanged) {
|
if (appUpdateSettingsChanged) {
|
||||||
void recheckForUpdates()
|
void recheckForUpdates()
|
||||||
@ -247,7 +304,10 @@ const Settings = () => {
|
|||||||
<Space size='small'>
|
<Space size='small'>
|
||||||
<ViewButton
|
<ViewButton
|
||||||
disabled={settingsLoading}
|
disabled={settingsLoading}
|
||||||
items={viewItems}
|
items={visibleSections.map((section) => ({
|
||||||
|
key: section.key,
|
||||||
|
label: section.name
|
||||||
|
}))}
|
||||||
visibleState={collapseState}
|
visibleState={collapseState}
|
||||||
updateVisibleState={updateCollapseState}
|
updateVisibleState={updateCollapseState}
|
||||||
/>
|
/>
|
||||||
@ -257,163 +317,51 @@ const Settings = () => {
|
|||||||
handleUpdate={handleSave}
|
handleUpdate={handleSave}
|
||||||
cancelEditing={cancelEditing}
|
cancelEditing={cancelEditing}
|
||||||
startEditing={startEditing}
|
startEditing={startEditing}
|
||||||
formValid={
|
formValid={areSettingsValid(draftSettings, { isElectron })}
|
||||||
Boolean(draftSettings.theme && draftSettings.density) &&
|
disabled={settingsLoading || !userSettingsLoaded}
|
||||||
(!isElectron ||
|
|
||||||
(Boolean(draftSettings.appUpdateBranch) &&
|
|
||||||
Boolean(normalizeAppUpdateEngine(draftSettings.appUpdateEngine))))
|
|
||||||
}
|
|
||||||
disabled={settingsLoading || (!isElectron && !userProfile)}
|
|
||||||
loading={saving}
|
loading={saving}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
<div style={{ height: '100%', minHeight: 0, overflowY: 'auto' }}>
|
<ScrollBox>
|
||||||
<Spin spinning={settingsLoading} indicator={<LoadingOutlined />}>
|
<Flex vertical gap='large'>
|
||||||
<Flex vertical gap='large'>
|
{visibleSections.map((section) => {
|
||||||
<InfoCollapse
|
const Icon = section.icon
|
||||||
title='Appearance Settings'
|
return (
|
||||||
icon={<SettingOutlined />}
|
|
||||||
active={collapseState.appearance}
|
|
||||||
onToggle={(expanded) =>
|
|
||||||
updateCollapseState('appearance', expanded)
|
|
||||||
}
|
|
||||||
collapseKey='appearance'
|
|
||||||
>
|
|
||||||
<Descriptions
|
|
||||||
bordered
|
|
||||||
column={{
|
|
||||||
xs: 1,
|
|
||||||
sm: 1,
|
|
||||||
md: 1,
|
|
||||||
lg: 2,
|
|
||||||
xl: 2,
|
|
||||||
xxl: 2
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Descriptions.Item label='Theme'>
|
|
||||||
{isEditing ? (
|
|
||||||
<Select
|
|
||||||
value={draftSettings.theme}
|
|
||||||
onChange={(value) =>
|
|
||||||
setDraftSettings((previous) => ({
|
|
||||||
...previous,
|
|
||||||
theme: value
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
>
|
|
||||||
<Option value='light'>Light</Option>
|
|
||||||
<Option value='dark'>Dark</Option>
|
|
||||||
<Option value='system'>System</Option>
|
|
||||||
</Select>
|
|
||||||
) : (
|
|
||||||
<Text>{currentThemeValue}</Text>
|
|
||||||
)}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label='UI Density'>
|
|
||||||
{isEditing ? (
|
|
||||||
<Select
|
|
||||||
value={draftSettings.density}
|
|
||||||
onChange={(value) =>
|
|
||||||
setDraftSettings((previous) => ({
|
|
||||||
...previous,
|
|
||||||
density: value
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
>
|
|
||||||
<Option value='comfortable'>Comfortable</Option>
|
|
||||||
<Option value='compact'>Compact</Option>
|
|
||||||
</Select>
|
|
||||||
) : (
|
|
||||||
<Text>{isCompact ? 'Compact' : 'Comfortable'}</Text>
|
|
||||||
)}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label='Navigation Labels'>
|
|
||||||
{isEditing ? (
|
|
||||||
<Select
|
|
||||||
value={
|
|
||||||
draftSettings.showNavigationLabels ? 'show' : 'hide'
|
|
||||||
}
|
|
||||||
onChange={(value) =>
|
|
||||||
setDraftSettings((previous) => ({
|
|
||||||
...previous,
|
|
||||||
showNavigationLabels: value === 'show'
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
>
|
|
||||||
<Option value='show'>Show</Option>
|
|
||||||
<Option value='hide'>Hide</Option>
|
|
||||||
</Select>
|
|
||||||
) : (
|
|
||||||
<Text>
|
|
||||||
{currentShowNavigationLabels ? 'Show' : 'Hide'}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Descriptions.Item>
|
|
||||||
</Descriptions>
|
|
||||||
</InfoCollapse>
|
|
||||||
{isElectron && (
|
|
||||||
<InfoCollapse
|
<InfoCollapse
|
||||||
title='App Update Settings'
|
key={section.key}
|
||||||
icon={<SettingOutlined />}
|
title={section.name}
|
||||||
active={collapseState.appUpdates}
|
icon={<Icon />}
|
||||||
|
active={collapseState[section.key]}
|
||||||
onToggle={(expanded) =>
|
onToggle={(expanded) =>
|
||||||
updateCollapseState('appUpdates', expanded)
|
updateCollapseState(section.key, expanded)
|
||||||
}
|
}
|
||||||
collapseKey='appUpdates'
|
collapseKey={section.key}
|
||||||
>
|
>
|
||||||
<Descriptions bordered column={1}>
|
<ObjectInfo
|
||||||
<Descriptions.Item label='Branch'>
|
loading={settingsLoading}
|
||||||
{isEditing ? (
|
isEditing={isEditing}
|
||||||
<Select
|
propertyDefinitions={section.properties}
|
||||||
value={draftSettings.appUpdateBranch}
|
objectData={resolvedSettings}
|
||||||
onChange={(value) =>
|
parentData={{ branches, isElectron }}
|
||||||
|
onPropertyChange={
|
||||||
|
isEditing
|
||||||
|
? (name, value) =>
|
||||||
setDraftSettings((previous) => ({
|
setDraftSettings((previous) => ({
|
||||||
...previous,
|
...previous,
|
||||||
appUpdateBranch: value
|
[name]:
|
||||||
|
value?.target && typeof value.target === 'object'
|
||||||
|
? value.target.value
|
||||||
|
: value
|
||||||
}))
|
}))
|
||||||
}
|
: undefined
|
||||||
style={{ width: '100%' }}
|
}
|
||||||
loading={branchLoading}
|
{...(section.column ? { column: section.column } : {})}
|
||||||
placeholder='Select a branch'
|
/>
|
||||||
>
|
|
||||||
{branchOptions}
|
|
||||||
</Select>
|
|
||||||
) : (
|
|
||||||
<Text>{currentBranch}</Text>
|
|
||||||
)}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label='Engine'>
|
|
||||||
{isEditing ? (
|
|
||||||
<Select
|
|
||||||
value={
|
|
||||||
normalizeAppUpdateEngine(draftSettings.appUpdateEngine) ||
|
|
||||||
currentEngine
|
|
||||||
}
|
|
||||||
onChange={(value) =>
|
|
||||||
setDraftSettings((previous) => ({
|
|
||||||
...previous,
|
|
||||||
appUpdateEngine: value
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
placeholder='Select an engine'
|
|
||||||
>
|
|
||||||
<Option value='native'>Native</Option>
|
|
||||||
<Option value='chromium'>Chromium</Option>
|
|
||||||
</Select>
|
|
||||||
) : (
|
|
||||||
<Text>{engineLabel(currentEngine)}</Text>
|
|
||||||
)}
|
|
||||||
</Descriptions.Item>
|
|
||||||
</Descriptions>
|
|
||||||
</InfoCollapse>
|
</InfoCollapse>
|
||||||
)}
|
)
|
||||||
</Flex>
|
})}
|
||||||
</Spin>
|
</Flex>
|
||||||
</div>
|
</ScrollBox>
|
||||||
</Flex>
|
</Flex>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,7 +38,8 @@ const createEmptyUserSettings = () => ({
|
|||||||
sortSidebarVisibility: {},
|
sortSidebarVisibility: {},
|
||||||
columnVisibility: {},
|
columnVisibility: {},
|
||||||
collapseState: {},
|
collapseState: {},
|
||||||
pageLayout: {}
|
pageLayout: {},
|
||||||
|
appearance: {}
|
||||||
})
|
})
|
||||||
|
|
||||||
const normalizeUserSettingsCategory = (category) =>
|
const normalizeUserSettingsCategory = (category) =>
|
||||||
@ -76,7 +77,8 @@ const normalizeUserSettings = (settings = {}) => ({
|
|||||||
),
|
),
|
||||||
columnVisibility: normalizeUserSettingsCategory(settings?.columnVisibility),
|
columnVisibility: normalizeUserSettingsCategory(settings?.columnVisibility),
|
||||||
collapseState: normalizeUserSettingsCategory(settings?.collapseState),
|
collapseState: normalizeUserSettingsCategory(settings?.collapseState),
|
||||||
pageLayout: normalizeUserSettingsCategory(settings?.pageLayout)
|
pageLayout: normalizeUserSettingsCategory(settings?.pageLayout),
|
||||||
|
appearance: normalizeUserSettingsCategory(settings?.appearance)
|
||||||
})
|
})
|
||||||
|
|
||||||
const emitWithAcknowledgement = (socket, eventName, data) =>
|
const emitWithAcknowledgement = (socket, eventName, data) =>
|
||||||
|
|||||||
306
src/database/Settings.js
Normal file
306
src/database/Settings.js
Normal file
@ -0,0 +1,306 @@
|
|||||||
|
import PersonIcon from '../components/Icons/PersonIcon'
|
||||||
|
import OpenAppIcon from '../components/Icons/OpenAppIcon'
|
||||||
|
import SoftwareUpdateIcon from '../components/Icons/SoftwareUpdateIcon'
|
||||||
|
|
||||||
|
export const DEFAULT_DATE_TIME_FORMAT = 'MM/DD/YYYY HH:mm:ss'
|
||||||
|
|
||||||
|
const DATE_FORMAT_TOKENS = [
|
||||||
|
'YYYY',
|
||||||
|
'YY',
|
||||||
|
'MMMM',
|
||||||
|
'MMM',
|
||||||
|
'MM',
|
||||||
|
'M',
|
||||||
|
'dddd',
|
||||||
|
'ddd',
|
||||||
|
'dd',
|
||||||
|
'd',
|
||||||
|
'DD',
|
||||||
|
'Do',
|
||||||
|
'D',
|
||||||
|
'WW',
|
||||||
|
'W',
|
||||||
|
'ww',
|
||||||
|
'w',
|
||||||
|
'Q'
|
||||||
|
]
|
||||||
|
const TIME_FORMAT_TOKENS = [
|
||||||
|
'HH',
|
||||||
|
'H',
|
||||||
|
'hh',
|
||||||
|
'h',
|
||||||
|
'mm',
|
||||||
|
'm',
|
||||||
|
'ss',
|
||||||
|
's',
|
||||||
|
'SSS',
|
||||||
|
'SS',
|
||||||
|
'S',
|
||||||
|
'A',
|
||||||
|
'a',
|
||||||
|
'ZZ',
|
||||||
|
'Z',
|
||||||
|
'X',
|
||||||
|
'x'
|
||||||
|
]
|
||||||
|
const FORMAT_TOKEN_PATTERN = new RegExp(
|
||||||
|
`\\[[^\\]]*\\]|${[...DATE_FORMAT_TOKENS, ...TIME_FORMAT_TOKENS]
|
||||||
|
.sort((left, right) => right.length - left.length)
|
||||||
|
.join('|')}`,
|
||||||
|
'g'
|
||||||
|
)
|
||||||
|
const DATE_FORMAT_TOKEN_SET = new Set(DATE_FORMAT_TOKENS)
|
||||||
|
const TIME_FORMAT_TOKEN_SET = new Set(TIME_FORMAT_TOKENS)
|
||||||
|
|
||||||
|
const lastPartIndex = (parts, type) => {
|
||||||
|
for (let index = parts.length - 1; index >= 0; index -= 1) {
|
||||||
|
if (parts[index].type === type) return index
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractFormatPart = (parts, type) => {
|
||||||
|
const first = parts.findIndex((part) => part.type === type)
|
||||||
|
const last = lastPartIndex(parts, type)
|
||||||
|
if (first === -1) return ''
|
||||||
|
return parts
|
||||||
|
.slice(first, last + 1)
|
||||||
|
.map((part) => part.value)
|
||||||
|
.join('')
|
||||||
|
.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const splitDateTimeFormat = (
|
||||||
|
format = DEFAULT_DATE_TIME_FORMAT
|
||||||
|
) => {
|
||||||
|
const source =
|
||||||
|
typeof format === 'string' && format.trim()
|
||||||
|
? format
|
||||||
|
: DEFAULT_DATE_TIME_FORMAT
|
||||||
|
const parts = []
|
||||||
|
let cursor = 0
|
||||||
|
|
||||||
|
for (const match of source.matchAll(FORMAT_TOKEN_PATTERN)) {
|
||||||
|
if (match.index > cursor) {
|
||||||
|
parts.push({ type: 'sep', value: source.slice(cursor, match.index) })
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = match[0]
|
||||||
|
if (token.startsWith('[')) {
|
||||||
|
parts.push({ type: 'sep', value: token })
|
||||||
|
} else if (DATE_FORMAT_TOKEN_SET.has(token)) {
|
||||||
|
parts.push({ type: 'date', value: token })
|
||||||
|
} else if (TIME_FORMAT_TOKEN_SET.has(token)) {
|
||||||
|
parts.push({ type: 'time', value: token })
|
||||||
|
} else {
|
||||||
|
parts.push({ type: 'sep', value: token })
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor = match.index + token.length
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cursor < source.length) {
|
||||||
|
parts.push({ type: 'sep', value: source.slice(cursor) })
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
dateFormat: extractFormatPart(parts, 'date') || 'MM/DD/YYYY',
|
||||||
|
timeFormat: extractFormatPart(parts, 'time') || 'HH:mm:ss'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const USER_SETTING_NAMES = [
|
||||||
|
'theme',
|
||||||
|
'density',
|
||||||
|
'showNavigationLabels',
|
||||||
|
'dateTimeFormat',
|
||||||
|
'timezone'
|
||||||
|
]
|
||||||
|
|
||||||
|
export const APP_SETTING_NAMES = [
|
||||||
|
'appTheme',
|
||||||
|
'appShowNavigationLabels',
|
||||||
|
'appUpdateBranch',
|
||||||
|
'appUpdateEngine'
|
||||||
|
]
|
||||||
|
|
||||||
|
const themeOptions = [
|
||||||
|
{ label: 'Light', value: 'light' },
|
||||||
|
{ label: 'Dark', value: 'dark' },
|
||||||
|
{ label: 'System', value: 'system' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const settings = {
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
key: 'appearance',
|
||||||
|
name: 'User Settings',
|
||||||
|
visible: true,
|
||||||
|
icon: PersonIcon,
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
name: 'theme',
|
||||||
|
label: 'Theme',
|
||||||
|
type: 'select',
|
||||||
|
required: true,
|
||||||
|
options: (_objectData, parentData) => [
|
||||||
|
...themeOptions,
|
||||||
|
...(parentData?.isElectron ? [{ label: 'App', value: 'app' }] : [])
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'density',
|
||||||
|
label: 'UI Density',
|
||||||
|
type: 'select',
|
||||||
|
required: true,
|
||||||
|
options: [
|
||||||
|
{ label: 'Comfortable', value: 'comfortable' },
|
||||||
|
{ label: 'Compact', value: 'compact' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'showNavigationLabels',
|
||||||
|
label: 'Navigation Labels',
|
||||||
|
type: 'select',
|
||||||
|
required: true,
|
||||||
|
options: [
|
||||||
|
{ label: 'Show', value: true },
|
||||||
|
{ label: 'Hide', value: false }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dateTimeFormat',
|
||||||
|
label: 'Date and Time Format',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
defaultValue: DEFAULT_DATE_TIME_FORMAT
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'timezone',
|
||||||
|
label: 'Timezone',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
defaultValue: 'UTC'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'app',
|
||||||
|
name: 'App Settings',
|
||||||
|
visible: ({ isElectron } = {}) => Boolean(isElectron),
|
||||||
|
icon: OpenAppIcon,
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
name: 'appTheme',
|
||||||
|
label: 'Theme',
|
||||||
|
type: 'select',
|
||||||
|
required: true,
|
||||||
|
defaultValue: 'system',
|
||||||
|
options: themeOptions
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'appShowNavigationLabels',
|
||||||
|
label: 'Navigation Labels',
|
||||||
|
type: 'select',
|
||||||
|
required: true,
|
||||||
|
defaultValue: false,
|
||||||
|
options: [
|
||||||
|
{ label: 'Show', value: true },
|
||||||
|
{ label: 'Hide', value: false }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'appUpdates',
|
||||||
|
name: 'App Update Settings',
|
||||||
|
visible: ({ isElectron } = {}) => Boolean(isElectron),
|
||||||
|
icon: SoftwareUpdateIcon,
|
||||||
|
column: 1,
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
name: 'appUpdateBranch',
|
||||||
|
label: 'Branch',
|
||||||
|
type: 'select',
|
||||||
|
required: true,
|
||||||
|
defaultValue: 'main',
|
||||||
|
options: (_objectData, parentData) =>
|
||||||
|
(parentData?.branches || []).map((branch) => ({
|
||||||
|
label: branch,
|
||||||
|
value: branch
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'appUpdateEngine',
|
||||||
|
label: 'Engine',
|
||||||
|
type: 'select',
|
||||||
|
required: true,
|
||||||
|
defaultValue: 'native',
|
||||||
|
options: [
|
||||||
|
{ label: 'Native', value: 'native' },
|
||||||
|
{ label: 'Chromium', value: 'chromium' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getVisibleSettingsSections = (context = {}) =>
|
||||||
|
settings.sections.filter((section) => {
|
||||||
|
if (typeof section.visible === 'function') {
|
||||||
|
return section.visible(context)
|
||||||
|
}
|
||||||
|
return section.visible !== false
|
||||||
|
})
|
||||||
|
|
||||||
|
export const getSettingsDefaults = (context = {}) => {
|
||||||
|
const defaults = {}
|
||||||
|
for (const section of getVisibleSettingsSections(context)) {
|
||||||
|
for (const property of section.properties || []) {
|
||||||
|
if (property.defaultValue !== undefined) {
|
||||||
|
defaults[property.name] = property.defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaults
|
||||||
|
}
|
||||||
|
|
||||||
|
export const areSettingsValid = (values = {}, context = {}) =>
|
||||||
|
getVisibleSettingsSections(context).every((section) =>
|
||||||
|
(section.properties || []).every((property) => {
|
||||||
|
if (!property.required) return true
|
||||||
|
const value = values[property.name]
|
||||||
|
return value !== undefined && value !== null && value !== ''
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
export const pickSettings = (values = {}, names = []) =>
|
||||||
|
names.reduce((picked, name) => {
|
||||||
|
if (values[name] !== undefined) {
|
||||||
|
picked[name] = values[name]
|
||||||
|
}
|
||||||
|
return picked
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
export const getEffectiveAppearance = ({
|
||||||
|
isElectron = false,
|
||||||
|
userAppearance = {},
|
||||||
|
electronSettings = {}
|
||||||
|
} = {}) => {
|
||||||
|
const overrideTheme = Boolean(isElectron && electronSettings.overrideTheme)
|
||||||
|
const appTheme = electronSettings.appTheme || electronSettings.theme
|
||||||
|
const appShowNavigationLabels =
|
||||||
|
electronSettings.appShowNavigationLabels ??
|
||||||
|
electronSettings.showNavigationLabels
|
||||||
|
|
||||||
|
return {
|
||||||
|
theme: overrideTheme ? appTheme : userAppearance.theme,
|
||||||
|
showNavigationLabels: isElectron
|
||||||
|
? (appShowNavigationLabels ?? userAppearance.showNavigationLabels)
|
||||||
|
: userAppearance.showNavigationLabels,
|
||||||
|
density: userAppearance.density
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default settings
|
||||||
Loading…
x
Reference in New Issue
Block a user