Feat/explore (#198)
parent
b6cca59517
commit
33b3eaf324
@ -0,0 +1,8 @@
|
||||
import AppList from "@/app/components/explore/app-list"
|
||||
import React from 'react'
|
||||
|
||||
const Apps = ({ }) => {
|
||||
return <AppList />
|
||||
}
|
||||
|
||||
export default React.memo(Apps)
|
||||
@ -0,0 +1,15 @@
|
||||
import React, { FC } from 'react'
|
||||
import Main from '@/app/components/explore/installed-app'
|
||||
|
||||
export interface IInstalledAppProps {
|
||||
params: {
|
||||
appId: string
|
||||
}
|
||||
}
|
||||
|
||||
const InstalledApp: FC<IInstalledAppProps> = ({ params: {appId} }) => {
|
||||
return (
|
||||
<Main id={appId} />
|
||||
)
|
||||
}
|
||||
export default React.memo(InstalledApp)
|
||||
@ -0,0 +1,16 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import ExploreClient from '@/app/components/explore'
|
||||
export type IAppDetail = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const AppDetail: FC<IAppDetail> = ({ children }) => {
|
||||
return (
|
||||
<ExploreClient>
|
||||
{children}
|
||||
</ExploreClient>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(AppDetail)
|
||||
@ -0,0 +1,65 @@
|
||||
'use client'
|
||||
import cn from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { App } from '@/models/explore'
|
||||
import AppModeLabel from '@/app/(commonLayout)/apps/AppModeLabel'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import { PlusIcon } from '@heroicons/react/20/solid'
|
||||
import Button from '../../base/button'
|
||||
|
||||
import s from './style.module.css'
|
||||
|
||||
const CustomizeBtn = (
|
||||
<svg width="15" height="14" viewBox="0 0 15 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.5 2.33366C6.69458 2.33366 6.04167 2.98658 6.04167 3.79199C6.04167 4.59741 6.69458 5.25033 7.5 5.25033C8.30542 5.25033 8.95833 4.59741 8.95833 3.79199C8.95833 2.98658 8.30542 2.33366 7.5 2.33366ZM7.5 2.33366V1.16699M12.75 8.71385C11.4673 10.1671 9.59071 11.0837 7.5 11.0837C5.40929 11.0837 3.53265 10.1671 2.25 8.71385M6.76782 5.05298L2.25 12.8337M8.23218 5.05298L12.75 12.8337" stroke="#344054" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export type AppCardProps = {
|
||||
app: App,
|
||||
canCreate: boolean,
|
||||
onCreate: () => void,
|
||||
onAddToWorkspace: (appId: string) => void,
|
||||
}
|
||||
|
||||
const AppCard = ({
|
||||
app,
|
||||
canCreate,
|
||||
onCreate,
|
||||
onAddToWorkspace,
|
||||
}: AppCardProps) => {
|
||||
const { t } = useTranslation()
|
||||
const {app: appBasicInfo} = app
|
||||
return (
|
||||
<div className={s.wrap}>
|
||||
<div className='col-span-1 bg-white border-2 border-solid border-transparent rounded-lg shadow-sm min-h-[160px] flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg'>
|
||||
<div className='flex pt-[14px] px-[14px] pb-3 h-[66px] items-center gap-3 grow-0 shrink-0'>
|
||||
<AppIcon size='small' icon={app.app.icon} background={app.app.icon_background} />
|
||||
<div className='relative h-8 text-sm font-medium leading-8 grow'>
|
||||
<div className='absolute top-0 left-0 w-full h-full overflow-hidden text-ellipsis whitespace-nowrap'>{appBasicInfo.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mb-3 px-[14px] h-9 text-xs leading-normal text-gray-500 line-clamp-2'>{app.description}</div>
|
||||
<div className='flex items-center flex-wrap min-h-[42px] px-[14px] pt-2 pb-[10px]'>
|
||||
<div className={s.mode}>
|
||||
<AppModeLabel mode={appBasicInfo.mode} />
|
||||
</div>
|
||||
<div className={cn(s.opWrap, 'flex items-center w-full space-x-2')}>
|
||||
<Button type='primary' className='grow flex items-center !h-7' onClick={() => onAddToWorkspace(appBasicInfo.id)}>
|
||||
<PlusIcon className='w-4 h-4 mr-1' />
|
||||
<span className='text-xs'>{t('explore.appCard.addToWorkspace')}</span>
|
||||
</Button>
|
||||
{canCreate && (
|
||||
<Button className='grow flex items-center !h-7 space-x-1' onClick={onCreate}>
|
||||
{CustomizeBtn}
|
||||
<span className='text-xs'>{t('explore.appCard.customize')}</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AppCard
|
||||
@ -0,0 +1,20 @@
|
||||
.wrap {
|
||||
min-width: 312px;
|
||||
}
|
||||
|
||||
.mode {
|
||||
display: flex;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.opWrap {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wrap:hover .mode {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wrap:hover .opWrap {
|
||||
display: flex;
|
||||
}
|
||||
@ -0,0 +1,130 @@
|
||||
'use client'
|
||||
import React, { FC, useEffect } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import ExploreContext from '@/context/explore-context'
|
||||
import { App } from '@/models/explore'
|
||||
import Category from '@/app/components/explore/category'
|
||||
import AppCard from '@/app/components/explore/app-card'
|
||||
import { fetchAppList, installApp, fetchAppDetail } from '@/service/explore'
|
||||
import { createApp } from '@/service/apps'
|
||||
import CreateAppModal from '@/app/components/explore/create-app-modal'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
|
||||
import s from './style.module.css'
|
||||
import Toast from '../../base/toast'
|
||||
|
||||
const Apps: FC = ({ }) => {
|
||||
const { t } = useTranslation()
|
||||
const router = useRouter()
|
||||
const { setControlUpdateInstalledApps, hasEditPermission } = useContext(ExploreContext)
|
||||
const [currCategory, setCurrCategory] = React.useState('')
|
||||
const [allList, setAllList] = React.useState<App[]>([])
|
||||
const [isLoaded, setIsLoaded] = React.useState(false)
|
||||
|
||||
const currList = (() => {
|
||||
if(currCategory === '') return allList
|
||||
return allList.filter(item => item.category === currCategory)
|
||||
})()
|
||||
const [categories, setCategories] = React.useState([])
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const {categories, recommended_apps}:any = await fetchAppList()
|
||||
setCategories(categories)
|
||||
setAllList(recommended_apps)
|
||||
setIsLoaded(true)
|
||||
})()
|
||||
}, [])
|
||||
|
||||
const handleAddToWorkspace = async (appId: string) => {
|
||||
await installApp(appId)
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('common.api.success'),
|
||||
})
|
||||
setControlUpdateInstalledApps(Date.now())
|
||||
}
|
||||
|
||||
const [currApp, setCurrApp] = React.useState<App | null>(null)
|
||||
const [isShowCreateModal, setIsShowCreateModal] = React.useState(false)
|
||||
const onCreate = async ({name, icon, icon_background}: any) => {
|
||||
const { app_model_config: model_config } = await fetchAppDetail(currApp?.app.id as string)
|
||||
|
||||
try {
|
||||
const app = await createApp({
|
||||
name,
|
||||
icon,
|
||||
icon_background,
|
||||
mode: currApp?.app.mode as any,
|
||||
config: model_config,
|
||||
})
|
||||
setIsShowCreateModal(false)
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('app.newApp.appCreated'),
|
||||
})
|
||||
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
|
||||
router.push(`/app/${app.id}/overview`)
|
||||
} catch (e) {
|
||||
Toast.notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
|
||||
}
|
||||
}
|
||||
|
||||
if(!isLoaded) {
|
||||
return (
|
||||
<div className='flex h-full items-center'>
|
||||
<Loading type='area' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='h-full flex flex-col'>
|
||||
<div className='shrink-0 pt-6 px-12'>
|
||||
<div className='mb-1 text-primary-600 text-xl font-semibold'>{t('explore.apps.title')}</div>
|
||||
<div className='text-gray-500 text-sm'>{t('explore.apps.description')}</div>
|
||||
</div>
|
||||
<Category
|
||||
className='mt-6 px-12'
|
||||
list={categories}
|
||||
value={currCategory}
|
||||
onChange={setCurrCategory}
|
||||
/>
|
||||
<div
|
||||
className='flex mt-6 flex-col overflow-auto bg-gray-100 shrink-0 grow'
|
||||
style={{
|
||||
maxHeight: 'calc(100vh - 243px)'
|
||||
}}
|
||||
>
|
||||
<nav
|
||||
className={`${s.appList} grid content-start grid-cols-1 gap-4 px-12 pb-10grow shrink-0`}>
|
||||
{currList.map(app => (
|
||||
<AppCard
|
||||
key={app.app_id}
|
||||
app={app}
|
||||
canCreate={hasEditPermission}
|
||||
onCreate={() => {
|
||||
setCurrApp(app)
|
||||
setIsShowCreateModal(true)
|
||||
}}
|
||||
onAddToWorkspace={handleAddToWorkspace}
|
||||
/>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{isShowCreateModal && (
|
||||
<CreateAppModal
|
||||
appName={currApp?.app.name || ''}
|
||||
show={isShowCreateModal}
|
||||
onConfirm={onCreate}
|
||||
onHide={() => setIsShowCreateModal(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Apps)
|
||||
@ -0,0 +1,17 @@
|
||||
@media (min-width: 1624px) {
|
||||
.appList {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr))
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1300px) and (max-width: 1624px) {
|
||||
.appList {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr))
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1025px) and (max-width: 1300px) {
|
||||
.appList {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr))
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
'use client'
|
||||
import React, { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import exploreI18n from '@/i18n/lang/explore.en'
|
||||
import cn from 'classnames'
|
||||
|
||||
const categoryI18n = exploreI18n.category
|
||||
|
||||
export interface ICategoryProps {
|
||||
className?: string
|
||||
list: string[]
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
|
||||
const Category: FC<ICategoryProps> = ({
|
||||
className,
|
||||
list,
|
||||
value,
|
||||
onChange
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const itemClassName = (isSelected: boolean) => cn(isSelected ? 'bg-white text-primary-600 border-gray-200 font-semibold' : 'border-transparent font-medium','flex items-center h-7 px-3 border cursor-pointer rounded-lg')
|
||||
const itemStyle = (isSelected: boolean) => isSelected ? {boxShadow: '0px 1px 2px rgba(16, 24, 40, 0.05)'} : {}
|
||||
return (
|
||||
<div className={cn(className, 'flex space-x-1 text-[13px]')}>
|
||||
<div
|
||||
className={itemClassName('' === value)}
|
||||
style={itemStyle('' === value)}
|
||||
onClick={() => onChange('')}
|
||||
>
|
||||
{t('explore.apps.allCategories')}
|
||||
</div>
|
||||
{list.map(name => (
|
||||
<div
|
||||
key={name}
|
||||
className={itemClassName(name === value)}
|
||||
style={itemStyle(name === value)}
|
||||
onClick={() => onChange(name)}
|
||||
>
|
||||
{(categoryI18n as any)[name] ? t(`explore.category.${name}`) : name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Category)
|
||||
@ -0,0 +1,86 @@
|
||||
'use client'
|
||||
import React, { useState } from 'react'
|
||||
import cn from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import EmojiPicker from '@/app/components/base/emoji-picker'
|
||||
|
||||
import s from './style.module.css'
|
||||
|
||||
type IProps = {
|
||||
appName: string,
|
||||
show: boolean,
|
||||
onConfirm: (info: any) => void,
|
||||
onHide: () => void,
|
||||
}
|
||||
|
||||
const CreateAppModal = ({
|
||||
appName,
|
||||
show = false,
|
||||
onConfirm,
|
||||
onHide,
|
||||
}: IProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [name, setName] = React.useState('')
|
||||
|
||||
const [showEmojiPicker, setShowEmojiPicker] = useState(false)
|
||||
const [emoji, setEmoji] = useState({ icon: '🍌', icon_background: '#FFEAD5' })
|
||||
|
||||
const submit = () => {
|
||||
if(!name.trim()) {
|
||||
Toast.notify({ type: 'error', message: t('explore.appCustomize.nameRequired') })
|
||||
return
|
||||
}
|
||||
onConfirm({
|
||||
name,
|
||||
...emoji,
|
||||
})
|
||||
onHide()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
isShow={show}
|
||||
onClose={onHide}
|
||||
className={cn(s.modal, '!max-w-[480px]', 'px-8')}
|
||||
>
|
||||
<span className={s.close} onClick={onHide}/>
|
||||
<div className={s.title}>{t('explore.appCustomize.title', {name: appName})}</div>
|
||||
<div className={s.content}>
|
||||
<div className={s.subTitle}>{t('explore.appCustomize.subTitle')}</div>
|
||||
<div className='flex items-center justify-between space-x-3'>
|
||||
<AppIcon size='large' onClick={() => { setShowEmojiPicker(true) }} className='cursor-pointer' icon={emoji.icon} background={emoji.icon_background} />
|
||||
<input
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
className='h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg grow'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-row-reverse'>
|
||||
<Button className='w-24 ml-2' type='primary' onClick={submit}>{t('common.operation.create')}</Button>
|
||||
<Button className='w-24' onClick={onHide}>{t('common.operation.cancel')}</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
{showEmojiPicker && <EmojiPicker
|
||||
onSelect={(icon, icon_background) => {
|
||||
console.log(icon, icon_background)
|
||||
setEmoji({ icon, icon_background })
|
||||
setShowEmojiPicker(false)
|
||||
}}
|
||||
onClose={() => {
|
||||
setEmoji({ icon: '🍌', icon_background: '#FFEAD5' })
|
||||
setShowEmojiPicker(false)
|
||||
}}
|
||||
/>}
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default CreateAppModal
|
||||
@ -0,0 +1,36 @@
|
||||
.modal {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.modal .close {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 25px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
background: center no-repeat url(~@/app/components/datasets/create/assets/close.svg);
|
||||
background-size: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.modal .title {
|
||||
@apply mb-9;
|
||||
font-weight: 600;
|
||||
font-size: 20px;
|
||||
line-height: 30px;
|
||||
color: #101828;
|
||||
}
|
||||
|
||||
.modal .content {
|
||||
@apply mb-9;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #101828;
|
||||
}
|
||||
|
||||
.subTitle {
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
'use client'
|
||||
import React, { FC, useEffect, useState } from 'react'
|
||||
import ExploreContext from '@/context/explore-context'
|
||||
import Sidebar from '@/app/components/explore/sidebar'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { fetchMembers } from '@/service/common'
|
||||
import { InstalledApp } from '@/models/explore'
|
||||
|
||||
export interface IExploreProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const Explore: FC<IExploreProps> = ({
|
||||
children
|
||||
}) => {
|
||||
const [controlUpdateInstalledApps, setControlUpdateInstalledApps] = useState(0)
|
||||
const { userProfile } = useAppContext()
|
||||
const [hasEditPermission, setHasEditPermission] = useState(false)
|
||||
const [installedApps, setInstalledApps] = useState<InstalledApp[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const { accounts } = await fetchMembers({ url: '/workspaces/current/members', params: {}})
|
||||
if(!accounts) return
|
||||
const currUser = accounts.find(account => account.id === userProfile.id)
|
||||
setHasEditPermission(currUser?.role !== 'normal')
|
||||
})()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className='flex h-full bg-gray-100 border-t border-gray-200'>
|
||||
<ExploreContext.Provider
|
||||
value={
|
||||
{
|
||||
controlUpdateInstalledApps,
|
||||
setControlUpdateInstalledApps,
|
||||
hasEditPermission,
|
||||
installedApps,
|
||||
setInstalledApps
|
||||
}
|
||||
}
|
||||
>
|
||||
<Sidebar controlUpdateInstalledApps={controlUpdateInstalledApps} />
|
||||
<div className='grow'>
|
||||
{children}
|
||||
</div>
|
||||
</ExploreContext.Provider>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Explore)
|
||||
@ -0,0 +1,37 @@
|
||||
'use client'
|
||||
import React, { FC } from 'react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import ExploreContext from '@/context/explore-context'
|
||||
import ChatApp from '@/app/components/share/chat'
|
||||
import TextGenerationApp from '@/app/components/share/text-generation'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
|
||||
export interface IInstalledAppProps {
|
||||
id: string
|
||||
}
|
||||
|
||||
const InstalledApp: FC<IInstalledAppProps> = ({
|
||||
id,
|
||||
}) => {
|
||||
const { installedApps } = useContext(ExploreContext)
|
||||
const installedApp = installedApps.find(item => item.id === id)
|
||||
|
||||
if(!installedApp) {
|
||||
return (
|
||||
<div className='flex h-full items-center'>
|
||||
<Loading type='area' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='h-full p-2'>
|
||||
{installedApp?.app.mode === 'chat' ? (
|
||||
<ChatApp isInstalledApp installedAppInfo={installedApp}/>
|
||||
): (
|
||||
<TextGenerationApp isInstalledApp installedAppInfo={installedApp}/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(InstalledApp)
|
||||
@ -0,0 +1,60 @@
|
||||
'use client'
|
||||
import React, { FC } from 'react'
|
||||
import cn from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Popover from '@/app/components/base/popover'
|
||||
import { TrashIcon } from '@heroicons/react/24/outline'
|
||||
|
||||
import s from './style.module.css'
|
||||
|
||||
const PinIcon = (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.00012 9.99967L8.00012 14.6663M5.33346 4.87176V6.29217C5.33346 6.43085 5.33346 6.50019 5.31985 6.56652C5.30777 6.62536 5.2878 6.6823 5.26047 6.73579C5.22966 6.79608 5.18635 6.85023 5.09972 6.95852L4.0532 8.26667C3.60937 8.82145 3.38746 9.09884 3.38721 9.33229C3.38699 9.53532 3.4793 9.72738 3.63797 9.85404C3.82042 9.99967 4.17566 9.99967 4.88612 9.99967H11.1141C11.8246 9.99967 12.1798 9.99967 12.3623 9.85404C12.5209 9.72738 12.6133 9.53532 12.613 9.33229C12.6128 9.09884 12.3909 8.82145 11.947 8.26667L10.9005 6.95852C10.8139 6.85023 10.7706 6.79608 10.7398 6.73579C10.7125 6.6823 10.6925 6.62536 10.6804 6.56652C10.6668 6.50019 10.6668 6.43085 10.6668 6.29217V4.87176C10.6668 4.79501 10.6668 4.75664 10.6711 4.71879C10.675 4.68517 10.6814 4.6519 10.6903 4.61925C10.7003 4.5825 10.7146 4.54687 10.7431 4.47561L11.415 2.79582C11.611 2.30577 11.709 2.06074 11.6682 1.86404C11.6324 1.69203 11.5302 1.54108 11.3838 1.44401C11.2163 1.33301 10.9524 1.33301 10.4246 1.33301H5.57563C5.04782 1.33301 4.78391 1.33301 4.61646 1.44401C4.47003 1.54108 4.36783 1.69203 4.33209 1.86404C4.29122 2.06074 4.38923 2.30577 4.58525 2.79583L5.25717 4.47561C5.28567 4.54687 5.29992 4.5825 5.30995 4.61925C5.31886 4.6519 5.32526 4.68517 5.32912 4.71879C5.33346 4.75664 5.33346 4.79501 5.33346 4.87176Z" stroke="#667085" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export interface IItemOperationProps {
|
||||
className?: string
|
||||
isPinned: boolean
|
||||
isShowDelete: boolean
|
||||
togglePin: () => void
|
||||
onDelete: () => void
|
||||
}
|
||||
|
||||
const ItemOperation: FC<IItemOperationProps> = ({
|
||||
className,
|
||||
isPinned,
|
||||
isShowDelete,
|
||||
togglePin,
|
||||
onDelete
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Popover
|
||||
htmlContent={
|
||||
<div className='w-full py-1' onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
}}>
|
||||
<div className={cn(s.actionItem, 'hover:bg-gray-50 group')} onClick={togglePin}>
|
||||
{PinIcon}
|
||||
<span className={s.actionName}>{isPinned ? t('explore.sidebar.action.unpin') : t('explore.sidebar.action.pin')}</span>
|
||||
</div>
|
||||
{isShowDelete && (
|
||||
<div className={cn(s.actionItem, s.deleteActionItem, 'hover:bg-gray-50 group')} onClick={onDelete} >
|
||||
<TrashIcon className={'w-4 h-4 stroke-current text-gray-500 stroke-2 group-hover:text-red-500'} />
|
||||
<span className={cn(s.actionName, 'group-hover:text-red-500')}>{t('explore.sidebar.action.delete')}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
}
|
||||
trigger='click'
|
||||
position='br'
|
||||
btnElement={<div />}
|
||||
btnClassName={(open) => cn(className, s.btn, 'h-6 w-6 rounded-md border-none p-1', open && '!bg-gray-100 !shadow-none')}
|
||||
className={`!w-[120px] h-fit !z-20`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default React.memo(ItemOperation)
|
||||
@ -0,0 +1,31 @@
|
||||
.actionItem {
|
||||
@apply h-9 py-2 px-3 mx-1 flex items-center gap-2 rounded-lg cursor-pointer;
|
||||
}
|
||||
|
||||
|
||||
.actionName {
|
||||
@apply text-gray-700 text-sm;
|
||||
}
|
||||
|
||||
.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(~@/app/components/datasets/documents/assets/action.svg);
|
||||
}
|
||||
|
||||
body .btn {
|
||||
background: url(~@/app/components/datasets/documents/assets/action.svg) center center no-repeat transparent;
|
||||
background-size: 16px 16px;
|
||||
/* mask-image: ; */
|
||||
}
|
||||
|
||||
body .btn:hover {
|
||||
/* background-image: ; */
|
||||
background-color: #F2F4F7;
|
||||
}
|
||||
@ -0,0 +1,73 @@
|
||||
'use client'
|
||||
import cn from 'classnames'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import ItemOperation from '@/app/components/explore/item-operation'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
|
||||
import s from './style.module.css'
|
||||
|
||||
export interface IAppNavItemProps {
|
||||
name: string
|
||||
id: string
|
||||
icon: string
|
||||
icon_background: string
|
||||
isSelected: boolean
|
||||
isPinned: boolean
|
||||
togglePin: () => void
|
||||
uninstallable: boolean
|
||||
onDelete: (id: string) => void
|
||||
}
|
||||
|
||||
export default function AppNavItem({
|
||||
name,
|
||||
id,
|
||||
icon,
|
||||
icon_background,
|
||||
isSelected,
|
||||
isPinned,
|
||||
togglePin,
|
||||
uninstallable,
|
||||
onDelete,
|
||||
}: IAppNavItemProps) {
|
||||
const router = useRouter()
|
||||
const url = `/explore/installed/${id}`
|
||||
|
||||
return (
|
||||
<div
|
||||
key={id}
|
||||
className={cn(
|
||||
s.item,
|
||||
isSelected ? s.active : 'hover:bg-gray-200',
|
||||
'flex h-8 justify-between px-2 rounded-lg text-sm font-normal ',
|
||||
)}
|
||||
onClick={() => {
|
||||
router.push(url) // use Link causes popup item always trigger jump. Can not be solved by e.stopPropagation().
|
||||
}}
|
||||
>
|
||||
<div className='flex items-center space-x-2 w-0 grow'>
|
||||
{/* <div
|
||||
className={cn(
|
||||
'shrink-0 mr-2 h-6 w-6 rounded-md border bg-[#D5F5F6]',
|
||||
)}
|
||||
style={{
|
||||
borderColor: '0.5px solid rgba(0, 0, 0, 0.05)'
|
||||
}}
|
||||
/> */}
|
||||
<AppIcon size='tiny' icon={icon} background={icon_background} />
|
||||
<div className='overflow-hidden text-ellipsis whitespace-nowrap'>{name}</div>
|
||||
</div>
|
||||
{
|
||||
!isSelected && (
|
||||
<div className={cn(s.opBtn, 'shrink-0')} onClick={e => e.stopPropagation()}>
|
||||
<ItemOperation
|
||||
isPinned={isPinned}
|
||||
togglePin={togglePin}
|
||||
isShowDelete={!uninstallable}
|
||||
onDelete={() => onDelete(id)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
/* .item:hover, */
|
||||
.item.active {
|
||||
border: 0.5px solid #EAECF0;
|
||||
box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05);
|
||||
border-radius: 8px;
|
||||
background: #FFFFFF;
|
||||
color: #344054;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.opBtn {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.item:hover .opBtn {
|
||||
visibility: visible;
|
||||
}
|
||||
@ -0,0 +1,128 @@
|
||||
'use client'
|
||||
import React, { FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import ExploreContext from '@/context/explore-context'
|
||||
import cn from 'classnames'
|
||||
import { useSelectedLayoutSegments } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import Item from './app-nav-item'
|
||||
import { fetchInstalledAppList as doFetchInstalledAppList, uninstallApp, updatePinStatus } from '@/service/explore'
|
||||
import Toast from '../../base/toast'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
|
||||
const SelectedDiscoveryIcon = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M13.4135 1.11725C13.5091 1.09983 13.6483 1.08355 13.8078 1.11745C14.0143 1.16136 14.2017 1.26953 14.343 1.42647C14.4521 1.54766 14.5076 1.67634 14.5403 1.76781C14.5685 1.84673 14.593 1.93833 14.6136 2.01504L15.5533 5.5222C15.5739 5.5989 15.5985 5.69049 15.6135 5.77296C15.6309 5.86852 15.6472 6.00771 15.6133 6.16722C15.5694 6.37378 15.4612 6.56114 15.3043 6.70245C15.1831 6.81157 15.0544 6.86706 14.9629 6.89975C14.884 6.92796 14.7924 6.95247 14.7157 6.97299L14.676 6.98364C14.3365 7.07461 14.0437 7.15309 13.7972 7.19802C13.537 7.24543 13.2715 7.26736 12.9946 7.20849C12.7513 7.15677 12.5213 7.06047 12.3156 6.92591L9.63273 7.64477C9.86399 7.97104 9.99992 8.36965 9.99992 8.80001C9.99992 9.2424 9.85628 9.65124 9.6131 9.98245L12.5508 14.291C12.7582 14.5952 12.6797 15.01 12.3755 15.2174C12.0713 15.4248 11.6566 15.3464 11.4492 15.0422L8.51171 10.7339C8.34835 10.777 8.17682 10.8 7.99992 10.8C7.82305 10.8 7.65155 10.777 7.48823 10.734L4.5508 15.0422C4.34338 15.3464 3.92863 15.4248 3.62442 15.2174C3.32021 15.01 3.24175 14.5952 3.44916 14.291L6.3868 9.98254C6.14358 9.65132 5.99992 9.24244 5.99992 8.80001C5.99992 8.73795 6.00274 8.67655 6.00827 8.61594L4.59643 8.99424C4.51973 9.01483 4.42813 9.03941 4.34567 9.05444C4.25011 9.07185 4.11092 9.08814 3.95141 9.05423C3.74485 9.01033 3.55748 8.90215 3.41618 8.74522C3.38535 8.71097 3.3588 8.67614 3.33583 8.64171L2.49206 8.8678C2.41536 8.88838 2.32376 8.91296 2.2413 8.92799C2.14574 8.94541 2.00655 8.96169 1.84704 8.92779C1.64048 8.88388 1.45311 8.77571 1.31181 8.61877C1.20269 8.49759 1.1472 8.3689 1.1145 8.27744C1.08629 8.1985 1.06177 8.10689 1.04125 8.03018L0.791701 7.09885C0.771119 7.02215 0.746538 6.93055 0.731508 6.84809C0.714092 6.75253 0.697808 6.61334 0.731712 6.45383C0.775619 6.24726 0.883793 6.0599 1.04073 5.9186C1.16191 5.80948 1.2906 5.75399 1.38206 5.72129C1.461 5.69307 1.55261 5.66856 1.62932 5.64804L2.47318 5.42193C2.47586 5.38071 2.48143 5.33735 2.49099 5.29237C2.5349 5.08581 2.64307 4.89844 2.80001 4.75714C2.92119 4.64802 3.04988 4.59253 3.14134 4.55983C3.22027 4.53162 3.31189 4.50711 3.3886 4.48658L11.1078 2.41824C11.2186 2.19888 11.3697 2.00049 11.5545 1.83406C11.7649 1.64462 12.0058 1.53085 12.2548 1.44183C12.4907 1.35749 12.7836 1.27904 13.123 1.18809L13.1628 1.17744C13.2395 1.15686 13.3311 1.13228 13.4135 1.11725ZM13.3642 2.5039C13.0648 2.58443 12.8606 2.64126 12.7036 2.69735C12.5325 2.75852 12.4742 2.80016 12.4467 2.82492C12.3421 2.91912 12.2699 3.04403 12.2407 3.18174C12.233 3.21793 12.2261 3.28928 12.2587 3.46805C12.2927 3.6545 12.3564 3.89436 12.4559 4.26563L12.5594 4.652C12.6589 5.02328 12.7236 5.26287 12.7874 5.44133C12.8486 5.61244 12.8902 5.67079 12.915 5.69829C13.0092 5.80291 13.1341 5.87503 13.2718 5.9043C13.308 5.91199 13.3793 5.91887 13.5581 5.88629C13.7221 5.85641 13.9273 5.80352 14.2269 5.72356L13.3642 2.5039Z" fill="#155EEF"/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
const DiscoveryIcon = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.74786 9.89676L12.0003 14.6669M7.25269 9.89676L4.00027 14.6669M9.3336 8.80031C9.3336 9.53669 8.73665 10.1336 8.00027 10.1336C7.26389 10.1336 6.66694 9.53669 6.66694 8.80031C6.66694 8.06393 7.26389 7.46698 8.00027 7.46698C8.73665 7.46698 9.3336 8.06393 9.3336 8.80031ZM11.4326 3.02182L3.57641 5.12689C3.39609 5.1752 3.30593 5.19936 3.24646 5.25291C3.19415 5.30001 3.15809 5.36247 3.14345 5.43132C3.12681 5.5096 3.15097 5.59976 3.19929 5.78008L3.78595 7.96951C3.83426 8.14984 3.85842 8.24 3.91197 8.29947C3.95907 8.35178 4.02153 8.38784 4.09038 8.40248C4.16866 8.41911 4.25882 8.39496 4.43914 8.34664L12.2953 6.24158L11.4326 3.02182ZM14.5285 6.33338C13.8072 6.52665 13.4466 6.62328 13.1335 6.55673C12.8581 6.49819 12.6082 6.35396 12.4198 6.14471C12.2056 5.90682 12.109 5.54618 11.9157 4.82489L11.8122 4.43852C11.6189 3.71722 11.5223 3.35658 11.5889 3.04347C11.6474 2.76805 11.7916 2.51823 12.0009 2.32982C12.2388 2.11563 12.5994 2.019 13.3207 1.82573C13.501 1.77741 13.5912 1.75325 13.6695 1.76989C13.7383 1.78452 13.8008 1.82058 13.8479 1.87289C13.9014 1.93237 13.9256 2.02253 13.9739 2.20285L14.9057 5.68018C14.954 5.86051 14.9781 5.95067 14.9615 6.02894C14.9469 6.0978 14.9108 6.16025 14.8585 6.20736C14.799 6.2609 14.7088 6.28506 14.5285 6.33338ZM2.33475 8.22033L3.23628 7.97876C3.4166 7.93044 3.50676 7.90628 3.56623 7.85274C3.61854 7.80563 3.6546 7.74318 3.66924 7.67433C3.68588 7.59605 3.66172 7.50589 3.6134 7.32556L3.37184 6.42403C3.32352 6.24371 3.29936 6.15355 3.24581 6.09408C3.19871 6.04176 3.13626 6.00571 3.0674 5.99107C2.98912 5.97443 2.89896 5.99859 2.71864 6.04691L1.81711 6.28847C1.63678 6.33679 1.54662 6.36095 1.48715 6.4145C1.43484 6.4616 1.39878 6.52405 1.38415 6.59291C1.36751 6.67119 1.39167 6.76135 1.43998 6.94167L1.68155 7.8432C1.72987 8.02352 1.75402 8.11369 1.80757 8.17316C1.85467 8.22547 1.91713 8.26153 1.98598 8.27616C2.06426 8.2928 2.15442 8.26864 2.33475 8.22033Z" stroke="#344054" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
const SideBar: FC<{
|
||||
controlUpdateInstalledApps: number,
|
||||
}> = ({
|
||||
controlUpdateInstalledApps,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const segments = useSelectedLayoutSegments()
|
||||
const lastSegment = segments.slice(-1)[0]
|
||||
const isDiscoverySelected = lastSegment === 'apps'
|
||||
const { installedApps, setInstalledApps } = useContext(ExploreContext)
|
||||
|
||||
const fetchInstalledAppList = async () => {
|
||||
const {installed_apps} : any = await doFetchInstalledAppList()
|
||||
setInstalledApps(installed_apps)
|
||||
}
|
||||
|
||||
const [showConfirm, setShowConfirm] = useState(false)
|
||||
const [currId, setCurrId] = useState('')
|
||||
const handleDelete = async () => {
|
||||
const id = currId
|
||||
await uninstallApp(id)
|
||||
setShowConfirm(false)
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('common.api.remove')
|
||||
})
|
||||
fetchInstalledAppList()
|
||||
}
|
||||
|
||||
const handleUpdatePinStatus = async (id: string, isPinned: boolean) => {
|
||||
await updatePinStatus(id, isPinned)
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('common.api.success')
|
||||
})
|
||||
fetchInstalledAppList()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchInstalledAppList()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
fetchInstalledAppList()
|
||||
}, [controlUpdateInstalledApps])
|
||||
|
||||
return (
|
||||
<div className='w-[216px] shrink-0 pt-6 px-4 border-gray-200 cursor-pointer'>
|
||||
<div>
|
||||
<Link
|
||||
href='/explore/apps'
|
||||
className={cn(isDiscoverySelected ? 'text-primary-600 bg-white font-semibold' : 'text-gray-700 font-medium','flex items-center h-9 pl-3 space-x-2 rounded-lg')}
|
||||
style={isDiscoverySelected ? {boxShadow: '0px 1px 2px rgba(16, 24, 40, 0.05)'} : {}}
|
||||
>
|
||||
{isDiscoverySelected ? <SelectedDiscoveryIcon /> : <DiscoveryIcon />}
|
||||
<div className='text-sm'>{t('explore.sidebar.discovery')}</div>
|
||||
</Link>
|
||||
</div>
|
||||
{installedApps.length > 0 && (
|
||||
<div className='mt-10'>
|
||||
<div className='pl-2 text-xs text-gray-500 font-medium uppercase'>{t('explore.sidebar.workspace')}</div>
|
||||
<div className='mt-3 space-y-1 overflow-y-auto overflow-x-hidden pb-20'
|
||||
style={{
|
||||
maxHeight: 'calc(100vh - 250px)'
|
||||
}}
|
||||
>
|
||||
{installedApps.map(({id, is_pinned, uninstallable, app : { name, icon, icon_background }}) => {
|
||||
return (
|
||||
<Item
|
||||
key={id}
|
||||
name={name}
|
||||
icon={icon}
|
||||
icon_background={icon_background}
|
||||
id={id}
|
||||
isSelected={lastSegment?.toLowerCase() === id}
|
||||
isPinned={is_pinned}
|
||||
togglePin={() => handleUpdatePinStatus(id, !is_pinned)}
|
||||
uninstallable={uninstallable}
|
||||
onDelete={(id) => {
|
||||
setCurrId(id)
|
||||
setShowConfirm(true)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{showConfirm && (
|
||||
<Confirm
|
||||
title={t('explore.sidebar.delete.title')}
|
||||
content={t('explore.sidebar.delete.content')}
|
||||
isShow={showConfirm}
|
||||
onClose={() => setShowConfirm(false)}
|
||||
onConfirm={handleDelete}
|
||||
onCancel={() => setShowConfirm(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(SideBar)
|
||||
@ -0,0 +1,27 @@
|
||||
'use client'
|
||||
import React, { FC } from 'react'
|
||||
import cn from 'classnames'
|
||||
import { appDefaultIconBackground } from '@/config/index'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
|
||||
export interface IAppInfoProps {
|
||||
className?: string
|
||||
icon: string
|
||||
icon_background?: string
|
||||
name: string
|
||||
}
|
||||
|
||||
const AppInfo: FC<IAppInfoProps> = ({
|
||||
className,
|
||||
icon,
|
||||
icon_background,
|
||||
name
|
||||
}) => {
|
||||
return (
|
||||
<div className={cn(className, 'flex items-center space-x-3')}>
|
||||
<AppIcon size="small" icon={icon} background={icon_background || appDefaultIconBackground} />
|
||||
<div className='w-0 grow text-sm font-semibold text-gray-800 overflow-hidden text-ellipsis whitespace-nowrap'>{name}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(AppInfo)
|
||||
@ -0,0 +1,3 @@
|
||||
.installedApp {
|
||||
height: calc(100vh - 74px);
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
import { createContext } from 'use-context-selector'
|
||||
import { InstalledApp } from '@/models/explore'
|
||||
|
||||
type IExplore = {
|
||||
controlUpdateInstalledApps: number
|
||||
setControlUpdateInstalledApps: (controlUpdateInstalledApps: number) => void
|
||||
hasEditPermission: boolean
|
||||
installedApps: InstalledApp[]
|
||||
setInstalledApps: (installedApps: InstalledApp[]) => void
|
||||
}
|
||||
|
||||
const ExploreContext = createContext<IExplore>({
|
||||
controlUpdateInstalledApps: 0,
|
||||
setControlUpdateInstalledApps: () => { },
|
||||
hasEditPermission: false,
|
||||
installedApps: [],
|
||||
setInstalledApps: () => { },
|
||||
})
|
||||
|
||||
export default ExploreContext
|
||||
@ -0,0 +1,38 @@
|
||||
const translation = {
|
||||
sidebar: {
|
||||
discovery: 'Discovery',
|
||||
workspace: 'Workspace',
|
||||
action: {
|
||||
pin: 'Pin',
|
||||
unpin: 'Unpin',
|
||||
delete: 'Delete',
|
||||
},
|
||||
delete: {
|
||||
title: 'Delete app',
|
||||
content: 'Are you sure you want to delete this app?',
|
||||
}
|
||||
},
|
||||
apps: {
|
||||
title: 'Explore Apps by Dify',
|
||||
description: 'Use these template apps instantly or customize your own apps based on the templates.',
|
||||
allCategories: 'All Categories',
|
||||
},
|
||||
appCard: {
|
||||
addToWorkspace: 'Add to Workspace',
|
||||
customize: 'Customize',
|
||||
},
|
||||
appCustomize: {
|
||||
title: 'Create app from {{name}}',
|
||||
subTitle: 'App icon & name',
|
||||
nameRequired: 'App name is required',
|
||||
},
|
||||
category: {
|
||||
'Assistant': 'Assistant',
|
||||
'Writing': 'Writing',
|
||||
'Translate': 'Translate',
|
||||
'Programming': 'Programming',
|
||||
'HR': 'HR',
|
||||
}
|
||||
}
|
||||
|
||||
export default translation
|
||||
@ -0,0 +1,38 @@
|
||||
const translation = {
|
||||
sidebar: {
|
||||
discovery: '发现',
|
||||
workspace: '工作区',
|
||||
action: {
|
||||
pin: '置顶',
|
||||
unpin: '取消置顶',
|
||||
delete: '删除',
|
||||
},
|
||||
delete: {
|
||||
title: '删除程序',
|
||||
content: '您确定要删除此程序吗?',
|
||||
}
|
||||
},
|
||||
apps: {
|
||||
title: '探索 Dify 的应用',
|
||||
description: '使用这些模板应用程序,或根据模板自定义您自己的应用程序。',
|
||||
allCategories: '所有类别',
|
||||
},
|
||||
appCard: {
|
||||
addToWorkspace: '添加到工作区',
|
||||
customize: '自定义',
|
||||
},
|
||||
appCustomize: {
|
||||
title: '从 {{name}} 创建应用程序',
|
||||
subTitle: '应用程序图标和名称',
|
||||
nameRequired: '应用程序名称不能为空',
|
||||
},
|
||||
category: {
|
||||
'Assistant': '助手',
|
||||
'Writing': '写作',
|
||||
'Translate': '翻译',
|
||||
'Programming': '编程',
|
||||
'HR': '人力资源',
|
||||
}
|
||||
}
|
||||
|
||||
export default translation
|
||||
@ -0,0 +1,30 @@
|
||||
import { AppMode } from "./app";
|
||||
|
||||
export type AppBasicInfo = {
|
||||
id: string;
|
||||
name: string;
|
||||
mode: AppMode;
|
||||
icon: string;
|
||||
icon_background: string;
|
||||
}
|
||||
|
||||
export type App = {
|
||||
app: AppBasicInfo;
|
||||
app_id: string;
|
||||
description: string;
|
||||
copyright: string;
|
||||
privacy_policy: string;
|
||||
category: string;
|
||||
position: number;
|
||||
is_listed: boolean;
|
||||
install_count: number;
|
||||
installed: boolean;
|
||||
editable: boolean;
|
||||
}
|
||||
|
||||
export type InstalledApp = {
|
||||
app: AppBasicInfo;
|
||||
id: string;
|
||||
uninstallable: boolean
|
||||
is_pinned: boolean
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
import { get, post, del, patch } from './base'
|
||||
|
||||
export const fetchAppList = () => {
|
||||
return get('/explore/apps')
|
||||
}
|
||||
|
||||
export const fetchAppDetail = (id: string) : Promise<any> => {
|
||||
return get(`/explore/apps/${id}`)
|
||||
}
|
||||
|
||||
export const fetchInstalledAppList = () => {
|
||||
return get('/installed-apps')
|
||||
}
|
||||
|
||||
export const installApp = (id: string) => {
|
||||
return post('/installed-apps', {
|
||||
body: {
|
||||
app_id: id
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const uninstallApp = (id: string) => {
|
||||
return del(`/installed-apps/${id}`)
|
||||
}
|
||||
|
||||
export const updatePinStatus = (id: string, isPinned: boolean) => {
|
||||
return patch(`/installed-apps/${id}`, {
|
||||
body: {
|
||||
is_pinned: isPinned
|
||||
}
|
||||
})
|
||||
}
|
||||
Loading…
Reference in New Issue