Files
rclone-ui/src/components/OptionsSection.tsx
T
2025-10-06 19:35:25 +02:00

218 lines
9.1 KiB
TypeScript

import { Chip, Textarea, Tooltip } from '@heroui/react'
import { LockKeyholeIcon, LockOpenIcon, XIcon } from 'lucide-react'
import { startTransition, useEffect, useState } from 'react'
import { replaceSmartQuotes } from '../../lib/format'
export default function OptionsSection({
optionsJson,
setOptionsJson,
globalOptions,
getAvailableOptions,
isLocked,
setIsLocked,
}: {
optionsJson: string
setOptionsJson: (value: string) => void
globalOptions: any[]
getAvailableOptions: () => Promise<any>
isLocked?: boolean
setIsLocked?: (value: boolean) => void
}) {
const [availableOptions, setAvailableOptions] = useState<any[]>([])
const [options, setOptions] = useState<any>({})
const [isJsonValid, setIsJsonValid] = useState(true)
useEffect(() => {
getAvailableOptions()
.then((flags) => {
return flags
})
.then((flags) => {
startTransition(() => {
setAvailableOptions(flags)
})
})
}, [getAvailableOptions])
useEffect(() => {
startTransition(() => {
try {
const parsedOptions = JSON.parse(optionsJson)
setOptions(parsedOptions)
setIsJsonValid(true)
} catch {
setIsJsonValid(false)
}
})
}, [optionsJson])
function isOptionAdded(option: string) {
return options[option] !== undefined
}
return (
<div className="flex flex-row gap-2 h-[400px]">
<Textarea
// className="w-1/2 "
classNames={{
'base': 'w-1/2',
inputWrapper: '!h-full',
}}
label="Custom Options"
description="Tap an option to add it. Scroll to see more. Hover to see details."
value={optionsJson}
onValueChange={(value) => {
console.log(value)
//weird curly apostrophe alternatives on macos, replace to normal apostrophe
const cleanedJson = replaceSmartQuotes(value)
setOptionsJson(cleanedJson)
}}
onKeyDown={(e) => {
//if it's tab key, add 2 spaces at the current text cursor position
if (e.key === 'Tab') {
e.preventDefault()
const text = e.currentTarget.value
const cursorPosition = e.currentTarget.selectionStart
const newText =
text.slice(0, cursorPosition) + ' ' + text.slice(cursorPosition)
e.currentTarget.value = newText
e.currentTarget.selectionStart = cursorPosition + 2
e.currentTarget.selectionEnd = cursorPosition + 2
}
}}
autoCapitalize="off"
autoComplete="off"
autoCorrect="off"
spellCheck="false"
isInvalid={!isJsonValid}
errorMessage={isJsonValid ? '' : 'Invalid JSON'}
disableAutosize={true}
rows={14}
size="lg"
onClear={() => {
setOptionsJson('{}')
}}
endContent={
setIsLocked && (
<Tooltip
content="Lock to prevent changes when switching paths"
className="max-w-48"
color="foreground"
>
{isLocked ? (
<LockKeyholeIcon
className="w-3 h-3 cursor-pointer"
onClick={() => setIsLocked(false)}
/>
) : (
<LockOpenIcon
className="w-3 h-3 cursor-pointer"
onClick={() => setIsLocked(true)}
/>
)}
</Tooltip>
)
}
data-focus-visible="false"
/>
<div className="flex flex-row flex-wrap items-start content-start justify-start w-1/2 overflow-y-auto gap-x-2.5 gap-y-3 rounded-medium">
{availableOptions.map((option) => {
const alreadyAdded = isOptionAdded(option.FieldName)
return (
<Tooltip
key={option.Name}
delay={500}
content={
<div className="flex flex-col w-full gap-1 overflow-y-auto max-w-52 max-h-80 overscroll-y-none">
<p className="sticky top-0 font-mono font-bold truncate bg-foreground shrink-0">
{option.Name}
</p>
<p>
{
availableOptions.find(
(o) => o.FieldName === option.FieldName
)?.Help
}
</p>
</div>
}
closeDelay={0}
className="pt-1.5 max-w-52"
color="foreground"
>
<Chip
isDisabled={!isJsonValid}
variant={alreadyAdded ? 'flat' : 'solid'}
onClick={() => {
startTransition(() => {
if (alreadyAdded) {
const newOptions = {
...options,
}
newOptions[option.FieldName] = undefined
setOptionsJson(JSON.stringify(newOptions, null, 2))
return
}
let value: string | number | boolean = ''
const defaultGlobalValue =
globalOptions[
option.FieldName as keyof typeof globalOptions
]
if (
defaultGlobalValue !== null &&
defaultGlobalValue !== undefined
) {
value = defaultGlobalValue
} else {
value = availableOptions.find(
(o) => o.FieldName === option.FieldName
)?.DefaultStr
}
const valueType =
availableOptions.find(
(o) => o.FieldName === option.FieldName
)?.Type || 'string'
if (valueType === 'bool') {
value = Boolean(value || false)
} else if (
valueType.includes('int') ||
valueType.includes('float')
) {
value = Number(value || 0)
}
const newOptions = {
...options,
[option.FieldName]: value,
}
setOptionsJson(JSON.stringify(newOptions, null, 2))
})
}}
className="cursor-pointer"
size="sm"
endContent={
alreadyAdded ? (
<XIcon className="w-3.5 h-3.5 mr-1 stroke-1" />
) : undefined
}
>
{option.Name}
</Chip>
</Tooltip>
)
})}
</div>
</div>
)
}