mirror of
https://github.com/langgenius/dify.git
synced 2026-05-23 10:29:07 +08:00
feat: enhance permission handling and UI updates across various components
This commit is contained in:
@ -31,6 +31,7 @@ import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import { usePathname, useRouter } from '@/next/navigation'
|
||||
import { fetchAppDetailDirect } from '@/service/apps'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { hasPermission } from '@/utils/permission'
|
||||
import s from './style.module.css'
|
||||
|
||||
type IAppDetailLayoutProps = {
|
||||
@ -48,7 +49,7 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
||||
const pathname = usePathname()
|
||||
const media = useBreakpoints()
|
||||
const isMobile = media === MediaType.mobile
|
||||
const { isCurrentWorkspaceEditor, isLoadingCurrentWorkspace, currentWorkspace } = useAppContext()
|
||||
const { isCurrentWorkspaceEditor, isLoadingCurrentWorkspace, currentWorkspace, workspacePermissionKeys } = useAppContext()
|
||||
const appInfoActions = useAppInfoActions({ resetKey: appId })
|
||||
const { appDetail, setAppDetail, setAppSidebarExpand } = useStore(useShallow(state => ({
|
||||
appDetail: state.appDetail,
|
||||
@ -64,6 +65,9 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
||||
selectedIcon: NavIcon
|
||||
}>>([])
|
||||
|
||||
const canAccessMonitor = hasPermission(workspacePermissionKeys, 'app.monitor.access')
|
||||
const canAccessLog = hasPermission(workspacePermissionKeys, 'app.log.access')
|
||||
|
||||
const getNavigationConfig = useCallback((appId: string, isCurrentWorkspaceEditor: boolean, mode: AppModeEnum) => {
|
||||
const navConfig = [
|
||||
...(isCurrentWorkspaceEditor
|
||||
@ -81,7 +85,7 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
||||
icon: RiTerminalBoxLine,
|
||||
selectedIcon: RiTerminalBoxFill,
|
||||
},
|
||||
...(isCurrentWorkspaceEditor
|
||||
...(canAccessLog
|
||||
? [{
|
||||
name: mode !== AppModeEnum.WORKFLOW
|
||||
? t('appMenus.logAndAnn', { ns: 'common' })
|
||||
@ -92,12 +96,14 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
||||
}]
|
||||
: []
|
||||
),
|
||||
{
|
||||
name: t('appMenus.overview', { ns: 'common' }),
|
||||
href: `/app/${appId}/overview`,
|
||||
icon: RiDashboard2Line,
|
||||
selectedIcon: RiDashboard2Fill,
|
||||
},
|
||||
...(canAccessMonitor
|
||||
? [{
|
||||
name: t('appMenus.overview', { ns: 'common' }),
|
||||
href: `/app/${appId}/overview`,
|
||||
icon: RiDashboard2Line,
|
||||
selectedIcon: RiDashboard2Fill,
|
||||
}]
|
||||
: []),
|
||||
...(isCurrentWorkspaceEditor
|
||||
? [{
|
||||
name: 'Access Config',
|
||||
|
||||
@ -4,21 +4,29 @@ import type { AliyunConfig, ArizeConfig, DatabricksConfig, LangFuseConfig, LangS
|
||||
import type { TracingStatus } from '@/models/app'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import {
|
||||
RiArrowDownDoubleLine,
|
||||
RiEqualizer2Line,
|
||||
} from '@remixicon/react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { AliyunIcon, ArizeIcon, DatabricksIcon, LangfuseIcon, LangsmithIcon, MlflowIcon, OpikIcon, PhoenixIcon, TencentIcon, WeaveIcon } from '@/app/components/base/icons/src/public/tracing'
|
||||
import {
|
||||
AliyunIcon,
|
||||
ArizeIcon,
|
||||
DatabricksIcon,
|
||||
LangfuseIcon,
|
||||
LangsmithIcon,
|
||||
MlflowIcon,
|
||||
OpikIcon,
|
||||
PhoenixIcon,
|
||||
TencentIcon,
|
||||
WeaveIcon,
|
||||
} from '@/app/components/base/icons/src/public/tracing'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
|
||||
import { usePathname } from '@/next/navigation'
|
||||
import { fetchTracingConfig as doFetchTracingConfig, fetchTracingStatus, updateTracingStatus } from '@/service/apps'
|
||||
import { hasPermission } from '@/utils/permission'
|
||||
import ConfigButton from './config-button'
|
||||
import TracingIcon from './tracing-icon'
|
||||
import { TracingProvider } from './type'
|
||||
@ -30,8 +38,9 @@ const Panel: FC = () => {
|
||||
const pathname = usePathname()
|
||||
const matched = /\/app\/([^/]+)/.exec(pathname)
|
||||
const appId = (matched?.length && matched[1]) ? matched[1] : ''
|
||||
const { isCurrentWorkspaceEditor } = useAppContext()
|
||||
const readOnly = !isCurrentWorkspaceEditor
|
||||
const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys)
|
||||
const canConfigTracing = hasPermission(workspacePermissionKeys, 'app.monitor.tracking_config')
|
||||
const readOnly = !canConfigTracing
|
||||
|
||||
const [isLoaded, {
|
||||
setTrue: setLoaded,
|
||||
@ -253,11 +262,11 @@ const Panel: FC = () => {
|
||||
<TracingIcon size="md" />
|
||||
<div className="mx-2 system-sm-semibold text-text-secondary">{t(`${I18N_PREFIX}.title`, { ns: 'app' })}</div>
|
||||
<div className="rounded-md p-1">
|
||||
<RiEqualizer2Line className="h-4 w-4 text-text-tertiary" />
|
||||
<span className="i-ri-equalizer-2-line h-4 w-4 text-text-tertiary" />
|
||||
</div>
|
||||
<Divider type="vertical" className="h-3.5" />
|
||||
<div className="rounded-md p-1">
|
||||
<RiArrowDownDoubleLine className="h-4 w-4 text-text-tertiary" />
|
||||
<span className="i-ri-arrow-down-double-line h-4 w-4 text-text-tertiary" />
|
||||
</div>
|
||||
</div>
|
||||
</ConfigButton>
|
||||
@ -297,7 +306,7 @@ const Panel: FC = () => {
|
||||
</div>
|
||||
{InUseProviderIcon && <InUseProviderIcon className="ml-1 h-4" />}
|
||||
<div className="ml-2 rounded-md p-1">
|
||||
<RiEqualizer2Line className="h-4 w-4 text-text-tertiary" />
|
||||
<span className="i-ri-equalizer-2-line h-4 w-4 text-text-tertiary" />
|
||||
</div>
|
||||
<Divider type="vertical" className="h-3.5" />
|
||||
</div>
|
||||
|
||||
@ -37,7 +37,7 @@ import AppIcon from '@/app/components/base/app-icon'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { UserAvatarList } from '@/app/components/base/user-avatar-list'
|
||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useAppContext, useSelector } from '@/context/app-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { AppCardTags } from '@/features/tag-management/components/app-card-tags'
|
||||
import { useAsyncWindowOpen } from '@/hooks/use-async-window-open'
|
||||
@ -53,6 +53,7 @@ import { fetchWorkflowDraft } from '@/service/workflow'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { getRedirection } from '@/utils/app-redirection'
|
||||
import { downloadBlob } from '@/utils/download'
|
||||
import { hasPermission } from '@/utils/permission'
|
||||
import { formatTime } from '@/utils/time'
|
||||
import { basePath } from '@/utils/var'
|
||||
|
||||
@ -107,6 +108,8 @@ const AppCardOperationsMenu: React.FC<AppCardOperationsMenuProps> = ({
|
||||
const { t } = useTranslation()
|
||||
const openAsyncWindow = useAsyncWindowOpen()
|
||||
const { push } = useRouter()
|
||||
const workspacePermissionKeys = useSelector(state => state.workspacePermissionKeys)
|
||||
const canManageAccessConfig = hasPermission(workspacePermissionKeys, 'app.access_config')
|
||||
|
||||
const handleMenuAction = useCallback((e: React.MouseEvent<HTMLElement>, action: () => void) => {
|
||||
e.stopPropagation()
|
||||
@ -177,9 +180,11 @@ const AppCardOperationsMenu: React.FC<AppCardOperationsMenuProps> = ({
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuItem className="gap-2 px-3" onClick={handleOpenAccessConfig}>
|
||||
<span className="text-sm leading-5 text-text-secondary">Access Config</span>
|
||||
</DropdownMenuItem>
|
||||
{canManageAccessConfig && (
|
||||
<DropdownMenuItem className="gap-2 px-3" onClick={handleOpenAccessConfig}>
|
||||
<span className="text-sm leading-5 text-text-secondary">Access Config</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
variant="destructive"
|
||||
@ -435,9 +440,9 @@ const AppCard = ({ app, onlineUsers = [], onRefresh, onOpenTagManagement = () =>
|
||||
e.preventDefault()
|
||||
getRedirection(isCurrentWorkspaceEditor, app, push)
|
||||
}}
|
||||
className="group relative col-span-1 inline-flex h-[160px] cursor-pointer flex-col rounded-xl border border-solid border-components-card-border bg-components-card-bg shadow-sm transition-shadow duration-200 ease-in-out hover:shadow-lg"
|
||||
className="group relative col-span-1 inline-flex h-40 cursor-pointer flex-col rounded-xl border border-solid border-components-card-border bg-components-card-bg shadow-sm transition-shadow duration-200 ease-in-out hover:shadow-lg"
|
||||
>
|
||||
<div className="flex h-[66px] shrink-0 grow-0 items-center gap-3 px-[14px] pt-[14px] pb-3">
|
||||
<div className="flex h-16.5 shrink-0 grow-0 items-center gap-3 px-3.5 pt-3.5 pb-3">
|
||||
<div className="relative shrink-0">
|
||||
<AppIcon
|
||||
size="large"
|
||||
@ -452,7 +457,7 @@ const AppCard = ({ app, onlineUsers = [], onRefresh, onOpenTagManagement = () =>
|
||||
<div className="flex items-center text-sm leading-5 font-semibold text-text-secondary">
|
||||
<div className="truncate" title={app.name}>{app.name}</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-[10px] leading-[18px] font-medium text-text-tertiary">
|
||||
<div className="flex items-center gap-1 text-2xs leading-4.5 font-medium text-text-tertiary">
|
||||
<div className="truncate" title={app.author_name}>{app.author_name}</div>
|
||||
<div>·</div>
|
||||
<div className="truncate" title={EditTimeText}>{EditTimeText}</div>
|
||||
@ -502,7 +507,7 @@ const AppCard = ({ app, onlineUsers = [], onRefresh, onOpenTagManagement = () =>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-[90px] px-[14px] text-xs leading-normal text-text-tertiary">
|
||||
<div className="h-22.5 px-3.5 text-xs leading-normal text-text-tertiary">
|
||||
<div
|
||||
className="line-clamp-2"
|
||||
title={app.description}
|
||||
@ -510,88 +515,86 @@ const AppCard = ({ app, onlineUsers = [], onRefresh, onOpenTagManagement = () =>
|
||||
{app.description}
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute right-0 bottom-1 left-0 flex h-[42px] shrink-0 items-center pt-1 pr-[6px] pb-[6px] pl-[14px]">
|
||||
<div className="absolute right-0 bottom-1 left-0 flex h-10.5 shrink-0 items-center pt-1 pr-1.5 pb-1.5 pl-3.5">
|
||||
<div
|
||||
className={cn('flex w-0 grow items-center gap-1')}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
}}
|
||||
>
|
||||
<div className="mr-10.25 min-w-0 grow overflow-hidden">
|
||||
<AppCardTags
|
||||
appId={app.id}
|
||||
tags={app.tags}
|
||||
onOpenTagManagement={onOpenTagManagement}
|
||||
onTagsChange={onRefresh}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{isCurrentWorkspaceEditor && (
|
||||
<>
|
||||
<div
|
||||
className={cn('flex w-0 grow items-center gap-1')}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
}}
|
||||
>
|
||||
<div className="mr-[41px] min-w-0 grow overflow-hidden">
|
||||
<AppCardTags
|
||||
appId={app.id}
|
||||
tags={app.tags}
|
||||
onOpenTagManagement={onOpenTagManagement}
|
||||
onTagsChange={onRefresh}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'absolute top-1/2 right-[6px] flex -translate-y-1/2 items-center transition-opacity',
|
||||
isOperationsMenuOpen
|
||||
? 'pointer-events-auto opacity-100'
|
||||
: 'pointer-events-none opacity-0 group-focus-within:pointer-events-auto group-focus-within:opacity-100 group-hover:pointer-events-auto group-hover:opacity-100',
|
||||
)}
|
||||
>
|
||||
<div className="mx-1 h-[14px] w-px shrink-0 bg-divider-regular" />
|
||||
<DropdownMenu modal={false} open={isOperationsMenuOpen} onOpenChange={setIsOperationsMenuOpen}>
|
||||
<DropdownMenuTrigger
|
||||
aria-label={t('operation.more', { ns: 'common' })}
|
||||
className={cn(
|
||||
isOperationsMenuOpen ? 'bg-state-base-hover shadow-none' : 'bg-transparent',
|
||||
'flex h-8 w-8 items-center justify-center rounded-md border-none p-2 hover:bg-state-base-hover focus-visible:bg-state-base-hover focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:ring-inset',
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
}}
|
||||
>
|
||||
<div className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-md">
|
||||
<span className="sr-only">{t('operation.more', { ns: 'common' })}</span>
|
||||
<span aria-hidden className="i-ri-more-fill h-4 w-4 text-text-tertiary" />
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
placement="bottom-end"
|
||||
sideOffset={4}
|
||||
popupClassName={operationsMenuWidthClassName}
|
||||
>
|
||||
{systemFeatures.webapp_auth.enabled
|
||||
? (
|
||||
<AppCardOperationsMenuContent
|
||||
app={app}
|
||||
shouldShowSwitchOption={shouldShowSwitchOption}
|
||||
shouldShowAccessControlOption={shouldShowAccessControlOption}
|
||||
onEdit={handleShowEditModal}
|
||||
onDuplicate={handleShowDuplicateModal}
|
||||
onExport={exportCheck}
|
||||
onSwitch={handleShowSwitchModal}
|
||||
onDelete={handleShowDeleteConfirm}
|
||||
onAccessControl={handleShowAccessControl}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<AppCardOperationsMenu
|
||||
app={app}
|
||||
shouldShowSwitchOption={shouldShowSwitchOption}
|
||||
shouldShowOpenInExploreOption={!app.has_draft_trigger}
|
||||
shouldShowAccessControlOption={shouldShowAccessControlOption}
|
||||
onEdit={handleShowEditModal}
|
||||
onDuplicate={handleShowDuplicateModal}
|
||||
onExport={exportCheck}
|
||||
onSwitch={handleShowSwitchModal}
|
||||
onDelete={handleShowDeleteConfirm}
|
||||
onAccessControl={handleShowAccessControl}
|
||||
/>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</>
|
||||
<div
|
||||
className={cn(
|
||||
'absolute top-1/2 right-1.5 flex -translate-y-1/2 items-center transition-opacity',
|
||||
isOperationsMenuOpen
|
||||
? 'pointer-events-auto opacity-100'
|
||||
: 'pointer-events-none opacity-0 group-focus-within:pointer-events-auto group-focus-within:opacity-100 group-hover:pointer-events-auto group-hover:opacity-100',
|
||||
)}
|
||||
>
|
||||
<div className="mx-1 h-3.5 w-px shrink-0 bg-divider-regular" />
|
||||
<DropdownMenu modal={false} open={isOperationsMenuOpen} onOpenChange={setIsOperationsMenuOpen}>
|
||||
<DropdownMenuTrigger
|
||||
aria-label={t('operation.more', { ns: 'common' })}
|
||||
className={cn(
|
||||
isOperationsMenuOpen ? 'bg-state-base-hover shadow-none' : 'bg-transparent',
|
||||
'flex h-8 w-8 items-center justify-center rounded-md border-none p-2 hover:bg-state-base-hover focus-visible:bg-state-base-hover focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:ring-inset',
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
}}
|
||||
>
|
||||
<div className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-md">
|
||||
<span className="sr-only">{t('operation.more', { ns: 'common' })}</span>
|
||||
<span aria-hidden className="i-ri-more-fill h-4 w-4 text-text-tertiary" />
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
placement="bottom-end"
|
||||
sideOffset={4}
|
||||
popupClassName={operationsMenuWidthClassName}
|
||||
>
|
||||
{systemFeatures.webapp_auth.enabled
|
||||
? (
|
||||
<AppCardOperationsMenuContent
|
||||
app={app}
|
||||
shouldShowSwitchOption={shouldShowSwitchOption}
|
||||
shouldShowAccessControlOption={shouldShowAccessControlOption}
|
||||
onEdit={handleShowEditModal}
|
||||
onDuplicate={handleShowDuplicateModal}
|
||||
onExport={exportCheck}
|
||||
onSwitch={handleShowSwitchModal}
|
||||
onDelete={handleShowDeleteConfirm}
|
||||
onAccessControl={handleShowAccessControl}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<AppCardOperationsMenu
|
||||
app={app}
|
||||
shouldShowSwitchOption={shouldShowSwitchOption}
|
||||
shouldShowOpenInExploreOption={!app.has_draft_trigger}
|
||||
shouldShowAccessControlOption={shouldShowAccessControlOption}
|
||||
onEdit={handleShowEditModal}
|
||||
onDuplicate={handleShowDuplicateModal}
|
||||
onExport={exportCheck}
|
||||
onSwitch={handleShowSwitchModal}
|
||||
onDelete={handleShowDeleteConfirm}
|
||||
onAccessControl={handleShowAccessControl}
|
||||
/>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -18,6 +18,7 @@ import dynamic from '@/next/dynamic'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { hasPermission } from '@/utils/permission'
|
||||
import AppCard from './app-card'
|
||||
import { AppCardSkeleton } from './app-card-skeleton'
|
||||
import { APP_LIST_SEARCH_DEBOUNCE_MS } from './constants'
|
||||
@ -43,7 +44,9 @@ const List: FC<Props> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||
const { isCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator, isLoadingCurrentWorkspace } = useAppContext()
|
||||
const { isLoadingCurrentWorkspace, workspacePermissionKeys } = useAppContext()
|
||||
const canCreateApp = hasPermission(workspacePermissionKeys, 'app.create')
|
||||
const canAccessAppList = hasPermission(workspacePermissionKeys, 'app_library.access')
|
||||
|
||||
// eslint-disable-next-line react/use-state -- custom URL query hook, not React.useState
|
||||
const {
|
||||
@ -68,7 +71,7 @@ const List: FC<Props> = ({
|
||||
const { dragging } = useDSLDragDrop({
|
||||
onDSLFileDropped: handleDSLFileDropped,
|
||||
containerRef,
|
||||
enabled: isCurrentWorkspaceEditor,
|
||||
enabled: canCreateApp,
|
||||
})
|
||||
|
||||
const appListQuery = useMemo<AppListQuery>(() => ({
|
||||
@ -101,7 +104,7 @@ const List: FC<Props> = ({
|
||||
initialPageParam: 1,
|
||||
placeholderData: keepPreviousData,
|
||||
}),
|
||||
enabled: !isCurrentWorkspaceDatasetOperator,
|
||||
enabled: canAccessAppList,
|
||||
refetchInterval: systemFeatures.enable_collaboration_mode ? 10000 : false,
|
||||
})
|
||||
|
||||
@ -113,12 +116,12 @@ const List: FC<Props> = ({
|
||||
|
||||
const anchorRef = useRef<HTMLDivElement>(null)
|
||||
const options = [
|
||||
{ value: 'all', text: t('types.all', { ns: 'app' }), icon: <span className="mr-1 i-ri-apps-2-line h-[14px] w-[14px]" /> },
|
||||
{ value: AppModeEnum.WORKFLOW, text: t('types.workflow', { ns: 'app' }), icon: <span className="mr-1 i-ri-exchange-2-line h-[14px] w-[14px]" /> },
|
||||
{ value: AppModeEnum.ADVANCED_CHAT, text: t('types.advanced', { ns: 'app' }), icon: <span className="mr-1 i-ri-message-3-line h-[14px] w-[14px]" /> },
|
||||
{ value: AppModeEnum.CHAT, text: t('types.chatbot', { ns: 'app' }), icon: <span className="mr-1 i-ri-message-3-line h-[14px] w-[14px]" /> },
|
||||
{ value: AppModeEnum.AGENT_CHAT, text: t('types.agent', { ns: 'app' }), icon: <span className="mr-1 i-ri-robot-3-line h-[14px] w-[14px]" /> },
|
||||
{ value: AppModeEnum.COMPLETION, text: t('types.completion', { ns: 'app' }), icon: <span className="mr-1 i-ri-file-4-line h-[14px] w-[14px]" /> },
|
||||
{ value: 'all', text: t('types.all', { ns: 'app' }), icon: <span className="mr-1 i-ri-apps-2-line size-3.5" /> },
|
||||
{ value: AppModeEnum.WORKFLOW, text: t('types.workflow', { ns: 'app' }), icon: <span className="mr-1 i-ri-exchange-2-line size-3.5" /> },
|
||||
{ value: AppModeEnum.ADVANCED_CHAT, text: t('types.advanced', { ns: 'app' }), icon: <span className="mr-1 i-ri-message-3-line size-3.5" /> },
|
||||
{ value: AppModeEnum.CHAT, text: t('types.chatbot', { ns: 'app' }), icon: <span className="mr-1 i-ri-message-3-line size-3.5" /> },
|
||||
{ value: AppModeEnum.AGENT_CHAT, text: t('types.agent', { ns: 'app' }), icon: <span className="mr-1 i-ri-robot-3-line size-3.5" /> },
|
||||
{ value: AppModeEnum.COMPLETION, text: t('types.completion', { ns: 'app' }), icon: <span className="mr-1 i-ri-file-4-line size-3.5" /> },
|
||||
]
|
||||
|
||||
useEffect(() => {
|
||||
@ -129,7 +132,7 @@ const List: FC<Props> = ({
|
||||
}, [refetch])
|
||||
|
||||
useEffect(() => {
|
||||
if (isCurrentWorkspaceDatasetOperator)
|
||||
if (!canAccessAppList)
|
||||
return
|
||||
const hasMore = hasNextPage ?? true
|
||||
let observer: IntersectionObserver | undefined
|
||||
@ -156,7 +159,7 @@ const List: FC<Props> = ({
|
||||
observer.observe(anchorRef.current)
|
||||
}
|
||||
return () => observer?.disconnect()
|
||||
}, [isLoading, isFetchingNextPage, fetchNextPage, error, hasNextPage, isCurrentWorkspaceDatasetOperator])
|
||||
}, [isLoading, isFetchingNextPage, fetchNextPage, error, hasNextPage, canAccessAppList])
|
||||
|
||||
const handleCreatedByMeChange = useCallback(() => {
|
||||
setIsCreatedByMe(!isCreatedByMe)
|
||||
@ -225,7 +228,7 @@ const List: FC<Props> = ({
|
||||
!hasAnyApp && 'overflow-hidden',
|
||||
)}
|
||||
>
|
||||
{(isCurrentWorkspaceEditor || isLoadingCurrentWorkspace) && (
|
||||
{(canCreateApp || isLoadingCurrentWorkspace) && (
|
||||
<NewAppCard
|
||||
ref={newAppCardRef}
|
||||
isLoading={isLoadingCurrentWorkspace}
|
||||
@ -252,7 +255,7 @@ const List: FC<Props> = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isCurrentWorkspaceEditor && (
|
||||
{canCreateApp && (
|
||||
<div
|
||||
className={`flex items-center justify-center gap-2 py-4 ${dragging ? 'text-text-accent' : 'text-text-quaternary'}`}
|
||||
role="region"
|
||||
|
||||
@ -6,7 +6,6 @@ import { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContextSelector } from 'use-context-selector'
|
||||
import { CreateFromDSLModalTab } from '@/app/components/app/create-from-dsl-modal'
|
||||
import { FileArrow01, FilePlus01, FilePlus02 } from '@/app/components/base/icons/src/vender/line/files'
|
||||
import AppListContext from '@/context/app-list-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import dynamic from '@/next/dynamic'
|
||||
@ -60,7 +59,6 @@ const CreateAppCard = ({
|
||||
const controlHideCreateFromTemplatePanel = useContextSelector(AppListContext, ctx => ctx.controlHideCreateFromTemplatePanel)
|
||||
useEffect(() => {
|
||||
if (controlHideCreateFromTemplatePanel > 0)
|
||||
// eslint-disable-next-line react-hooks-extra/no-direct-set-state-in-use-effect
|
||||
setShowNewAppTemplateDialog(false)
|
||||
}, [controlHideCreateFromTemplatePanel])
|
||||
|
||||
@ -68,27 +66,27 @@ const CreateAppCard = ({
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative col-span-1 inline-flex h-[160px] flex-col justify-between rounded-xl border-[0.5px] border-components-card-border bg-components-card-bg transition-opacity',
|
||||
'relative col-span-1 inline-flex h-40 flex-col justify-between rounded-xl border-[0.5px] border-components-card-border bg-components-card-bg transition-opacity',
|
||||
isLoading && 'pointer-events-none opacity-50',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="grow rounded-t-xl p-2">
|
||||
<div className="px-6 pt-2 pb-1 text-xs leading-[18px] font-medium text-text-tertiary">{t('createApp', { ns: 'app' })}</div>
|
||||
<button type="button" className="mb-1 flex w-full cursor-pointer items-center rounded-lg px-6 py-[7px] text-[13px] leading-[18px] font-medium text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary" onClick={() => setShowNewAppModal(true)}>
|
||||
<FilePlus01 className="mr-2 h-4 w-4 shrink-0" />
|
||||
<div className="px-6 pt-2 pb-1 text-xs leading-4.5 font-medium text-text-tertiary">{t('createApp', { ns: 'app' })}</div>
|
||||
<button type="button" className="mb-1 flex w-full cursor-pointer items-center rounded-lg px-6 py-1.75 text-[13px] leading-4.5 font-medium text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary" onClick={() => setShowNewAppModal(true)}>
|
||||
<span className="mr-2 i-custom-vender-line-files-file-plus-01 h-4 w-4 shrink-0" />
|
||||
{t('newApp.startFromBlank', { ns: 'app' })}
|
||||
</button>
|
||||
<button type="button" className="flex w-full cursor-pointer items-center rounded-lg px-6 py-[7px] text-[13px] leading-[18px] font-medium text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary" onClick={() => setShowNewAppTemplateDialog(true)}>
|
||||
<FilePlus02 className="mr-2 h-4 w-4 shrink-0" />
|
||||
<button type="button" className="flex w-full cursor-pointer items-center rounded-lg px-6 py-1.75 text-[13px] leading-4.5 font-medium text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary" onClick={() => setShowNewAppTemplateDialog(true)}>
|
||||
<span className="mr-2 i-custom-vender-line-files-file-plus-02 h-4 w-4 shrink-0" />
|
||||
{t('newApp.startFromTemplate', { ns: 'app' })}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowCreateFromDSLModal(true)}
|
||||
className="flex w-full cursor-pointer items-center rounded-lg px-6 py-[7px] text-[13px] leading-[18px] font-medium text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary"
|
||||
className="flex w-full cursor-pointer items-center rounded-lg px-6 py-1.75 text-[13px] leading-4.5 font-medium text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary"
|
||||
>
|
||||
<FileArrow01 className="mr-2 h-4 w-4 shrink-0" />
|
||||
<span className="mr-2 i-custom-vender-line-files-file-arrow-01 h-4 w-4 shrink-0" />
|
||||
{t('importDSL', { ns: 'app' })}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -6,6 +6,8 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@langgenius/dify-ui/dropdown-menu'
|
||||
import * as React from 'react'
|
||||
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
|
||||
import { hasPermission } from '@/utils/permission'
|
||||
import Operations from '../operations'
|
||||
|
||||
type OperationsDropdownProps = {
|
||||
@ -26,6 +28,8 @@ const OperationsDropdown = ({
|
||||
openAccessConfig,
|
||||
}: OperationsDropdownProps) => {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys)
|
||||
const canManageAccessConfig = hasPermission(workspacePermissionKeys, 'dataset.access_config')
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -57,6 +61,7 @@ const OperationsDropdown = ({
|
||||
<Operations
|
||||
showDelete={!isCurrentWorkspaceDatasetOperator}
|
||||
showExportPipeline={dataset.runtime_mode === 'rag_pipeline'}
|
||||
showAccessConfig={canManageAccessConfig}
|
||||
openRenameModal={openRenameModal}
|
||||
handleExportPipeline={handleExportPipeline}
|
||||
detectIsUsedByApp={detectIsUsedByApp}
|
||||
|
||||
@ -65,7 +65,7 @@ const DatasetCard = ({
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="group relative col-span-1 flex h-[190px] cursor-pointer flex-col rounded-xl border-[0.5px] border-solid border-components-card-border bg-components-card-bg shadow-xs shadow-shadow-shadow-3 transition-all duration-200 ease-in-out hover:bg-components-card-bg-alt hover:shadow-md hover:shadow-shadow-shadow-5"
|
||||
className="group relative col-span-1 flex h-47.5 cursor-pointer flex-col rounded-xl border-[0.5px] border-solid border-components-card-border bg-components-card-bg shadow-xs shadow-shadow-shadow-3 transition-all duration-200 ease-in-out hover:bg-components-card-bg-alt hover:shadow-md hover:shadow-shadow-shadow-5"
|
||||
data-disable-nprogress={true}
|
||||
onClick={handleCardClick}
|
||||
>
|
||||
|
||||
@ -8,6 +8,7 @@ import { useTranslation } from 'react-i18next'
|
||||
type OperationsProps = {
|
||||
showDelete: boolean
|
||||
showExportPipeline: boolean
|
||||
showAccessConfig?: boolean
|
||||
openRenameModal: () => void
|
||||
handleExportPipeline: () => void
|
||||
detectIsUsedByApp: () => void
|
||||
@ -18,6 +19,7 @@ type OperationsProps = {
|
||||
const Operations = ({
|
||||
showDelete,
|
||||
showExportPipeline,
|
||||
showAccessConfig = false,
|
||||
openRenameModal,
|
||||
handleExportPipeline,
|
||||
detectIsUsedByApp,
|
||||
@ -58,10 +60,12 @@ const Operations = ({
|
||||
{t('operations.exportPipeline', { ns: 'datasetPipeline' })}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem onClick={handleAccessConfig}>
|
||||
<span aria-hidden className="mr-1 i-ri-user-settings-line size-4 text-text-tertiary" />
|
||||
Access Config
|
||||
</DropdownMenuItem>
|
||||
{showAccessConfig && (
|
||||
<DropdownMenuItem onClick={handleAccessConfig}>
|
||||
<span aria-hidden className="mr-1 i-ri-user-settings-line size-4 text-text-tertiary" />
|
||||
Access Config
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{showDelete && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
@ -3,7 +3,6 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
|
||||
import { useDatasetList, useInvalidDatasetList } from '@/service/knowledge/use-dataset'
|
||||
import DatasetCard from './dataset-card'
|
||||
import NewDatasetCard from './new-dataset-card'
|
||||
@ -22,7 +21,6 @@ const Datasets = ({
|
||||
onOpenTagManagement = () => {},
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const isCurrentWorkspaceEditor = useAppContextWithSelector(state => state.isCurrentWorkspaceEditor)
|
||||
const {
|
||||
data: datasetList,
|
||||
fetchNextPage,
|
||||
@ -60,7 +58,7 @@ const Datasets = ({
|
||||
return (
|
||||
<>
|
||||
<nav className="grid grow grid-cols-1 content-start gap-3 px-12 pt-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
|
||||
{isCurrentWorkspaceEditor && <NewDatasetCard />}
|
||||
<NewDatasetCard />
|
||||
{datasetList?.pages.map(({ data: datasets }) => datasets.map(dataset => (
|
||||
<DatasetCard key={dataset.id} dataset={dataset} onSuccess={invalidDatasetList} onOpenTagManagement={onOpenTagManagement} />),
|
||||
))}
|
||||
|
||||
@ -16,6 +16,7 @@ import { TagManagementModal } from '@/features/tag-management/components/tag-man
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import { useDatasetApiBaseUrl, useInvalidDatasetList } from '@/service/knowledge/use-dataset'
|
||||
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||
import { hasPermission } from '@/utils/permission'
|
||||
// Components
|
||||
import ExternalAPIPanel from '../external-api/external-api-panel'
|
||||
import ServiceApi from '../extra-info/service-api'
|
||||
@ -52,7 +53,9 @@ const List = () => {
|
||||
}
|
||||
|
||||
const isCurrentWorkspaceManager = useAppContextSelector(state => state.isCurrentWorkspaceManager)
|
||||
const workspacePermissionKeys = useAppContextSelector(state => state.workspacePermissionKeys)
|
||||
const { data: apiBaseInfo } = useDatasetApiBaseUrl()
|
||||
const canConnectExternalDataset = hasPermission(workspacePermissionKeys, 'dataset.external.connect')
|
||||
|
||||
return (
|
||||
<div className="relative flex grow flex-col overflow-y-auto bg-background-body">
|
||||
@ -82,14 +85,18 @@ const List = () => {
|
||||
<ServiceApi apiBaseUrl={apiBaseInfo?.api_base_url ?? ''} />
|
||||
)
|
||||
}
|
||||
<div className="h-4 w-px bg-divider-regular" />
|
||||
<Button
|
||||
className="gap-0.5 shadow-xs"
|
||||
onClick={() => setShowExternalApiPanel(true)}
|
||||
>
|
||||
<span className="i-custom-vender-solid-development-api-connection-mod h-4 w-4 text-components-button-secondary-text" />
|
||||
<span className="flex items-center justify-center gap-1 px-0.5 system-sm-medium text-components-button-secondary-text">{t('externalAPIPanelTitle', { ns: 'dataset' })}</span>
|
||||
</Button>
|
||||
{canConnectExternalDataset && (
|
||||
<>
|
||||
<div className="h-4 w-px bg-divider-regular" />
|
||||
<Button
|
||||
className="gap-0.5 shadow-xs"
|
||||
onClick={() => setShowExternalApiPanel(true)}
|
||||
>
|
||||
<span className="i-custom-vender-solid-development-api-connection-mod h-4 w-4 text-components-button-secondary-text" />
|
||||
<span className="flex items-center justify-center gap-1 px-0.5 system-sm-medium text-components-button-secondary-text">{t('externalAPIPanelTitle', { ns: 'dataset' })}</span>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Datasets tags={tagIDs} keywords={searchKeywords} includeAll={includeAll} onOpenTagManagement={() => setShowTagManagementModal(true)} />
|
||||
|
||||
@ -6,20 +6,27 @@ import {
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ApiConnectionMod } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
|
||||
import { hasPermission } from '@/utils/permission'
|
||||
import Option from './option'
|
||||
|
||||
const CreateAppCard = () => {
|
||||
const { t } = useTranslation()
|
||||
const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys)
|
||||
const canAddDataset = hasPermission(workspacePermissionKeys, 'dataset.create')
|
||||
const canConnectExternalDataset = hasPermission(workspacePermissionKeys, 'dataset.external.connect')
|
||||
|
||||
return (
|
||||
<div className="flex h-[190px] flex-col gap-y-0.5 rounded-xl bg-background-default-dimmed">
|
||||
<div className="flex h-47.5 flex-col gap-y-0.5 rounded-xl bg-background-default-dimmed">
|
||||
<div className="flex grow flex-col items-center justify-center p-2">
|
||||
<Option
|
||||
disabled={!canAddDataset}
|
||||
href="/datasets/create"
|
||||
Icon={RiAddLine}
|
||||
text={t('createDataset', { ns: 'dataset' })}
|
||||
/>
|
||||
<Option
|
||||
disabled={!canAddDataset}
|
||||
href="/datasets/create-from-pipeline"
|
||||
Icon={RiFunctionAddLine}
|
||||
text={t('createFromPipeline', { ns: 'dataset' })}
|
||||
@ -27,6 +34,7 @@ const CreateAppCard = () => {
|
||||
</div>
|
||||
<div className="border-t-[0.5px] border-divider-subtle p-2">
|
||||
<Option
|
||||
disabled={!canConnectExternalDataset}
|
||||
href="/datasets/connect"
|
||||
Icon={ApiConnectionMod}
|
||||
text={t('connectDataset', { ns: 'dataset' })}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import * as React from 'react'
|
||||
import Link from '@/next/link'
|
||||
|
||||
@ -5,13 +6,28 @@ type OptionProps = {
|
||||
Icon: React.ComponentType<{ className?: string }>
|
||||
text: string
|
||||
href: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const Option = ({
|
||||
Icon,
|
||||
text,
|
||||
href,
|
||||
disabled = false,
|
||||
}: OptionProps) => {
|
||||
if (disabled) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex w-full cursor-not-allowed items-center gap-x-2 rounded-lg bg-transparent px-4 py-2 text-text-tertiary opacity-50 shadow-shadow-shadow-3',
|
||||
)}
|
||||
>
|
||||
<Icon className="h-4 w-4 shrink-0" />
|
||||
<span className="grow text-left system-sm-medium">{text}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
type="button"
|
||||
|
||||
@ -16,11 +16,14 @@ import LabelFilter from '@/app/components/tools/labels/filter'
|
||||
import CustomCreateCard from '@/app/components/tools/provider/custom-create-card'
|
||||
import ProviderDetail from '@/app/components/tools/provider/detail'
|
||||
import WorkflowToolEmpty from '@/app/components/tools/provider/empty'
|
||||
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
|
||||
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||
import { useCheckInstalled, useInvalidateInstalledPluginList } from '@/service/use-plugins'
|
||||
import { useAllToolProviders } from '@/service/use-tools'
|
||||
import { hasPermission } from '@/utils/permission'
|
||||
import Marketplace from './marketplace'
|
||||
import { useMarketplace } from './marketplace/hooks'
|
||||
|
||||
import MCPList from './mcp'
|
||||
import { getToolType } from './utils'
|
||||
|
||||
@ -44,14 +47,21 @@ const ProviderList = () => {
|
||||
...systemFeaturesQueryOptions(),
|
||||
select: s => s.enable_marketplace,
|
||||
})
|
||||
const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const [activeTab, setActiveTab] = useQueryState('category', parseAsToolProviderCategory)
|
||||
const canManageCustomAndWorkflow = hasPermission(workspacePermissionKeys, 'tool.manage')
|
||||
const canManageMCP = hasPermission(workspacePermissionKeys, 'mcp.manage')
|
||||
const options = [
|
||||
{ value: 'builtin', text: t('type.builtIn', { ns: 'tools' }) },
|
||||
{ value: 'api', text: t('type.custom', { ns: 'tools' }) },
|
||||
{ value: 'workflow', text: t('type.workflow', { ns: 'tools' }) },
|
||||
{ value: 'mcp', text: 'MCP' },
|
||||
...(canManageCustomAndWorkflow
|
||||
? [
|
||||
{ value: 'api', text: t('type.custom', { ns: 'tools' }) },
|
||||
{ value: 'workflow', text: t('type.workflow', { ns: 'tools' }) },
|
||||
]
|
||||
: []),
|
||||
...(canManageMCP ? [{ value: 'mcp', text: 'MCP' }] : []),
|
||||
]
|
||||
const [tagFilterValue, setTagFilterValue] = useState<string[]>([])
|
||||
const handleTagsChange = (value: string[]) => {
|
||||
@ -130,7 +140,7 @@ const ProviderList = () => {
|
||||
className="relative flex grow flex-col overflow-y-auto bg-background-body"
|
||||
>
|
||||
<div className={cn(
|
||||
'sticky top-0 z-10 flex flex-wrap items-center justify-between gap-y-2 bg-background-body px-12 pt-4 pb-2 leading-[56px]',
|
||||
'sticky top-0 z-10 flex flex-wrap items-center justify-between gap-y-2 bg-background-body px-12 pt-4 pb-2 leading-14',
|
||||
currentProviderId && 'pr-6',
|
||||
)}
|
||||
>
|
||||
|
||||
@ -3,6 +3,8 @@ import type { TagType } from '@/contract/console/tags'
|
||||
import { ComboboxInput, ComboboxInputGroup, ComboboxItem, ComboboxItemIndicator, ComboboxItemText, ComboboxList, ComboboxSeparator, useComboboxFilteredItems } from '@langgenius/dify-ui/combobox'
|
||||
import { Fragment } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
|
||||
import { hasPermission } from '@/utils/permission'
|
||||
import { isCreateTagOption } from './tag-combobox-item'
|
||||
|
||||
type TagPanelProps = {
|
||||
@ -26,6 +28,12 @@ export const TagPanel = ({
|
||||
const hasCreateOption = filteredItems.some(isCreateTagOption)
|
||||
const placeholder = t('tag.selectorPlaceholder', { ns: 'common' }) || ''
|
||||
|
||||
const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys)
|
||||
const canManageTags
|
||||
= type === 'app'
|
||||
? hasPermission(workspacePermissionKeys, 'app.tag.manage')
|
||||
: hasPermission(workspacePermissionKeys, 'dataset.tag.manage')
|
||||
|
||||
return (
|
||||
<div className="relative w-full">
|
||||
<div className="p-2 pb-1">
|
||||
@ -53,7 +61,7 @@ export const TagPanel = ({
|
||||
{filteredItems.length > 0 && (
|
||||
<ComboboxList className="max-h-58">
|
||||
{(tag: TagComboboxItem) => {
|
||||
if (isCreateTagOption(tag)) {
|
||||
if (isCreateTagOption(tag) && canManageTags) {
|
||||
return (
|
||||
<Fragment key={tag.id}>
|
||||
<ComboboxItem
|
||||
@ -89,22 +97,26 @@ export const TagPanel = ({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<ComboboxSeparator />
|
||||
<div className="p-1">
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full cursor-pointer touch-manipulation items-center gap-x-1 rounded-lg px-2 py-1.5 text-left outline-hidden hover:bg-state-base-hover focus-visible:ring-1 focus-visible:ring-components-input-border-active"
|
||||
onClick={() => {
|
||||
onOpenTagManagement?.()
|
||||
onClose?.()
|
||||
}}
|
||||
>
|
||||
<span aria-hidden="true" className="i-ri-price-tag-3-line h-4 w-4 text-text-tertiary" />
|
||||
<span className="min-w-0 grow truncate px-1 system-md-regular text-text-secondary">
|
||||
{t('tag.manageTags', { ns: 'common' })}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
{canManageTags && (
|
||||
<>
|
||||
<ComboboxSeparator />
|
||||
<div className="p-1">
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full cursor-pointer touch-manipulation items-center gap-x-1 rounded-lg px-2 py-1.5 text-left outline-hidden hover:bg-state-base-hover focus-visible:ring-1 focus-visible:ring-components-input-border-active"
|
||||
onClick={() => {
|
||||
onOpenTagManagement?.()
|
||||
onClose?.()
|
||||
}}
|
||||
>
|
||||
<span aria-hidden="true" className="i-ri-price-tag-3-line h-4 w-4 text-text-tertiary" />
|
||||
<span className="min-w-0 grow truncate px-1 system-md-regular text-text-secondary">
|
||||
{t('tag.manageTags', { ns: 'common' })}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -8,7 +8,9 @@ import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useMutation, useQuery } from '@tanstack/react-query'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { hasPermission } from '@/utils/permission'
|
||||
import { useApplyTagBindingsMutation } from '../hooks/use-tag-mutations'
|
||||
import { isCreateTagOption } from './tag-combobox-item'
|
||||
import { TagPanel } from './tag-panel'
|
||||
@ -66,6 +68,8 @@ export const TagSelector = ({
|
||||
const [open, setOpen] = useState(false)
|
||||
const [draftTags, setDraftTags] = useState<Tag[]>(value)
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys)
|
||||
const canManageTags = type === 'app' ? hasPermission(workspacePermissionKeys, 'app.tag.manage') : hasPermission(workspacePermissionKeys, 'dataset.tag.manage')
|
||||
const applyTagBindingsMutation = useApplyTagBindingsMutation()
|
||||
const {
|
||||
isPending: isCreatingTag,
|
||||
@ -212,6 +216,7 @@ export const TagSelector = ({
|
||||
isItemEqualToValue={isSameTag}
|
||||
>
|
||||
<ComboboxTrigger
|
||||
disabled={!canManageTags}
|
||||
aria-label={triggerLabel}
|
||||
className={cn(
|
||||
open ? 'bg-state-base-hover' : 'bg-transparent',
|
||||
|
||||
Reference in New Issue
Block a user