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:
NamH 2024-06-02 22:41:27 +07:00 committed by GitHub
parent 41404e1abd
commit 02478b3242
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 105 additions and 20 deletions

View File

@ -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 = {

View File

@ -6,7 +6,9 @@
"controllerType": "input", "controllerType": "input",
"controllerProps": { "controllerProps": {
"value": "", "value": "",
"placeholder": "hf_**********************************" "placeholder": "hf_**********************************",
"type": "password",
"inputActions": ["unobscure", "copy"]
} }
} }
] ]

View File

@ -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',

View File

@ -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/')

View File

@ -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))]"
/> />

View File

@ -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,
}), }),

View File

@ -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
// 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))]"
/> />
}
</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

View File

@ -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))]"
/> />

View File

@ -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 }}
/> />
)} )}