Tom Butcher 78dc567a8f
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
Implemented software update installation.
2026-06-21 15:18:04 +01:00

311 lines
9.8 KiB
JavaScript

import { useContext, useEffect, useMemo, useState } from 'react'
import { Descriptions, Flex, Select, Space, Spin, Typography } from 'antd'
import { LoadingOutlined, SettingOutlined } from '@ant-design/icons'
import { useThemeContext } from '../context/ThemeContext'
import { ApiServerContext } from '../context/ApiServerContext'
import { ElectronContext } from '../context/ElectronContext'
import { AuthContext } from '../context/AuthContext'
import { useMessageContext } from '../context/MessageContext'
import useCollapseState from '../hooks/useCollapseState'
import InfoCollapse from '../common/InfoCollapse'
import ViewButton from '../common/ViewButton'
import EditButtons from '../common/EditButtons'
const { Text } = Typography
const { Option } = Select
const DEFAULT_UPDATE_BRANCH = 'main'
const Settings = () => {
const { isDarkMode, isCompact, isSystem, setThemeMode, setDensityMode } =
useThemeContext()
const { fetchAppUpdateBranches } = useContext(ApiServerContext)
const { isElectron, getAppSettings, setAppSettings } =
useContext(ElectronContext)
const { userProfile, setUserProfile } = useContext(AuthContext)
const { showSuccess, showError } = useMessageContext()
const [collapseState, updateCollapseState] = useCollapseState('Settings', {
appearance: true,
appUpdates: true
})
const [isEditing, setIsEditing] = useState(false)
const [settingsLoading, setSettingsLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [appSettings, setAppSettingsState] = useState({})
const [draftSettings, setDraftSettings] = useState({})
const [branches, setBranches] = useState([])
const [branchLoading, setBranchLoading] = useState(false)
useEffect(() => {
const loadSettings = async () => {
setSettingsLoading(true)
const storedSettings = isElectron
? await getAppSettings()
: userProfile?.settings || {}
setAppSettingsState(storedSettings || {})
setDraftSettings(storedSettings || {})
setSettingsLoading(false)
}
loadSettings()
}, [getAppSettings, isElectron, userProfile?.settings])
useEffect(() => {
if (settingsLoading || isEditing) return
if (appSettings.theme) setThemeMode(appSettings.theme)
if (appSettings.density) setDensityMode(appSettings.density)
}, [
appSettings.density,
appSettings.theme,
isEditing,
setDensityMode,
setThemeMode,
settingsLoading
])
useEffect(() => {
if (!isElectron) {
setBranches([])
setBranchLoading(false)
return
}
const loadBranches = async () => {
setBranchLoading(true)
const availableBranches = await fetchAppUpdateBranches()
setBranches(availableBranches)
setDraftSettings((previous) => {
if (previous.appUpdateBranch) return previous
const defaultBranch = availableBranches.includes(DEFAULT_UPDATE_BRANCH)
? DEFAULT_UPDATE_BRANCH
: availableBranches[0]
return defaultBranch
? { ...previous, appUpdateBranch: defaultBranch }
: previous
})
setBranchLoading(false)
}
loadBranches()
}, [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 currentBranch =
appSettings.appUpdateBranch ||
(branches.includes(DEFAULT_UPDATE_BRANCH) ? DEFAULT_UPDATE_BRANCH : null) ||
branches[0] ||
'Not configured'
const startEditing = () => {
setDraftSettings({
...appSettings,
appUpdateBranch:
currentBranch === 'Not configured' ? undefined : currentBranch,
theme: currentThemeValue,
density: currentDensityValue
})
setIsEditing(true)
}
const cancelEditing = () => {
setDraftSettings(appSettings)
setIsEditing(false)
}
const handleSave = async () => {
setSaving(true)
try {
const nextSettings = {
...appSettings,
theme: draftSettings.theme,
density: draftSettings.density,
...(isElectron
? { appUpdateBranch: draftSettings.appUpdateBranch }
: {})
}
const saved = isElectron
? await setAppSettings(nextSettings)
: Boolean(userProfile)
if (!saved) {
showError('Unable to save settings.')
return
}
if (!isElectron) {
setUserProfile((previous) => ({
...previous,
settings: {
...(previous?.settings || {}),
theme: draftSettings.theme,
density: draftSettings.density
}
}))
}
setThemeMode(draftSettings.theme)
setDensityMode(draftSettings.density)
setAppSettingsState(nextSettings)
setDraftSettings(nextSettings)
setIsEditing(false)
showSuccess('Settings saved.')
} finally {
setSaving(false)
}
}
return (
<Flex vertical gap='large' style={{ height: '100%', minHeight: 0 }}>
<Flex justify='space-between' align='center'>
<Space size='small'>
<ViewButton
disabled={settingsLoading}
items={viewItems}
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
</Space>
<EditButtons
isEditing={isEditing}
handleUpdate={handleSave}
cancelEditing={cancelEditing}
startEditing={startEditing}
formValid={
Boolean(draftSettings.theme && draftSettings.density) &&
(!isElectron || Boolean(draftSettings.appUpdateBranch))
}
disabled={settingsLoading || (!isElectron && !userProfile)}
loading={saving}
/>
</Flex>
<div style={{ height: '100%', minHeight: 0, overflowY: 'auto' }}>
<Spin spinning={settingsLoading} indicator={<LoadingOutlined />}>
<Flex vertical gap='large'>
<InfoCollapse
title='Appearance Settings'
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>
</InfoCollapse>
{isElectron && (
<InfoCollapse
title='App Update Settings'
icon={<SettingOutlined />}
active={collapseState.appUpdates}
onToggle={(expanded) =>
updateCollapseState('appUpdates', expanded)
}
collapseKey='appUpdates'
>
<Descriptions bordered column={1}>
<Descriptions.Item label='Branch'>
{isEditing ? (
<Select
value={draftSettings.appUpdateBranch}
onChange={(value) =>
setDraftSettings((previous) => ({
...previous,
appUpdateBranch: value
}))
}
style={{ width: '100%' }}
loading={branchLoading}
placeholder='Select a branch'
>
{branchOptions}
</Select>
) : (
<Text>{currentBranch}</Text>
)}
</Descriptions.Item>
</Descriptions>
</InfoCollapse>
)}
</Flex>
</Spin>
</div>
</Flex>
)
}
export default Settings