285 lines
10 KiB
TypeScript
285 lines
10 KiB
TypeScript
import decamelize from 'decamelize'
|
|
import type { ForwardedRef } from 'react'
|
|
import { forwardRef, useMemo } from 'react'
|
|
import toast from 'react-hot-toast'
|
|
import { useLocation, useNavigate } from 'react-router-dom'
|
|
import { Fragment } from 'react/jsx-runtime'
|
|
|
|
import { ActionButton } from '@src/components/ActionButton'
|
|
import { SettingsFieldInput } from '@src/components/Settings/SettingsFieldInput'
|
|
import { SettingsSection } from '@src/components/Settings/SettingsSection'
|
|
import { getSettingsFolderPaths } from '@src/lib/desktopFS'
|
|
import { isDesktop } from '@src/lib/isDesktop'
|
|
import { openExternalBrowserIfDesktop } from '@src/lib/openWindow'
|
|
import { onboardingStartPath } from '@src/lib/onboardingPaths'
|
|
import { PATHS } from '@src/lib/paths'
|
|
import type { Setting } from '@src/lib/settings/initialSettings'
|
|
import type {
|
|
SetEventTypes,
|
|
SettingsLevel,
|
|
} from '@src/lib/settings/settingsTypes'
|
|
import {
|
|
shouldHideSetting,
|
|
shouldShowSettingInput,
|
|
} from '@src/lib/settings/settingsUtils'
|
|
import { reportRejection } from '@src/lib/trap'
|
|
import { toSync } from '@src/lib/utils'
|
|
import {
|
|
codeManager,
|
|
kclManager,
|
|
settingsActor,
|
|
useSettings,
|
|
} from '@src/lib/singletons'
|
|
import { APP_VERSION, IS_STAGING, getReleaseUrl } from '@src/routes/utils'
|
|
import {
|
|
acceptOnboarding,
|
|
catchOnboardingWarnError,
|
|
} from '@src/routes/Onboarding/utils'
|
|
|
|
interface AllSettingsFieldsProps {
|
|
searchParamTab: SettingsLevel
|
|
isFileSettings: boolean
|
|
}
|
|
|
|
export const AllSettingsFields = forwardRef(
|
|
(
|
|
{ searchParamTab, isFileSettings }: AllSettingsFieldsProps,
|
|
scrollRef: ForwardedRef<HTMLDivElement>
|
|
) => {
|
|
const location = useLocation()
|
|
const navigate = useNavigate()
|
|
const context = useSettings()
|
|
|
|
const projectPath = useMemo(() => {
|
|
const filteredPathname = location.pathname
|
|
.replace(PATHS.FILE, '')
|
|
.replace(PATHS.SETTINGS, '')
|
|
const lastSlashIndex = filteredPathname.lastIndexOf(
|
|
// This is slicing off any remaining browser path segments,
|
|
// so we don't use window.electron.sep here
|
|
'/'
|
|
)
|
|
const projectPath =
|
|
isFileSettings && isDesktop()
|
|
? decodeURIComponent(filteredPathname.slice(lastSlashIndex + 1))
|
|
: undefined
|
|
|
|
return projectPath
|
|
}, [location.pathname, isFileSettings])
|
|
|
|
async function restartOnboarding() {
|
|
const props = {
|
|
onboardingStatus: onboardingStartPath,
|
|
navigate,
|
|
codeManager,
|
|
kclManager,
|
|
}
|
|
// We need to navigate out of settings before accepting onboarding
|
|
// in the web
|
|
if (!isDesktop()) {
|
|
navigate('..')
|
|
}
|
|
acceptOnboarding(props).catch((reason) =>
|
|
catchOnboardingWarnError(reason, props)
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="relative overflow-y-auto">
|
|
<div ref={scrollRef} className="flex flex-col gap-4 px-2">
|
|
{Object.entries(context)
|
|
.filter(([_, categorySettings]) =>
|
|
// Filter out categories that don't have any non-hidden settings
|
|
Object.values(categorySettings).some(
|
|
(setting) => !shouldHideSetting(setting, searchParamTab)
|
|
)
|
|
)
|
|
.map(([category, categorySettings]) => (
|
|
<Fragment key={category}>
|
|
<h2
|
|
id={`category-${category}`}
|
|
className="text-xl mt-6 first-of-type:mt-0 capitalize font-bold"
|
|
>
|
|
{decamelize(category, { separator: ' ' })}
|
|
</h2>
|
|
{Object.entries(categorySettings)
|
|
.filter(
|
|
// Filter out settings that don't have a Component or inputType
|
|
// or are hidden on the current level or the current platform
|
|
(item: [string, Setting<unknown>]) =>
|
|
shouldShowSettingInput(item[1], searchParamTab)
|
|
)
|
|
.map(([settingName, s]) => {
|
|
const setting = s as Setting
|
|
const parentValue =
|
|
setting[setting.getParentLevel(searchParamTab)]
|
|
return (
|
|
<SettingsSection
|
|
title={decamelize(settingName, {
|
|
separator: ' ',
|
|
})}
|
|
id={settingName}
|
|
className={
|
|
location.hash === `#${settingName}`
|
|
? 'bg-primary/5 dark:bg-chalkboard-90'
|
|
: ''
|
|
}
|
|
key={`${category}-${settingName}-${searchParamTab}`}
|
|
description={setting.description}
|
|
settingHasChanged={
|
|
setting[searchParamTab] !== undefined &&
|
|
setting[searchParamTab] !==
|
|
setting.getFallback(searchParamTab)
|
|
}
|
|
parentLevel={setting.getParentLevel(searchParamTab)}
|
|
onFallback={() =>
|
|
settingsActor.send({
|
|
type: `set.${category}.${settingName}`,
|
|
data: {
|
|
level: searchParamTab,
|
|
value:
|
|
parentValue !== undefined
|
|
? parentValue
|
|
: setting.getFallback(searchParamTab),
|
|
},
|
|
} as SetEventTypes)
|
|
}
|
|
>
|
|
<SettingsFieldInput
|
|
category={category}
|
|
settingName={settingName}
|
|
settingsLevel={searchParamTab}
|
|
setting={setting}
|
|
/>
|
|
</SettingsSection>
|
|
)
|
|
})}
|
|
</Fragment>
|
|
))}
|
|
<h2 id="settings-resets" className="text-2xl mt-6 font-bold">
|
|
Resets
|
|
</h2>
|
|
<SettingsSection
|
|
title="Onboarding"
|
|
description="Replay the onboarding process"
|
|
>
|
|
<ActionButton
|
|
Element="button"
|
|
onClick={() => {
|
|
restartOnboarding().catch(reportRejection)
|
|
}}
|
|
iconStart={{
|
|
icon: 'refresh',
|
|
size: 'sm',
|
|
className: 'p-1',
|
|
}}
|
|
>
|
|
Replay Onboarding
|
|
</ActionButton>
|
|
</SettingsSection>
|
|
<SettingsSection
|
|
title="Reset settings"
|
|
description={`Restore settings to their default values. Your settings are saved in
|
|
${
|
|
isDesktop()
|
|
? ' a file in the app data folder for your OS.'
|
|
: " your browser's local storage."
|
|
}
|
|
`}
|
|
>
|
|
<div className="flex flex-col items-start gap-4">
|
|
{isDesktop() && (
|
|
<ActionButton
|
|
Element="button"
|
|
onClick={toSync(async () => {
|
|
const paths = await getSettingsFolderPaths(projectPath)
|
|
const finalPath = paths[searchParamTab]
|
|
if (!finalPath) {
|
|
return new Error('finalPath undefined')
|
|
}
|
|
window.electron.showInFolder(finalPath)
|
|
}, reportRejection)}
|
|
iconStart={{
|
|
icon: 'folder',
|
|
size: 'sm',
|
|
className: 'p-1',
|
|
}}
|
|
>
|
|
Show in folder
|
|
</ActionButton>
|
|
)}
|
|
<ActionButton
|
|
Element="button"
|
|
onClick={() => {
|
|
settingsActor.send({
|
|
type: 'Reset settings',
|
|
level: searchParamTab,
|
|
})
|
|
toast.success(
|
|
`Your ${searchParamTab}-level settings were reset`
|
|
)
|
|
}}
|
|
iconStart={{
|
|
icon: 'refresh',
|
|
size: 'sm',
|
|
className: 'p-1 text-chalkboard-10',
|
|
bgClassName: 'bg-destroy-70',
|
|
}}
|
|
>
|
|
Reset {searchParamTab}-level settings
|
|
</ActionButton>
|
|
</div>
|
|
</SettingsSection>
|
|
<h2 id="settings-about" className="text-2xl mt-6 font-bold">
|
|
About Design Studio
|
|
</h2>
|
|
<div className="text-sm mb-12">
|
|
<p>
|
|
{/* This uses a Vite plugin, set in vite.config.ts
|
|
to inject the version from package.json */}
|
|
App version {APP_VERSION}.{' '}
|
|
</p>
|
|
<div className="flex gap-2 flex-wrap my-4">
|
|
<ActionButton
|
|
Element="externalLink"
|
|
to={getReleaseUrl()}
|
|
iconStart={{ icon: 'file', className: 'p-1' }}
|
|
>
|
|
View release on GitHub
|
|
</ActionButton>
|
|
<ActionButton
|
|
Element="button"
|
|
onClick={() => {
|
|
window.electron.appCheckForUpdates().catch(reportRejection)
|
|
}}
|
|
iconStart={{
|
|
icon: 'refresh',
|
|
size: 'sm',
|
|
className: 'p-1',
|
|
}}
|
|
>
|
|
Check for updates
|
|
</ActionButton>
|
|
</div>
|
|
<p className="max-w-2xl mt-6">
|
|
Don't see the feature you want? Check to see if it's on{' '}
|
|
<a
|
|
onClick={openExternalBrowserIfDesktop(
|
|
'https://github.com/KittyCAD/modeling-app/discussions'
|
|
)}
|
|
href="https://github.com/KittyCAD/modeling-app/discussions"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
our roadmap
|
|
</a>
|
|
, and start a discussion if you don't see it! Your feedback will
|
|
help us prioritize what to build next.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
)
|