mirror of https://github.com/langgenius/dify.git
Merge branch 'langgenius:main' into bugfix_workflow_skip
This commit is contained in:
commit
6b9dd4063a
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"enabledPlugins": {
|
||||
"feature-dev@claude-plugins-official": true,
|
||||
"context7@claude-plugins-official": true,
|
||||
"typescript-lsp@claude-plugins-official": true,
|
||||
"pyright-lsp@claude-plugins-official": true
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
{
|
||||
"permissions": {
|
||||
"allow": [],
|
||||
"deny": []
|
||||
},
|
||||
"env": {
|
||||
"__comment": "Environment variables for MCP servers. Override in .claude/settings.local.json with actual values.",
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
"enabledMcpjsonServers": [
|
||||
"context7",
|
||||
"sequential-thinking",
|
||||
"github",
|
||||
"fetch",
|
||||
"playwright",
|
||||
"ide"
|
||||
],
|
||||
"enableAllProjectMcpServers": true
|
||||
}
|
||||
34
.mcp.json
34
.mcp.json
|
|
@ -1,34 +0,0 @@
|
|||
{
|
||||
"mcpServers": {
|
||||
"context7": {
|
||||
"type": "http",
|
||||
"url": "https://mcp.context7.com/mcp"
|
||||
},
|
||||
"sequential-thinking": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-sequential-thinking"],
|
||||
"env": {}
|
||||
},
|
||||
"github": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-github"],
|
||||
"env": {
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_PERSONAL_ACCESS_TOKEN}"
|
||||
}
|
||||
},
|
||||
"fetch": {
|
||||
"type": "stdio",
|
||||
"command": "uvx",
|
||||
"args": ["mcp-server-fetch"],
|
||||
"env": {}
|
||||
},
|
||||
"playwright": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@playwright/mcp@latest"],
|
||||
"env": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -313,6 +313,9 @@ class StreamableHTTPTransport:
|
|||
if is_initialization:
|
||||
self._maybe_extract_session_id_from_response(response)
|
||||
|
||||
# Per https://modelcontextprotocol.io/specification/2025-06-18/basic#notifications:
|
||||
# The server MUST NOT send a response to notifications.
|
||||
if isinstance(message.root, JSONRPCRequest):
|
||||
content_type = cast(str, response.headers.get(CONTENT_TYPE, "").lower())
|
||||
|
||||
if content_type.startswith(JSON):
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ from enums.quota_type import QuotaType, unlimited
|
|||
from extensions.otel import AppGenerateHandler, trace_span
|
||||
from models.model import Account, App, AppMode, EndUser
|
||||
from models.workflow import Workflow
|
||||
from services.errors.app import InvokeRateLimitError, QuotaExceededError, WorkflowIdFormatError, WorkflowNotFoundError
|
||||
from services.errors.app import QuotaExceededError, WorkflowIdFormatError, WorkflowNotFoundError
|
||||
from services.errors.llm import InvokeRateLimitError
|
||||
from services.workflow_service import WorkflowService
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ from models.model import App, EndUser
|
|||
from models.trigger import WorkflowTriggerLog
|
||||
from models.workflow import Workflow
|
||||
from repositories.sqlalchemy_workflow_trigger_log_repository import SQLAlchemyWorkflowTriggerLogRepository
|
||||
from services.errors.app import InvokeRateLimitError, QuotaExceededError, WorkflowNotFoundError
|
||||
from services.errors.app import QuotaExceededError, WorkflowNotFoundError, WorkflowQuotaLimitError
|
||||
from services.workflow.entities import AsyncTriggerResponse, TriggerData, WorkflowTaskData
|
||||
from services.workflow.queue_dispatcher import QueueDispatcherManager, QueuePriority
|
||||
from services.workflow_service import WorkflowService
|
||||
|
|
@ -141,7 +141,7 @@ class AsyncWorkflowService:
|
|||
trigger_log_repo.update(trigger_log)
|
||||
session.commit()
|
||||
|
||||
raise InvokeRateLimitError(
|
||||
raise WorkflowQuotaLimitError(
|
||||
f"Workflow execution quota limit reached for tenant {trigger_data.tenant_id}"
|
||||
) from e
|
||||
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ class WorkflowIdFormatError(Exception):
|
|||
pass
|
||||
|
||||
|
||||
class InvokeRateLimitError(Exception):
|
||||
"""Raised when rate limit is exceeded for workflow invocations."""
|
||||
class WorkflowQuotaLimitError(Exception):
|
||||
"""Raised when workflow execution quota is exceeded (for async/background workflows)."""
|
||||
|
||||
pass
|
||||
|
||||
|
|
|
|||
|
|
@ -863,10 +863,18 @@ class WebhookService:
|
|||
not_found_in_cache.append(node_id)
|
||||
continue
|
||||
|
||||
with Session(db.engine) as session:
|
||||
lock_key = f"{cls.__WEBHOOK_NODE_CACHE_KEY__}:apps:{app.id}:lock"
|
||||
lock = redis_client.lock(lock_key, timeout=10)
|
||||
lock_acquired = False
|
||||
|
||||
try:
|
||||
# lock the concurrent webhook trigger creation
|
||||
redis_client.lock(f"{cls.__WEBHOOK_NODE_CACHE_KEY__}:apps:{app.id}:lock", timeout=10)
|
||||
# acquire the lock with blocking and timeout
|
||||
lock_acquired = lock.acquire(blocking=True, blocking_timeout=10)
|
||||
if not lock_acquired:
|
||||
logger.warning("Failed to acquire lock for webhook sync, app %s", app.id)
|
||||
raise RuntimeError("Failed to acquire lock for webhook trigger synchronization")
|
||||
|
||||
with Session(db.engine) as session:
|
||||
# fetch the non-cached nodes from DB
|
||||
all_records = session.scalars(
|
||||
select(WorkflowWebhookTrigger).where(
|
||||
|
|
@ -907,7 +915,12 @@ class WebhookService:
|
|||
logger.exception("Failed to sync webhook relationships for app %s", app.id)
|
||||
raise
|
||||
finally:
|
||||
redis_client.delete(f"{cls.__WEBHOOK_NODE_CACHE_KEY__}:apps:{app.id}:lock")
|
||||
# release the lock only if it was acquired
|
||||
if lock_acquired:
|
||||
try:
|
||||
lock.release()
|
||||
except Exception:
|
||||
logger.exception("Failed to release lock for webhook sync, app %s", app.id)
|
||||
|
||||
@classmethod
|
||||
def generate_webhook_id(cls) -> str:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,308 @@
|
|||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import Avatar from './index'
|
||||
|
||||
describe('Avatar', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// Rendering tests - verify component renders correctly in different states
|
||||
describe('Rendering', () => {
|
||||
it('should render img element with correct alt and src when avatar URL is provided', () => {
|
||||
const avatarUrl = 'https://example.com/avatar.jpg'
|
||||
const props = { name: 'John Doe', avatar: avatarUrl }
|
||||
|
||||
render(<Avatar {...props} />)
|
||||
|
||||
const img = screen.getByRole('img', { name: 'John Doe' })
|
||||
expect(img).toBeInTheDocument()
|
||||
expect(img).toHaveAttribute('src', avatarUrl)
|
||||
})
|
||||
|
||||
it('should render fallback div with uppercase initial when avatar is null', () => {
|
||||
const props = { name: 'alice', avatar: null }
|
||||
|
||||
render(<Avatar {...props} />)
|
||||
|
||||
expect(screen.queryByRole('img')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('A')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Props tests - verify all props are applied correctly
|
||||
describe('Props', () => {
|
||||
describe('size prop', () => {
|
||||
it.each([
|
||||
{ size: undefined, expected: '30px', label: 'default (30px)' },
|
||||
{ size: 50, expected: '50px', label: 'custom (50px)' },
|
||||
])('should apply $label size to img element', ({ size, expected }) => {
|
||||
const props = { name: 'Test', avatar: 'https://example.com/avatar.jpg', size }
|
||||
|
||||
render(<Avatar {...props} />)
|
||||
|
||||
expect(screen.getByRole('img')).toHaveStyle({
|
||||
width: expected,
|
||||
height: expected,
|
||||
fontSize: expected,
|
||||
lineHeight: expected,
|
||||
})
|
||||
})
|
||||
|
||||
it('should apply size to fallback div when avatar is null', () => {
|
||||
const props = { name: 'Test', avatar: null, size: 40 }
|
||||
|
||||
render(<Avatar {...props} />)
|
||||
|
||||
const textElement = screen.getByText('T')
|
||||
const outerDiv = textElement.parentElement as HTMLElement
|
||||
expect(outerDiv).toHaveStyle({ width: '40px', height: '40px' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('className prop', () => {
|
||||
it('should merge className with default avatar classes on img', () => {
|
||||
const props = {
|
||||
name: 'Test',
|
||||
avatar: 'https://example.com/avatar.jpg',
|
||||
className: 'custom-class',
|
||||
}
|
||||
|
||||
render(<Avatar {...props} />)
|
||||
|
||||
const img = screen.getByRole('img')
|
||||
expect(img).toHaveClass('custom-class')
|
||||
expect(img).toHaveClass('shrink-0', 'flex', 'items-center', 'rounded-full', 'bg-primary-600')
|
||||
})
|
||||
|
||||
it('should merge className with default avatar classes on fallback div', () => {
|
||||
const props = {
|
||||
name: 'Test',
|
||||
avatar: null,
|
||||
className: 'my-custom-class',
|
||||
}
|
||||
|
||||
render(<Avatar {...props} />)
|
||||
|
||||
const textElement = screen.getByText('T')
|
||||
const outerDiv = textElement.parentElement as HTMLElement
|
||||
expect(outerDiv).toHaveClass('my-custom-class')
|
||||
expect(outerDiv).toHaveClass('shrink-0', 'flex', 'items-center', 'rounded-full', 'bg-primary-600')
|
||||
})
|
||||
})
|
||||
|
||||
describe('textClassName prop', () => {
|
||||
it('should apply textClassName to the initial text element', () => {
|
||||
const props = {
|
||||
name: 'Test',
|
||||
avatar: null,
|
||||
textClassName: 'custom-text-class',
|
||||
}
|
||||
|
||||
render(<Avatar {...props} />)
|
||||
|
||||
const textElement = screen.getByText('T')
|
||||
expect(textElement).toHaveClass('custom-text-class')
|
||||
expect(textElement).toHaveClass('scale-[0.4]', 'text-center', 'text-white')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// State Management tests - verify useState and useEffect behavior
|
||||
describe('State Management', () => {
|
||||
it('should switch to fallback when image fails to load', async () => {
|
||||
const props = { name: 'John', avatar: 'https://example.com/broken.jpg' }
|
||||
render(<Avatar {...props} />)
|
||||
const img = screen.getByRole('img')
|
||||
|
||||
fireEvent.error(img)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('img')).not.toBeInTheDocument()
|
||||
})
|
||||
expect(screen.getByText('J')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should reset error state when avatar URL changes', async () => {
|
||||
const initialProps = { name: 'John', avatar: 'https://example.com/broken.jpg' }
|
||||
const { rerender } = render(<Avatar {...initialProps} />)
|
||||
const img = screen.getByRole('img')
|
||||
|
||||
// First, trigger error
|
||||
fireEvent.error(img)
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('img')).not.toBeInTheDocument()
|
||||
})
|
||||
expect(screen.getByText('J')).toBeInTheDocument()
|
||||
|
||||
rerender(<Avatar name="John" avatar="https://example.com/new-avatar.jpg" />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('img')).toBeInTheDocument()
|
||||
})
|
||||
expect(screen.queryByText('J')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not reset error state if avatar becomes null', async () => {
|
||||
const initialProps = { name: 'John', avatar: 'https://example.com/broken.jpg' }
|
||||
const { rerender } = render(<Avatar {...initialProps} />)
|
||||
|
||||
// Trigger error
|
||||
fireEvent.error(screen.getByRole('img'))
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('J')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
rerender(<Avatar name="John" avatar={null} />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('img')).not.toBeInTheDocument()
|
||||
})
|
||||
expect(screen.getByText('J')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Event Handlers tests - verify onError callback behavior
|
||||
describe('Event Handlers', () => {
|
||||
it('should call onError with true when image fails to load', () => {
|
||||
const onErrorMock = vi.fn()
|
||||
const props = {
|
||||
name: 'John',
|
||||
avatar: 'https://example.com/broken.jpg',
|
||||
onError: onErrorMock,
|
||||
}
|
||||
render(<Avatar {...props} />)
|
||||
|
||||
fireEvent.error(screen.getByRole('img'))
|
||||
|
||||
expect(onErrorMock).toHaveBeenCalledTimes(1)
|
||||
expect(onErrorMock).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it('should call onError with false when image loads successfully', () => {
|
||||
const onErrorMock = vi.fn()
|
||||
const props = {
|
||||
name: 'John',
|
||||
avatar: 'https://example.com/avatar.jpg',
|
||||
onError: onErrorMock,
|
||||
}
|
||||
render(<Avatar {...props} />)
|
||||
|
||||
fireEvent.load(screen.getByRole('img'))
|
||||
|
||||
expect(onErrorMock).toHaveBeenCalledTimes(1)
|
||||
expect(onErrorMock).toHaveBeenCalledWith(false)
|
||||
})
|
||||
|
||||
it('should not throw when onError is not provided', async () => {
|
||||
const props = { name: 'John', avatar: 'https://example.com/broken.jpg' }
|
||||
render(<Avatar {...props} />)
|
||||
|
||||
expect(() => fireEvent.error(screen.getByRole('img'))).not.toThrow()
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('J')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Edge Cases tests - verify handling of unusual inputs
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty string name gracefully', () => {
|
||||
const props = { name: '', avatar: null }
|
||||
|
||||
const { container } = render(<Avatar {...props} />)
|
||||
|
||||
// Note: Using querySelector here because empty name produces no visible text,
|
||||
// making semantic queries (getByRole, getByText) impossible
|
||||
const textElement = container.querySelector('.text-white') as HTMLElement
|
||||
expect(textElement).toBeInTheDocument()
|
||||
expect(textElement.textContent).toBe('')
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ name: '中文名', expected: '中', label: 'Chinese characters' },
|
||||
{ name: '123User', expected: '1', label: 'number' },
|
||||
])('should display first character when name starts with $label', ({ name, expected }) => {
|
||||
const props = { name, avatar: null }
|
||||
|
||||
render(<Avatar {...props} />)
|
||||
|
||||
expect(screen.getByText(expected)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle empty string avatar as falsy value', () => {
|
||||
const props = { name: 'Test', avatar: '' as string | null }
|
||||
|
||||
render(<Avatar {...props} />)
|
||||
|
||||
expect(screen.queryByRole('img')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('T')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle undefined className and textClassName', () => {
|
||||
const props = { name: 'Test', avatar: null }
|
||||
|
||||
render(<Avatar {...props} />)
|
||||
|
||||
const textElement = screen.getByText('T')
|
||||
const outerDiv = textElement.parentElement as HTMLElement
|
||||
expect(outerDiv).toHaveClass('shrink-0', 'flex', 'items-center', 'rounded-full', 'bg-primary-600')
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ size: 0, expected: '0px', label: 'zero' },
|
||||
{ size: 1000, expected: '1000px', label: 'very large' },
|
||||
])('should handle $label size value', ({ size, expected }) => {
|
||||
const props = { name: 'Test', avatar: null, size }
|
||||
|
||||
render(<Avatar {...props} />)
|
||||
|
||||
const textElement = screen.getByText('T')
|
||||
const outerDiv = textElement.parentElement as HTMLElement
|
||||
expect(outerDiv).toHaveStyle({ width: expected, height: expected })
|
||||
})
|
||||
})
|
||||
|
||||
// Combined props tests - verify props work together correctly
|
||||
describe('Combined Props', () => {
|
||||
it('should apply all props correctly when used together', () => {
|
||||
const onErrorMock = vi.fn()
|
||||
const props = {
|
||||
name: 'Test User',
|
||||
avatar: 'https://example.com/avatar.jpg',
|
||||
size: 64,
|
||||
className: 'custom-avatar',
|
||||
onError: onErrorMock,
|
||||
}
|
||||
|
||||
render(<Avatar {...props} />)
|
||||
|
||||
const img = screen.getByRole('img')
|
||||
expect(img).toHaveAttribute('alt', 'Test User')
|
||||
expect(img).toHaveAttribute('src', 'https://example.com/avatar.jpg')
|
||||
expect(img).toHaveStyle({ width: '64px', height: '64px' })
|
||||
expect(img).toHaveClass('custom-avatar')
|
||||
|
||||
// Trigger load to verify onError callback
|
||||
fireEvent.load(img)
|
||||
expect(onErrorMock).toHaveBeenCalledWith(false)
|
||||
})
|
||||
|
||||
it('should apply all fallback props correctly when used together', () => {
|
||||
const props = {
|
||||
name: 'Fallback User',
|
||||
avatar: null,
|
||||
size: 48,
|
||||
className: 'fallback-custom',
|
||||
textClassName: 'custom-text-style',
|
||||
}
|
||||
|
||||
render(<Avatar {...props} />)
|
||||
|
||||
const textElement = screen.getByText('F')
|
||||
const outerDiv = textElement.parentElement as HTMLElement
|
||||
expect(outerDiv).toHaveClass('fallback-custom')
|
||||
expect(outerDiv).toHaveStyle({ width: '48px', height: '48px' })
|
||||
expect(textElement).toHaveClass('custom-text-style')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -34,7 +34,6 @@ import Records from './components/records'
|
|||
import ResultItem from './components/result-item'
|
||||
import ResultItemExternal from './components/result-item-external'
|
||||
import ModifyRetrievalModal from './modify-retrieval-modal'
|
||||
import s from './style.module.css'
|
||||
|
||||
const limit = 10
|
||||
|
||||
|
|
@ -115,8 +114,8 @@ const HitTestingPage: FC<Props> = ({ datasetId }: Props) => {
|
|||
}, [isMobile, setShowRightPanel])
|
||||
|
||||
return (
|
||||
<div className={s.container}>
|
||||
<div className="flex flex-col px-6 py-3">
|
||||
<div className="relative flex h-full w-full gap-x-6 overflow-y-auto pl-6">
|
||||
<div className="flex min-w-0 flex-1 flex-col py-3">
|
||||
<div className="mb-4 flex flex-col justify-center">
|
||||
<h1 className="text-base font-semibold text-text-primary">{t('datasetHitTesting.title')}</h1>
|
||||
<p className="mt-0.5 text-[13px] font-normal leading-4 text-text-tertiary">{t('datasetHitTesting.desc')}</p>
|
||||
|
|
@ -161,7 +160,7 @@ const HitTestingPage: FC<Props> = ({ datasetId }: Props) => {
|
|||
onClose={hideRightPanel}
|
||||
footer={null}
|
||||
>
|
||||
<div className="flex flex-col pt-3">
|
||||
<div className="flex min-w-0 flex-1 flex-col pt-3">
|
||||
{isRetrievalLoading
|
||||
? (
|
||||
<div className="flex h-full flex-col rounded-tl-2xl bg-background-body px-4 py-3">
|
||||
|
|
|
|||
|
|
@ -1,43 +0,0 @@
|
|||
.container {
|
||||
@apply flex h-full w-full relative overflow-y-auto;
|
||||
}
|
||||
|
||||
.container>div {
|
||||
@apply flex-1 h-full;
|
||||
}
|
||||
|
||||
.commonIcon {
|
||||
@apply w-3.5 h-3.5 inline-block align-middle;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.app_icon {
|
||||
background-image: url(./assets/grid.svg);
|
||||
}
|
||||
|
||||
.hit_testing_icon {
|
||||
background-image: url(../documents/assets/target.svg);
|
||||
}
|
||||
|
||||
.plugin_icon {
|
||||
background-image: url(./assets/plugin.svg);
|
||||
}
|
||||
|
||||
.cardWrapper {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(284px, auto));
|
||||
grid-gap: 16px;
|
||||
grid-auto-rows: 216px;
|
||||
}
|
||||
|
||||
.clockWrapper {
|
||||
border: 0.5px solid #eaecf5;
|
||||
@apply rounded-lg w-11 h-11 flex justify-center items-center;
|
||||
}
|
||||
|
||||
.clockIcon {
|
||||
mask-image: url(./assets/clock.svg);
|
||||
@apply bg-gray-500;
|
||||
}
|
||||
|
|
@ -48,6 +48,7 @@ const EditCustomCollectionModal: FC<Props> = ({
|
|||
|
||||
const [editFirst, setEditFirst] = useState(!isAdd)
|
||||
const [paramsSchemas, setParamsSchemas] = useState<CustomParamSchema[]>(payload?.tools || [])
|
||||
const [labels, setLabels] = useState<string[]>(payload?.labels || [])
|
||||
const [customCollection, setCustomCollection, getCustomCollection] = useGetState<CustomCollectionBackend>(isAdd
|
||||
? {
|
||||
provider: '',
|
||||
|
|
@ -67,6 +68,15 @@ const EditCustomCollectionModal: FC<Props> = ({
|
|||
|
||||
const originalProvider = isEdit ? payload.provider : ''
|
||||
|
||||
// Sync customCollection state when payload changes
|
||||
useEffect(() => {
|
||||
if (isEdit) {
|
||||
setCustomCollection(payload)
|
||||
setParamsSchemas(payload.tools || [])
|
||||
setLabels(payload.labels || [])
|
||||
}
|
||||
}, [isEdit, payload])
|
||||
|
||||
const [showEmojiPicker, setShowEmojiPicker] = useState(false)
|
||||
const emoji = customCollection.icon
|
||||
const setEmoji = (emoji: Emoji) => {
|
||||
|
|
@ -124,7 +134,6 @@ const EditCustomCollectionModal: FC<Props> = ({
|
|||
const [currTool, setCurrTool] = useState<CustomParamSchema | null>(null)
|
||||
const [isShowTestApi, setIsShowTestApi] = useState(false)
|
||||
|
||||
const [labels, setLabels] = useState<string[]>(payload?.labels || [])
|
||||
const handleLabelSelect = (value: string[]) => {
|
||||
setLabels(value)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,9 +100,28 @@ const ProviderDetail = ({
|
|||
const [isShowEditCollectionToolModal, setIsShowEditCustomCollectionModal] = useState(false)
|
||||
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
|
||||
const [deleteAction, setDeleteAction] = useState('')
|
||||
|
||||
const getCustomProvider = useCallback(async () => {
|
||||
setIsDetailLoading(true)
|
||||
const res = await fetchCustomCollection(collection.name)
|
||||
if (res.credentials.auth_type === AuthType.apiKey && !res.credentials.api_key_header_prefix) {
|
||||
if (res.credentials.api_key_value)
|
||||
res.credentials.api_key_header_prefix = AuthHeaderPrefix.custom
|
||||
}
|
||||
setCustomCollection({
|
||||
...res,
|
||||
labels: collection.labels,
|
||||
provider: collection.name,
|
||||
})
|
||||
setIsDetailLoading(false)
|
||||
}, [collection.labels, collection.name])
|
||||
|
||||
const doUpdateCustomToolCollection = async (data: CustomCollectionBackend) => {
|
||||
await updateCustomCollection(data)
|
||||
onRefreshData()
|
||||
await getCustomProvider()
|
||||
// Use fresh data from form submission to avoid race condition with collection.labels
|
||||
setCustomCollection(prev => prev ? { ...prev, labels: data.labels } : null)
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('common.api.actionSuccess'),
|
||||
|
|
@ -118,20 +137,6 @@ const ProviderDetail = ({
|
|||
})
|
||||
setIsShowEditCustomCollectionModal(false)
|
||||
}
|
||||
const getCustomProvider = useCallback(async () => {
|
||||
setIsDetailLoading(true)
|
||||
const res = await fetchCustomCollection(collection.name)
|
||||
if (res.credentials.auth_type === AuthType.apiKey && !res.credentials.api_key_header_prefix) {
|
||||
if (res.credentials.api_key_value)
|
||||
res.credentials.api_key_header_prefix = AuthHeaderPrefix.custom
|
||||
}
|
||||
setCustomCollection({
|
||||
...res,
|
||||
labels: collection.labels,
|
||||
provider: collection.name,
|
||||
})
|
||||
setIsDetailLoading(false)
|
||||
}, [collection.labels, collection.name])
|
||||
// workflow provider
|
||||
const [isShowEditWorkflowToolModal, setIsShowEditWorkflowToolModal] = useState(false)
|
||||
const getWorkflowToolProvider = useCallback(async () => {
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ const VersionHistoryButton: FC<VersionHistoryButtonProps> = ({
|
|||
>
|
||||
<Button
|
||||
className={cn(
|
||||
'p-2 rounded-lg border border-transparent',
|
||||
'rounded-lg border border-transparent p-2',
|
||||
theme === 'dark' && 'border-black/5 bg-white/10 backdrop-blur-sm',
|
||||
)}
|
||||
onClick={handleViewVersionHistory}
|
||||
|
|
|
|||
|
|
@ -1,68 +0,0 @@
|
|||
'use client'
|
||||
import BaseForm from '../components/base/form/form-scenarios/base'
|
||||
import { BaseFieldType } from '../components/base/form/form-scenarios/base/types'
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className="flex h-screen w-full items-center justify-center p-20">
|
||||
<div className="w-[400px] rounded-lg border border-components-panel-border bg-components-panel-bg">
|
||||
<BaseForm
|
||||
initialData={{
|
||||
type: 'option_1',
|
||||
variable: 'test',
|
||||
label: 'Test',
|
||||
maxLength: 48,
|
||||
required: true,
|
||||
}}
|
||||
configurations={[
|
||||
{
|
||||
type: BaseFieldType.textInput,
|
||||
variable: 'variable',
|
||||
label: 'Variable',
|
||||
required: true,
|
||||
showConditions: [],
|
||||
},
|
||||
{
|
||||
type: BaseFieldType.textInput,
|
||||
variable: 'label',
|
||||
label: 'Label',
|
||||
required: true,
|
||||
showConditions: [],
|
||||
},
|
||||
{
|
||||
type: BaseFieldType.numberInput,
|
||||
variable: 'maxLength',
|
||||
label: 'Max Length',
|
||||
required: true,
|
||||
showConditions: [],
|
||||
max: 100,
|
||||
min: 1,
|
||||
},
|
||||
{
|
||||
type: BaseFieldType.checkbox,
|
||||
variable: 'required',
|
||||
label: 'Required',
|
||||
required: true,
|
||||
showConditions: [],
|
||||
},
|
||||
{
|
||||
type: BaseFieldType.select,
|
||||
variable: 'type',
|
||||
label: 'Type',
|
||||
required: true,
|
||||
showConditions: [],
|
||||
options: [
|
||||
{ label: 'Option 1', value: 'option_1' },
|
||||
{ label: 'Option 2', value: 'option_2' },
|
||||
{ label: 'Option 3', value: 'option_3' },
|
||||
],
|
||||
},
|
||||
]}
|
||||
onSubmit={(value) => {
|
||||
console.log('onSubmit', value)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in New Issue