Compare commits

..

5 Commits

Author SHA1 Message Date
becd5d5e82 Enhance TimeDisplay Component with User Settings Integration
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
- Integrated user settings context to dynamically adjust date and time formats in the TimeDisplay component, improving customization based on user preferences.
- Refactored date formatting logic to utilize user-defined formats, ensuring accurate display of date and time based on user settings.
- Improved overall readability and maintainability of the component by streamlining the formatting logic.
2026-09-04 02:33:24 +01:00
8ff580bf0f Enhance DashboardNavigation Component with User Settings Integration
- Updated DashboardNavigation to utilize user settings for appearance and navigation label preferences, improving user experience.
- Introduced logic to dynamically set theme mode and density based on user preferences and application settings.
- Refactored useEffect to ensure settings are applied only when user settings are loaded, enhancing performance and reliability.
2026-09-04 02:33:20 +01:00
8b33a82a65 Refactor ObjectForm Component to Use Fetched Data for Editing State
- Updated ObjectForm to prefer fetched snapshot data over stale objectData when entering edit mode, ensuring accurate state management.
- Enhanced logic to compute next object data based on the latest server data, improving user experience during editing.
- Simplified state change handling by integrating base data checks, reducing potential errors during data updates.
2026-09-04 02:33:15 +01:00
9a601245b6 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.
2026-09-04 02:33:05 +01:00
da8fe8de2d Refactor FilamentSku, PartSku, and ProductSku Models for Improved Readability and Structure
- Updated lazy loading syntax for component imports to enhance code clarity.
- Reformatted content return statements in SKU components for better readability.
- Added 'overrideCost' and 'overridePrice' fields to SKU models for expanded functionality.
- Adjusted column widths for 'Override Cost' and 'Override Price' fields to improve layout consistency.
2026-09-04 01:33:11 +01:00
9 changed files with 626 additions and 311 deletions

View File

@ -1,10 +1,8 @@
import { useContext, useEffect, useMemo, useState } from 'react'
import { Descriptions, Flex, Select, Space, Spin, Typography } from 'antd'
import { LoadingOutlined, SettingOutlined } from '@ant-design/icons'
import { Flex, Space } from 'antd'
import { useThemeContext } from '../context/ThemeContext'
import { ApiServerContext } from '../context/ApiServerContext'
import { ElectronContext } from '../context/ElectronContext'
import { AuthContext } from '../context/AuthContext'
import {
normalizeAppUpdateEngine,
useAppUpdateContext
@ -12,20 +10,29 @@ import {
import { useMessageContext } from '../context/MessageContext'
import useCollapseState from '../hooks/useCollapseState'
import InfoCollapse from '../common/InfoCollapse'
import ObjectInfo from '../common/ObjectInfo'
import ViewButton from '../common/ViewButton'
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_ENGINE = 'native'
const engineLabel = (engine) => {
const normalized = normalizeAppUpdateEngine(engine)
if (normalized === 'chromium') return 'Chromium'
if (normalized === 'native') return 'Native'
return 'Not configured'
}
const LEGACY_ELECTRON_USER_KEYS = [
'theme',
'density',
'showNavigationLabels',
'dateTimeFormat',
'timezone'
]
const Settings = () => {
const {
@ -37,145 +44,183 @@ const Settings = () => {
showNavigationLabels,
setShowNavigationLabels
} = useThemeContext()
const { fetchAppUpdateBranches } = useContext(ApiServerContext)
const {
connected,
fetchAppUpdateBranches,
updateUserSettings,
userSettings,
userSettingsLoaded
} = useContext(ApiServerContext)
const { isElectron, getAppSettings, setAppSettings, getAppEngine } =
useContext(ElectronContext)
const { recheckForUpdates } = useAppUpdateContext()
const { userProfile, setUserProfile } = useContext(AuthContext)
const { showSuccess, showError } = useMessageContext()
const [collapseState, updateCollapseState] = useCollapseState('Settings', {
appearance: true,
appUpdates: true
})
const visibleSections = useMemo(
() => getVisibleSettingsSections({ isElectron }),
[isElectron]
)
const [collapseState, updateCollapseState] = useCollapseState(
'Settings',
Object.fromEntries(
settingsSchema.sections.map((section) => [section.key, true])
)
)
const [isEditing, setIsEditing] = useState(false)
const [settingsLoading, setSettingsLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [appSettings, setAppSettingsState] = useState({})
const [savedSettings, setSavedSettings] = useState({})
const [draftSettings, setDraftSettings] = useState({})
const [electronSettings, setElectronSettings] = 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(() => {
if (!userSettingsLoaded || isEditing) return
const loadSettings = async () => {
setSettingsLoading(true)
const [storedSettings, detectedEngine] = await Promise.all([
isElectron ? getAppSettings() : Promise.resolve(userProfile?.settings || {}),
const [storedElectronSettings, detectedEngine] = await Promise.all([
isElectron ? getAppSettings() : Promise.resolve({}),
isElectron ? getAppEngine() : Promise.resolve(DEFAULT_UPDATE_ENGINE)
])
const nextEngine =
normalizeAppUpdateEngine(detectedEngine) || DEFAULT_UPDATE_ENGINE
setRunningEngine(nextEngine)
const nextSettings = { ...(storedSettings || {}) }
if (isElectron && !normalizeAppUpdateEngine(nextSettings.appUpdateEngine)) {
nextSettings.appUpdateEngine = nextEngine
const nextElectronSettings = { ...(storedElectronSettings || {}) }
if (
isElectron &&
!normalizeAppUpdateEngine(nextElectronSettings.appUpdateEngine)
) {
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)
applyAppearance(nextSettings, nextElectronSettings)
setSettingsLoading(false)
}
loadSettings()
}, [getAppEngine, getAppSettings, isElectron, userProfile?.settings])
useEffect(() => {
if (settingsLoading || isEditing) return
if (appSettings.theme) setThemeMode(appSettings.theme)
if (appSettings.density) setDensityMode(appSettings.density)
if (appSettings.showNavigationLabels !== undefined) {
setShowNavigationLabels(appSettings.showNavigationLabels)
}
// eslint-disable-next-line react-hooks/exhaustive-deps -- reload when stored appearance/app settings change
}, [
appSettings.density,
appSettings.showNavigationLabels,
appSettings.theme,
getAppEngine,
getAppSettings,
isEditing,
setDensityMode,
setShowNavigationLabels,
setThemeMode,
settingsLoading
isElectron,
userAppearance.dateTimeFormat,
userAppearance.density,
userAppearance.showNavigationLabels,
userAppearance.theme,
userAppearance.timezone,
userSettingsLoaded
])
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 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 = () => {
setDraftSettings({
...appSettings,
appUpdateBranch:
currentBranch === 'Not configured' ? undefined : currentBranch,
appUpdateEngine: currentEngine,
theme: currentThemeValue,
density: currentDensityValue,
showNavigationLabels: currentShowNavigationLabels
})
setDraftSettings(resolvedSettings)
setIsEditing(true)
}
const cancelEditing = () => {
setDraftSettings(appSettings)
setDraftSettings(savedSettings)
setIsEditing(false)
}
@ -183,55 +228,67 @@ const Settings = () => {
setSaving(true)
try {
const useAppTheme = isElectron && draftSettings.theme === 'app'
const nextEngine =
normalizeAppUpdateEngine(draftSettings.appUpdateEngine) ||
currentEngine
DEFAULT_UPDATE_ENGINE
const nextSettings = {
...appSettings,
theme: draftSettings.theme,
density: draftSettings.density,
...getSettingsDefaults({ isElectron }),
...savedSettings,
...draftSettings,
showNavigationLabels: Boolean(draftSettings.showNavigationLabels),
...(isElectron
? {
appShowNavigationLabels: Boolean(
draftSettings.appShowNavigationLabels
),
appUpdateBranch: draftSettings.appUpdateBranch,
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.')
return
}
if (!isElectron) {
setUserProfile((previous) => ({
...previous,
settings: {
...(previous?.settings || {}),
theme: draftSettings.theme,
density: draftSettings.density,
showNavigationLabels: Boolean(draftSettings.showNavigationLabels)
let nextElectronSettings = electronSettings
if (isElectron) {
nextElectronSettings = { ...electronSettings }
for (const key of LEGACY_ELECTRON_USER_KEYS) {
delete nextElectronSettings[key]
}
}))
nextElectronSettings = {
...nextElectronSettings,
...pickSettings(nextSettings, APP_SETTING_NAMES),
overrideTheme: useAppTheme
}
setThemeMode(draftSettings.theme)
setDensityMode(draftSettings.density)
setShowNavigationLabels(draftSettings.showNavigationLabels)
setAppSettingsState(nextSettings)
const saved = await setAppSettings(nextElectronSettings)
if (!saved) {
showError('Unable to save settings.')
return
}
}
applyAppearance(nextSettings, nextElectronSettings)
setElectronSettings(nextElectronSettings)
setSavedSettings(nextSettings)
setDraftSettings(nextSettings)
setIsEditing(false)
showSuccess('Settings saved.')
const appUpdateSettingsChanged =
isElectron &&
(nextSettings.appUpdateBranch !== appSettings.appUpdateBranch ||
(nextSettings.appUpdateBranch !== savedSettings.appUpdateBranch ||
nextSettings.appUpdateEngine !==
normalizeAppUpdateEngine(appSettings.appUpdateEngine))
normalizeAppUpdateEngine(savedSettings.appUpdateEngine))
if (appUpdateSettingsChanged) {
void recheckForUpdates()
@ -247,7 +304,10 @@ const Settings = () => {
<Space size='small'>
<ViewButton
disabled={settingsLoading}
items={viewItems}
items={visibleSections.map((section) => ({
key: section.key,
label: section.name
}))}
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
@ -257,163 +317,51 @@ const Settings = () => {
handleUpdate={handleSave}
cancelEditing={cancelEditing}
startEditing={startEditing}
formValid={
Boolean(draftSettings.theme && draftSettings.density) &&
(!isElectron ||
(Boolean(draftSettings.appUpdateBranch) &&
Boolean(normalizeAppUpdateEngine(draftSettings.appUpdateEngine))))
}
disabled={settingsLoading || (!isElectron && !userProfile)}
formValid={areSettingsValid(draftSettings, { isElectron })}
disabled={settingsLoading || !userSettingsLoaded}
loading={saving}
/>
</Flex>
<div style={{ height: '100%', minHeight: 0, overflowY: 'auto' }}>
<Spin spinning={settingsLoading} indicator={<LoadingOutlined />}>
<ScrollBox>
<Flex vertical gap='large'>
{visibleSections.map((section) => {
const Icon = section.icon
return (
<InfoCollapse
title='Appearance Settings'
icon={<SettingOutlined />}
active={collapseState.appearance}
key={section.key}
title={section.name}
icon={<Icon />}
active={collapseState[section.key]}
onToggle={(expanded) =>
updateCollapseState('appearance', expanded)
updateCollapseState(section.key, expanded)
}
collapseKey='appearance'
collapseKey={section.key}
>
<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) =>
<ObjectInfo
loading={settingsLoading}
isEditing={isEditing}
propertyDefinitions={section.properties}
objectData={resolvedSettings}
parentData={{ branches, isElectron }}
onPropertyChange={
isEditing
? (name, value) =>
setDraftSettings((previous) => ({
...previous,
theme: value
[name]:
value?.target && typeof value.target === 'object'
? value.target.value
: value
}))
: undefined
}
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>
{...(section.column ? { column: section.column } : {})}
/>
</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.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>
)}
)
})}
</Flex>
</Spin>
</div>
</ScrollBox>
</Flex>
)
}

View File

@ -49,15 +49,22 @@ import {
import { useAppUpdateContext } from '../context/AppUpdateContext'
import { useThemeContext } from '../context/ThemeContext'
import { getEffectiveAppearance } from '../../../database/Settings'
const { Text } = Typography
const DashboardNavigation = () => {
const { userProfile } = useContext(AuthContext)
const { showSpotlight } = useContext(SpotlightContext)
const { connecting, connected } = useContext(ApiServerContext)
const { connecting, connected, userSettings, userSettingsLoaded } =
useContext(ApiServerContext)
const { authenticated } = useContext(AuthContext)
const { showNavigationLabels, setShowNavigationLabels } = useThemeContext()
const {
showNavigationLabels,
setShowNavigationLabels,
setThemeMode,
setDensityMode
} = useThemeContext()
const { toggleNotificationCenter, unreadCount } =
useContext(NotificationContext)
const [apiServerState, setApiServerState] = useState('disconnected')
@ -81,21 +88,31 @@ const DashboardNavigation = () => {
const { availableUpdate, checkForUpdates } = useAppUpdateContext()
useEffect(() => {
const hydrateNavigationLabels = async () => {
const settings = isElectron
? await getAppSettings()
: userProfile?.settings || {}
if (settings?.showNavigationLabels !== undefined) {
setShowNavigationLabels(settings.showNavigationLabels)
if (!userSettingsLoaded) return
const hydrateAppearance = async () => {
const electronSettings = isElectron ? await getAppSettings() : {}
const effective = getEffectiveAppearance({
isElectron,
userAppearance: userSettings?.appearance || {},
electronSettings
})
if (effective.theme) setThemeMode(effective.theme)
if (effective.density) setDensityMode(effective.density)
if (effective.showNavigationLabels !== undefined) {
setShowNavigationLabels(effective.showNavigationLabels)
}
}
void hydrateNavigationLabels()
void hydrateAppearance()
}, [
getAppSettings,
isElectron,
setDensityMode,
setShowNavigationLabels,
userProfile?.settings
setThemeMode,
userSettings?.appearance,
userSettingsLoaded
])
const includeDev = import.meta.env.DEV

View File

@ -770,9 +770,13 @@ const ObjectForm = forwardRef(
isEditingRef.current = true
setIsEditing(true)
const computedEntries = calculateComputedValues(objectData, model)
// Prefer the fetched snapshot over a stale objectData closure
// (?action=edit can start before fetch finishes).
const baseData = serverObjectData.current
if (baseData) {
const computedEntries = calculateComputedValues(baseData, model)
const nextObjectData = {
...applyComputedEntries(objectData, computedEntries),
...applyComputedEntries(baseData, computedEntries),
_isEditing: true
}
setObjectData(nextObjectData)
@ -781,6 +785,9 @@ const ObjectForm = forwardRef(
objectData: nextObjectData,
editDisabled: getEditDisabled(model, nextObjectData, userProfile)
})
} else {
onStateChangeRef.current({ isEditing: true })
}
} catch (err) {
console.error(err)
showError(

View File

@ -1,8 +1,12 @@
// PrinterSelect.js
import { useState, useEffect } from 'react'
import { useContext, useState, useEffect } from 'react'
import PropTypes from 'prop-types'
import { Flex, Typography, Tag } from 'antd'
import dayjs from 'dayjs'
import { ApiServerContext } from '../context/ApiServerContext'
import {
DEFAULT_DATE_TIME_FORMAT,
splitDateTimeFormat
} from '../../../database/Settings'
const { Text } = Typography
@ -80,7 +84,11 @@ const TimeDisplay = ({
showSince = false,
type = 'primary'
}) => {
const { userSettings } = useContext(ApiServerContext) || {}
const [timeAgo, setTimeAgo] = useState(formatTimeDifference(dateTime))
const dateTimeFormat =
userSettings?.appearance?.dateTimeFormat || DEFAULT_DATE_TIME_FORMAT
const { dateFormat, timeFormat } = splitDateTimeFormat(dateTimeFormat)
useEffect(() => {
if (showSince) {
@ -96,15 +104,18 @@ const TimeDisplay = ({
return <Text type='secondary'>n/a</Text>
}
var dateFormat = ''
if (showDate == true) {
dateFormat += 'YYYY-MM-DD '
}
if (showTime == true) {
dateFormat += 'HH:mm:ss '
let displayFormat = ''
if (showDate && showTime) {
displayFormat = dateTimeFormat
} else if (showDate) {
displayFormat = dateFormat
} else if (showTime) {
displayFormat = timeFormat
}
const formattedDate = dayjs(dateTime).format(dateFormat)
const formattedDate = displayFormat
? dayjs(dateTime).format(displayFormat)
: ''
return (
<Flex align={'center'} gap={'small'} wrap>

View File

@ -38,7 +38,8 @@ const createEmptyUserSettings = () => ({
sortSidebarVisibility: {},
columnVisibility: {},
collapseState: {},
pageLayout: {}
pageLayout: {},
appearance: {}
})
const normalizeUserSettingsCategory = (category) =>
@ -76,7 +77,8 @@ const normalizeUserSettings = (settings = {}) => ({
),
columnVisibility: normalizeUserSettingsCategory(settings?.columnVisibility),
collapseState: normalizeUserSettingsCategory(settings?.collapseState),
pageLayout: normalizeUserSettingsCategory(settings?.pageLayout)
pageLayout: normalizeUserSettingsCategory(settings?.pageLayout),
appearance: normalizeUserSettingsCategory(settings?.appearance)
})
const emitWithAcknowledgement = (socket, eventName, data) =>

306
src/database/Settings.js Normal file
View 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

View File

@ -1,10 +1,12 @@
import { createElement, lazy } from 'react'
const FilamentSkuInfo = lazy(
() => import('../../components/Dashboard/Management/FilamentSkus/FilamentSkuInfo')
() =>
import('../../components/Dashboard/Management/FilamentSkus/FilamentSkuInfo')
)
const NewFilamentSku = lazy(
() => import('../../components/Dashboard/Management/FilamentSkus/NewFilamentSku')
() =>
import('../../components/Dashboard/Management/FilamentSkus/NewFilamentSku')
)
const DeleteObject = lazy(
() => import('../../components/Dashboard/common/DeleteObject')
@ -34,7 +36,11 @@ export const FilamentSku = {
label: 'New Filament SKU',
icon: PlusIcon,
content: (objectData, { onOk } = {}) => {
return createElement(NewFilamentSku, { defaultValues: objectData, onOk, reset: true })
return createElement(NewFilamentSku, {
defaultValues: objectData,
onOk,
reset: true
})
}
},
{
@ -123,6 +129,7 @@ export const FilamentSku = {
'name',
'color',
'cost',
'overrideCost',
'costWithTax',
'createdAt',
'updatedAt',
@ -135,6 +142,7 @@ export const FilamentSku = {
'color',
'cost',
'costWithTax',
'overrideCost',
'createdAt',
'updatedAt'
],
@ -216,7 +224,7 @@ export const FilamentSku = {
label: 'Override Cost',
required: true,
type: 'bool',
columnWidth: 150
columnWidth: 162
},
{
name: 'cost',

View File

@ -34,7 +34,11 @@ export const PartSku = {
label: 'New Part SKU',
icon: PlusIcon,
content: (objectData, { onOk } = {}) => {
return createElement(NewPartSku, { defaultValues: objectData, onOk, reset: true })
return createElement(NewPartSku, {
defaultValues: objectData,
onOk,
reset: true
})
}
},
{
@ -126,6 +130,8 @@ export const PartSku = {
'cost',
'costWithTax',
'price',
'overridePrice',
'overrideCost',
'priceWithTax',
'margin',
'createdAt',
@ -137,8 +143,10 @@ export const PartSku = {
'part',
'name',
'cost',
'overrideCost',
'costWithTax',
'price',
'overridePrice',
'priceWithTax',
'margin',
'createdAt',
@ -214,7 +222,7 @@ export const PartSku = {
label: 'Override Cost',
required: true,
type: 'bool',
columnWidth: 150
columnWidth: 162
},
{
@ -295,7 +303,7 @@ export const PartSku = {
: objectData?.part?.priceMode
},
type: 'priceMode',
columnWidth: 150
columnWidth: 165
},
{
name: 'price',

View File

@ -1,10 +1,12 @@
import { createElement, lazy } from 'react'
const ProductSkuInfo = lazy(
() => import('../../components/Dashboard/Management/ProductSkus/ProductSkuInfo')
() =>
import('../../components/Dashboard/Management/ProductSkus/ProductSkuInfo')
)
const NewProductSku = lazy(
() => import('../../components/Dashboard/Management/ProductSkus/NewProductSku')
() =>
import('../../components/Dashboard/Management/ProductSkus/NewProductSku')
)
const DeleteObject = lazy(
() => import('../../components/Dashboard/common/DeleteObject')
@ -34,7 +36,11 @@ export const ProductSku = {
label: 'New Product SKU',
icon: PlusIcon,
content: (objectData, { onOk } = {}) => {
return createElement(NewProductSku, { defaultValues: objectData, onOk, reset: true })
return createElement(NewProductSku, {
defaultValues: objectData,
onOk,
reset: true
})
}
},
{
@ -126,6 +132,8 @@ export const ProductSku = {
'cost',
'costWithTax',
'price',
'overridePrice',
'overrideCost',
'priceWithTax',
'margin',
'createdAt',
@ -140,6 +148,8 @@ export const ProductSku = {
'costWithTax',
'price',
'priceWithTax',
'overridePrice',
'overrideCost',
'margin',
'createdAt',
'updatedAt'
@ -215,7 +225,7 @@ export const ProductSku = {
label: 'Override Cost',
required: true,
type: 'bool',
columnWidth: 150
columnWidth: 162
},
{
name: 'cost',
@ -229,9 +239,7 @@ export const ProductSku = {
return objectData?.overrideCost
},
value: (objectData) =>
objectData?.overrideCost
? objectData?.cost
: objectData?.product?.cost,
objectData?.overrideCost ? objectData?.cost : objectData?.product?.cost,
columnWidth: 100
},
{
@ -282,7 +290,7 @@ export const ProductSku = {
label: 'Override Price',
required: true,
type: 'bool',
columnWidth: 150
columnWidth: 165
},
{
name: 'priceMode',