2024-06-04 13:56:20 -04:00
|
|
|
import decamelize from 'decamelize'
|
|
|
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
|
|
|
import { Setting } from 'lib/settings/initialSettings'
|
|
|
|
import { SetEventTypes, SettingsLevel } from 'lib/settings/settingsTypes'
|
|
|
|
import {
|
|
|
|
shouldHideSetting,
|
|
|
|
shouldShowSettingInput,
|
|
|
|
} from 'lib/settings/settingsUtils'
|
|
|
|
import { Fragment } from 'react/jsx-runtime'
|
|
|
|
import { SettingsSection } from './SettingsSection'
|
|
|
|
import { useLocation, useNavigate } from 'react-router-dom'
|
|
|
|
import { isTauri } from 'lib/isTauri'
|
|
|
|
import { ActionButton } from 'components/ActionButton'
|
|
|
|
import { SettingsFieldInput } from './SettingsFieldInput'
|
|
|
|
import { getInitialDefaultDir, showInFolder } from 'lib/tauri'
|
|
|
|
import toast from 'react-hot-toast'
|
|
|
|
import { APP_VERSION } from 'routes/Settings'
|
|
|
|
import { createAndOpenNewProject, getSettingsFolderPaths } from 'lib/tauriFS'
|
2024-08-09 02:47:25 -04:00
|
|
|
import { PATHS } from 'lib/paths'
|
2024-06-04 13:56:20 -04:00
|
|
|
import { useDotDotSlash } from 'hooks/useDotDotSlash'
|
|
|
|
import { sep } from '@tauri-apps/api/path'
|
2024-08-02 15:38:39 -04:00
|
|
|
import { ForwardedRef, forwardRef, useEffect } from 'react'
|
|
|
|
import { useLspContext } from 'components/LspProvider'
|
2024-06-04 13:56:20 -04:00
|
|
|
|
|
|
|
interface AllSettingsFieldsProps {
|
|
|
|
searchParamTab: SettingsLevel
|
|
|
|
isFileSettings: boolean
|
|
|
|
}
|
|
|
|
|
|
|
|
export const AllSettingsFields = forwardRef(
|
|
|
|
(
|
|
|
|
{ searchParamTab, isFileSettings }: AllSettingsFieldsProps,
|
|
|
|
scrollRef: ForwardedRef<HTMLDivElement>
|
|
|
|
) => {
|
|
|
|
const location = useLocation()
|
|
|
|
const navigate = useNavigate()
|
2024-08-02 15:38:39 -04:00
|
|
|
const { onProjectOpen } = useLspContext()
|
2024-06-04 13:56:20 -04:00
|
|
|
const dotDotSlash = useDotDotSlash()
|
|
|
|
const {
|
2024-08-02 15:38:39 -04:00
|
|
|
settings: { send, context, state },
|
2024-06-04 13:56:20 -04:00
|
|
|
} = useSettingsAuthContext()
|
|
|
|
|
|
|
|
const projectPath =
|
|
|
|
isFileSettings && isTauri()
|
|
|
|
? decodeURI(
|
|
|
|
location.pathname
|
2024-08-09 02:47:25 -04:00
|
|
|
.replace(PATHS.FILE + '/', '')
|
|
|
|
.replace(PATHS.SETTINGS, '')
|
2024-06-04 13:56:20 -04:00
|
|
|
.slice(0, decodeURI(location.pathname).lastIndexOf(sep()))
|
|
|
|
)
|
|
|
|
: undefined
|
|
|
|
|
2024-08-02 15:38:39 -04:00
|
|
|
async function restartOnboarding() {
|
2024-06-04 13:56:20 -04:00
|
|
|
send({
|
|
|
|
type: `set.app.onboardingStatus`,
|
|
|
|
data: { level: 'user', value: '' },
|
|
|
|
})
|
2024-08-02 15:38:39 -04:00
|
|
|
}
|
2024-06-04 13:56:20 -04:00
|
|
|
|
2024-08-02 15:38:39 -04:00
|
|
|
/**
|
|
|
|
* A "listener" for the XState to return to "idle" state
|
|
|
|
* when the user resets the onboarding, using the callback above
|
|
|
|
*/
|
|
|
|
useEffect(() => {
|
|
|
|
async function navigateToOnboardingStart() {
|
|
|
|
if (
|
|
|
|
state.context.app.onboardingStatus.user === '' &&
|
|
|
|
state.matches('idle')
|
|
|
|
) {
|
|
|
|
if (isFileSettings) {
|
|
|
|
// If we're in a project, first navigate to the onboarding start here
|
|
|
|
// so we can trigger the warning screen if necessary
|
2024-08-09 02:47:25 -04:00
|
|
|
navigate(dotDotSlash(1) + PATHS.ONBOARDING.INDEX)
|
2024-08-02 15:38:39 -04:00
|
|
|
} else {
|
|
|
|
// If we're in the global settings, create a new project and navigate
|
|
|
|
// to the onboarding start in that project
|
|
|
|
await createAndOpenNewProject({ onProjectOpen, navigate })
|
|
|
|
}
|
|
|
|
}
|
2024-06-04 13:56:20 -04:00
|
|
|
}
|
2024-08-02 15:38:39 -04:00
|
|
|
navigateToOnboardingStart()
|
|
|
|
}, [isFileSettings, navigate, state])
|
2024-06-04 13:56:20 -04:00
|
|
|
|
|
|
|
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={() =>
|
|
|
|
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}
|
|
|
|
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
|
|
|
|
${
|
|
|
|
isTauri()
|
|
|
|
? ' a file in the app data folder for your OS.'
|
|
|
|
: " your browser's local storage."
|
|
|
|
}
|
|
|
|
`}
|
|
|
|
>
|
|
|
|
<div className="flex flex-col items-start gap-4">
|
|
|
|
{isTauri() && (
|
|
|
|
<ActionButton
|
|
|
|
Element="button"
|
|
|
|
onClick={async () => {
|
|
|
|
const paths = await getSettingsFolderPaths(
|
|
|
|
projectPath ? decodeURIComponent(projectPath) : undefined
|
|
|
|
)
|
|
|
|
showInFolder(paths[searchParamTab])
|
|
|
|
}}
|
|
|
|
iconStart={{
|
|
|
|
icon: 'folder',
|
|
|
|
size: 'sm',
|
|
|
|
className: 'p-1',
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
Show in folder
|
|
|
|
</ActionButton>
|
|
|
|
)}
|
|
|
|
<ActionButton
|
|
|
|
Element="button"
|
|
|
|
onClick={async () => {
|
|
|
|
const defaultDirectory = await getInitialDefaultDir()
|
|
|
|
send({
|
|
|
|
type: 'Reset settings',
|
|
|
|
defaultDirectory,
|
|
|
|
})
|
|
|
|
toast.success('Settings restored to default')
|
|
|
|
}}
|
|
|
|
iconStart={{
|
|
|
|
icon: 'refresh',
|
|
|
|
size: 'sm',
|
|
|
|
className: 'p-1 text-chalkboard-10',
|
|
|
|
bgClassName: 'bg-destroy-70',
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
Restore default settings
|
|
|
|
</ActionButton>
|
|
|
|
</div>
|
|
|
|
</SettingsSection>
|
|
|
|
<h2 id="settings-about" className="text-2xl mt-6 font-bold">
|
|
|
|
About Modeling App
|
|
|
|
</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}.{' '}
|
|
|
|
<a
|
|
|
|
href={`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`}
|
|
|
|
target="_blank"
|
|
|
|
rel="noopener noreferrer"
|
|
|
|
>
|
|
|
|
View release on GitHub
|
|
|
|
</a>
|
|
|
|
</p>
|
|
|
|
<p className="max-w-2xl mt-6">
|
|
|
|
Don't see the feature you want? Check to see if it's on{' '}
|
|
|
|
<a
|
|
|
|
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>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
)
|