feat: support app rename and make app card ui better (#766)

Co-authored-by: Gillian97 <jinling.sunshine@gmail.com>
This commit is contained in:
Joel
2023-08-16 10:31:08 +08:00
committed by GitHub
parent 216fc5d312
commit f95f6db0e3
53 changed files with 612 additions and 419 deletions

View File

@@ -2,64 +2,165 @@
import { useContext, useContextSelector } from 'use-context-selector'
import Link from 'next/link'
import type { MouseEventHandler } from 'react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import cn from 'classnames'
import style from '../list.module.css'
import AppModeLabel from './AppModeLabel'
import s from './style.module.css'
import SettingsModal from '@/app/components/app/overview/settings'
import type { App } from '@/types/app'
import Confirm from '@/app/components/base/confirm'
import { ToastContext } from '@/app/components/base/toast'
import { deleteApp } from '@/service/apps'
import { deleteApp, fetchAppDetail, updateAppSiteConfig } from '@/service/apps'
import AppIcon from '@/app/components/base/app-icon'
import AppsContext, { useAppContext } from '@/context/app-context'
import CustomPopover from '@/app/components/base/popover'
import Divider from '@/app/components/base/divider'
import { asyncRunSafe } from '@/utils'
export type AppCardProps = {
app: App
onDelete?: () => void
onRefresh?: () => void
}
const AppCard = ({
app,
onDelete,
}: AppCardProps) => {
const AppCard = ({ app, onRefresh }: AppCardProps) => {
const { t } = useTranslation()
const { notify } = useContext(ToastContext)
const { isCurrentWorkspaceManager } = useAppContext()
const mutateApps = useContextSelector(AppsContext, state => state.mutateApps)
const mutateApps = useContextSelector(
AppsContext,
state => state.mutateApps,
)
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
const onDeleteClick: MouseEventHandler = useCallback((e) => {
e.preventDefault()
setShowConfirmDelete(true)
}, [])
const [showSettingsModal, setShowSettingsModal] = useState(false)
const [detailState, setDetailState] = useState<{
loading: boolean
detail?: App
}>({ loading: false })
const onConfirmDelete = useCallback(async () => {
try {
await deleteApp(app.id)
notify({ type: 'success', message: t('app.appDeleted') })
if (onDelete)
onDelete()
if (onRefresh)
onRefresh()
mutateApps()
}
catch (e: any) {
notify({ type: 'error', message: `${t('app.appDeleteFailed')}${'message' in e ? `: ${e.message}` : ''}` })
notify({
type: 'error',
message: `${t('app.appDeleteFailed')}${
'message' in e ? `: ${e.message}` : ''
}`,
})
}
setShowConfirmDelete(false)
}, [app.id])
const getAppDetail = async () => {
setDetailState({ loading: true })
const [err, res] = await asyncRunSafe<App>(
fetchAppDetail({ url: '/apps', id: app.id }) as Promise<App>,
)
if (!err) {
setDetailState({ loading: false, detail: res })
setShowSettingsModal(true)
}
else { setDetailState({ loading: false }) }
}
const onSaveSiteConfig = useCallback(
async (params: any) => {
const [err] = await asyncRunSafe<App>(
updateAppSiteConfig({
url: `/apps/${app.id}/site`,
body: params,
}) as Promise<App>,
)
if (!err) {
notify({
type: 'success',
message: t('common.actionMsg.modifiedSuccessfully'),
})
if (onRefresh)
onRefresh()
mutateApps()
}
else {
notify({
type: 'error',
message: t('common.actionMsg.modificationFailed'),
})
}
},
[app.id],
)
const Operations = (props: any) => {
const onClickSettings = async (e: any) => {
props?.onClose()
e.preventDefault()
await getAppDetail()
}
const onClickDelete = async (e: any) => {
props?.onClose()
e.preventDefault()
setShowConfirmDelete(true)
}
return (
<div className="w-full py-1">
<button className={s.actionItem} onClick={onClickSettings} disabled={detailState.loading}>
<span className={s.actionName}>{t('common.operation.settings')}</span>
</button>
<Divider className="!my-1" />
<div
className={cn(s.actionItem, s.deleteActionItem, 'group')}
onClick={onClickDelete}
>
<span className={cn(s.actionName, 'group-hover:text-red-500')}>
{t('common.operation.delete')}
</span>
</div>
</div>
)
}
return (
<>
<Link href={`/app/${app.id}/overview`} className={style.listItem}>
<Link
href={`/app/${app.id}/overview`}
className={style.listItem}
>
<div className={style.listItemTitle}>
<AppIcon size='small' icon={app.icon} background={app.icon_background} />
<AppIcon
size="small"
icon={app.icon}
background={app.icon_background}
/>
<div className={style.listItemHeading}>
<div className={style.listItemHeadingContent}>{app.name}</div>
</div>
{ isCurrentWorkspaceManager
&& <span className={style.deleteAppIcon} onClick={onDeleteClick} />}
{isCurrentWorkspaceManager && <CustomPopover
htmlContent={<Operations />}
position="br"
trigger="click"
btnElement={<div className={cn(s.actionIcon, s.commonIcon)} />}
btnClassName={open =>
cn(
open ? '!bg-gray-100 !shadow-none' : '!bg-transparent',
style.actionIconWrapper,
)
}
className={'!w-[128px] h-fit !z-20'}
/>}
</div>
<div className={style.listItemDescription}>
{app.model_config?.pre_prompt}
</div>
<div className={style.listItemDescription}>{app.model_config?.pre_prompt}</div>
<div className={style.listItemFooter}>
<AppModeLabel mode={app.mode} />
</div>
@@ -74,6 +175,14 @@ const AppCard = ({
onCancel={() => setShowConfirmDelete(false)}
/>
)}
{showSettingsModal && detailState.detail && (
<SettingsModal
appInfo={detailState.detail}
isShow={showSettingsModal}
onClose={() => setShowSettingsModal(false)}
onSave={onSaveSiteConfig}
/>
)}
</Link>
</>
)

View File

@@ -2,8 +2,8 @@
import classNames from 'classnames'
import { useTranslation } from 'react-i18next'
import { type AppMode } from '@/types/app'
import style from '../list.module.css'
import { type AppMode } from '@/types/app'
export type AppModeLabelProps = {
mode: AppMode

View File

@@ -54,7 +54,7 @@ const Apps = () => {
return (
<nav className='grid content-start grid-cols-1 gap-4 px-12 pt-8 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 grow shrink-0'>
{data?.map(({ data: apps }) => apps.map(app => (
<AppCard key={app.id} app={app} onDelete={mutate} />
<AppCard key={app.id} app={app} onRefresh={mutate} />
)))}
{ isCurrentWorkspaceManager
&& <NewAppCard ref={anchorRef} onSuccess={mutate} />}

View File

@@ -119,7 +119,7 @@ const NewAppDialog = ({ show, onSuccess, onClose }: NewAppDialogProps) => {
<div className='flex items-center justify-between gap-3 mb-8'>
<AppIcon size='large' onClick={() => { setShowEmojiPicker(true) }} className='cursor-pointer' icon={emoji.icon} background={emoji.icon_background} />
<input ref={nameInputRef} className='h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg grow' />
<input ref={nameInputRef} className='h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg grow' placeholder={t('app.appNamePlaceholder') || ''}/>
</div>
<div className='h-[247px]'>

View File

@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.6665 7.33333V11.3333M9.33317 7.33333V11.3333M2.6665 4.66667H13.3332M12.6665 4.66667L12.0885 12.7613C12.0646 13.0977 11.914 13.4125 11.6672 13.6424C11.4205 13.8722 11.0957 14 10.7585 14H5.24117C4.90393 14 4.57922 13.8722 4.33243 13.6424C4.08564 13.4125 3.93511 13.0977 3.91117 12.7613L3.33317 4.66667H12.6665ZM9.99984 4.66667V2.66667C9.99984 2.48986 9.9296 2.32029 9.80457 2.19526C9.67955 2.07024 9.50998 2 9.33317 2H6.6665C6.48969 2 6.32012 2.07024 6.1951 2.19526C6.07008 2.32029 5.99984 2.48986 5.99984 2.66667V4.66667H9.99984Z" stroke="#1F2A37" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 711 B

View File

@@ -0,0 +1,21 @@
.commonIcon {
@apply w-4 h-4 inline-block align-middle;
background-repeat: no-repeat;
background-position: center center;
background-size: contain;
}
.actionIcon {
@apply bg-gray-500;
mask-image: url(~@/assets/action.svg);
}
.actionItem {
@apply h-9 py-2 px-3 mx-1 flex items-center gap-2 hover:bg-gray-100 rounded-lg cursor-pointer;
width: calc(100% - 0.5rem);
}
.deleteActionItem {
@apply hover:bg-red-50 !important;
}
.actionName {
@apply text-gray-700 text-sm;
}