feat: add input actions for setting item (#2978)
Signed-off-by: James <james@jan.ai> Co-authored-by: James <james@jan.ai>
This commit is contained in:
parent
41404e1abd
commit
02478b3242
@ -16,11 +16,16 @@ export type ControllerType = 'slider' | 'checkbox' | 'input'
|
|||||||
|
|
||||||
export type InputType = 'password' | 'text' | 'email' | 'number' | 'tel' | 'url'
|
export type InputType = 'password' | 'text' | 'email' | 'number' | 'tel' | 'url'
|
||||||
|
|
||||||
|
const InputActions = ['unobscure', 'copy'] as const
|
||||||
|
export type InputActionsTuple = typeof InputActions
|
||||||
|
export type InputAction = InputActionsTuple[number]
|
||||||
|
|
||||||
export type InputComponentProps = {
|
export type InputComponentProps = {
|
||||||
placeholder: string
|
placeholder: string
|
||||||
value: string
|
value: string
|
||||||
type?: InputType
|
type?: InputType
|
||||||
textAlign?: 'left' | 'right'
|
textAlign?: 'left' | 'right'
|
||||||
|
inputActions?: InputAction[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SliderComponentProps = {
|
export type SliderComponentProps = {
|
||||||
|
|||||||
@ -6,7 +6,9 @@
|
|||||||
"controllerType": "input",
|
"controllerType": "input",
|
||||||
"controllerProps": {
|
"controllerProps": {
|
||||||
"value": "",
|
"value": "",
|
||||||
"placeholder": "hf_**********************************"
|
"placeholder": "hf_**********************************",
|
||||||
|
"type": "password",
|
||||||
|
"inputActions": ["unobscure", "copy"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@ -57,6 +57,11 @@ module.exports = {
|
|||||||
selector: 'typeLike',
|
selector: 'typeLike',
|
||||||
format: ['PascalCase'],
|
format: ['PascalCase'],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
selector: 'property',
|
||||||
|
filter: '^__html$',
|
||||||
|
format: null,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
'@next/next/no-img-element': 'off',
|
'@next/next/no-img-element': 'off',
|
||||||
|
|||||||
@ -21,3 +21,5 @@ export const proxyAtom = atomWithStorage(HTTPS_PROXY_FEATURE, '')
|
|||||||
export const ignoreSslAtom = atomWithStorage(IGNORE_SSL, false)
|
export const ignoreSslAtom = atomWithStorage(IGNORE_SSL, false)
|
||||||
export const vulkanEnabledAtom = atomWithStorage(VULKAN_ENABLED, false)
|
export const vulkanEnabledAtom = atomWithStorage(VULKAN_ENABLED, false)
|
||||||
export const quickAskEnabledAtom = atomWithStorage(QUICK_ASK_ENABLED, false)
|
export const quickAskEnabledAtom = atomWithStorage(QUICK_ASK_ENABLED, false)
|
||||||
|
|
||||||
|
export const hostAtom = atom('http://localhost:1337/')
|
||||||
|
|||||||
@ -81,7 +81,6 @@ const ExtensionItem: React.FC<Props> = ({ item }) => {
|
|||||||
<h6 className="font-semibold">Additional Dependencies</h6>
|
<h6 className="font-semibold">Additional Dependencies</h6>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
||||||
dangerouslySetInnerHTML={{ __html: description }}
|
dangerouslySetInnerHTML={{ __html: description }}
|
||||||
className="font-medium leading-relaxed text-[hsla(var(--text-secondary))]"
|
className="font-medium leading-relaxed text-[hsla(var(--text-secondary))]"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -199,7 +199,6 @@ const ExtensionCatalog = () => {
|
|||||||
<div
|
<div
|
||||||
className="w-full font-medium leading-relaxed text-[hsla(var(--text-secondary))] sm:w-4/5"
|
className="w-full font-medium leading-relaxed text-[hsla(var(--text-secondary))] sm:w-4/5"
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
||||||
__html: marked.parse(item.description ?? '', {
|
__html: marked.parse(item.description ?? '', {
|
||||||
async: false,
|
async: false,
|
||||||
}),
|
}),
|
||||||
@ -240,7 +239,6 @@ const ExtensionCatalog = () => {
|
|||||||
<div
|
<div
|
||||||
className="font-medium leading-relaxed text-[hsla(var(--text-secondary))]"
|
className="font-medium leading-relaxed text-[hsla(var(--text-secondary))]"
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
||||||
__html: marked.parse(item.description ?? '', {
|
__html: marked.parse(item.description ?? '', {
|
||||||
async: false,
|
async: false,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -1,5 +1,13 @@
|
|||||||
import { InputComponentProps, SettingComponentProps } from '@janhq/core'
|
import { useCallback, useState, Fragment } from 'react'
|
||||||
|
|
||||||
|
import {
|
||||||
|
InputAction,
|
||||||
|
InputComponentProps,
|
||||||
|
SettingComponentProps,
|
||||||
|
} from '@janhq/core'
|
||||||
|
|
||||||
import { Input } from '@janhq/joi'
|
import { Input } from '@janhq/joi'
|
||||||
|
import { CopyIcon, EyeIcon, FolderOpenIcon } from 'lucide-react'
|
||||||
import { Marked, Renderer } from 'marked'
|
import { Marked, Renderer } from 'marked'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -9,14 +17,13 @@ type Props = {
|
|||||||
|
|
||||||
const marked: Marked = new Marked({
|
const marked: Marked = new Marked({
|
||||||
renderer: {
|
renderer: {
|
||||||
link: (href, title, text) => {
|
link: (href, title, text) =>
|
||||||
return Renderer.prototype.link
|
Renderer.prototype.link
|
||||||
?.apply(this, [href, title, text])
|
?.apply(this, [href, title, text])
|
||||||
.replace(
|
.replace(
|
||||||
'<a',
|
'<a',
|
||||||
"<a class='text-[hsla(var(--app-link))]' target='_blank'"
|
"<a class='text-[hsla(var(--app-link))]' target='_blank'"
|
||||||
)
|
),
|
||||||
},
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -24,36 +31,105 @@ const SettingDetailTextInputItem = ({
|
|||||||
settingProps,
|
settingProps,
|
||||||
onValueChanged,
|
onValueChanged,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { value, type, placeholder, textAlign } =
|
const { value, type, placeholder, textAlign, inputActions } =
|
||||||
settingProps.controllerProps as InputComponentProps
|
settingProps.controllerProps as InputComponentProps
|
||||||
|
const [obscure, setObscure] = useState(type === 'password')
|
||||||
|
|
||||||
const description = marked.parse(settingProps.description ?? '', {
|
const description = marked.parse(settingProps.description ?? '', {
|
||||||
async: false,
|
async: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const toggleObscure = useCallback(() => {
|
||||||
|
setObscure((prev) => !prev)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const copy = useCallback(() => {
|
||||||
|
navigator.clipboard.writeText(value)
|
||||||
|
}, [value])
|
||||||
|
|
||||||
|
const onAction = useCallback(
|
||||||
|
(action: InputAction) => {
|
||||||
|
switch (action) {
|
||||||
|
case 'copy':
|
||||||
|
copy()
|
||||||
|
break
|
||||||
|
case 'unobscure':
|
||||||
|
toggleObscure()
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[toggleObscure, copy]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col justify-between gap-4 py-6 sm:flex-row">
|
<div className="flex w-full flex-col justify-between gap-4 py-6 sm:flex-row">
|
||||||
<div className="flex flex-1 flex-col space-y-1">
|
<div className="flex flex-1 flex-col space-y-1">
|
||||||
<h1 className="font-semibold">{settingProps.title}</h1>
|
<h1 className="font-semibold">{settingProps.title}</h1>
|
||||||
{
|
<div
|
||||||
<div
|
dangerouslySetInnerHTML={{ __html: description }}
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
className="font-medium leading-relaxed text-[hsla(var(--text-secondary))]"
|
||||||
dangerouslySetInnerHTML={{ __html: description }}
|
/>
|
||||||
className="font-medium leading-relaxed text-[hsla(var(--text-secondary))]"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full flex-shrink-0 pr-1 sm:w-1/2">
|
<div className="w-full flex-shrink-0 pr-1 sm:w-1/2">
|
||||||
<Input
|
<Input
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
type={type}
|
type={obscure ? 'password' : 'text'}
|
||||||
textAlign={textAlign}
|
textAlign={textAlign}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => onValueChanged?.(e.target.value)}
|
onChange={(e) => onValueChanged?.(e.target.value)}
|
||||||
|
suffixIcon={
|
||||||
|
<InputExtraActions
|
||||||
|
actions={inputActions ?? []}
|
||||||
|
onAction={onAction}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type InputActionProps = {
|
||||||
|
actions: InputAction[]
|
||||||
|
onAction: (action: InputAction) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const InputExtraActions: React.FC<InputActionProps> = ({
|
||||||
|
actions,
|
||||||
|
onAction,
|
||||||
|
}) => {
|
||||||
|
if (actions.length === 0) return <Fragment />
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-row space-x-2">
|
||||||
|
{actions.map((action) => {
|
||||||
|
switch (action) {
|
||||||
|
case 'copy':
|
||||||
|
return (
|
||||||
|
<CopyIcon
|
||||||
|
key={action}
|
||||||
|
size={16}
|
||||||
|
onClick={() => onAction(action)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
case 'unobscure':
|
||||||
|
return (
|
||||||
|
<EyeIcon
|
||||||
|
key={action}
|
||||||
|
size={16}
|
||||||
|
onClick={() => onAction(action)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return <FolderOpenIcon key={action} />
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default SettingDetailTextInputItem
|
export default SettingDetailTextInputItem
|
||||||
|
|||||||
@ -38,7 +38,6 @@ const SettingDetailToggleItem: React.FC<Props> = ({
|
|||||||
<h1 className="font-semibold">{settingProps.title}</h1>
|
<h1 className="font-semibold">{settingProps.title}</h1>
|
||||||
{
|
{
|
||||||
<div
|
<div
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
||||||
dangerouslySetInnerHTML={{ __html: description }}
|
dangerouslySetInnerHTML={{ __html: description }}
|
||||||
className="font-medium leading-relaxed text-[hsla(var(--text-secondary))]"
|
className="font-medium leading-relaxed text-[hsla(var(--text-secondary))]"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -282,7 +282,6 @@ const SimpleTextMessage: React.FC<ThreadMessage> = (props) => {
|
|||||||
'message max-width-[100%] flex flex-col gap-y-2 overflow-auto font-medium leading-relaxed',
|
'message max-width-[100%] flex flex-col gap-y-2 overflow-auto font-medium leading-relaxed',
|
||||||
isUser && 'whitespace-pre-wrap break-words'
|
isUser && 'whitespace-pre-wrap break-words'
|
||||||
)}
|
)}
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
||||||
dangerouslySetInnerHTML={{ __html: parsedText }}
|
dangerouslySetInnerHTML={{ __html: parsedText }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user