mirror of
https://github.com/langgenius/dify.git
synced 2026-09-01 21:55:46 +08:00
Merge remote-tracking branch 'origin/main' into feat/agent-v2
This commit is contained in:
commit
8203d42233
@ -80,6 +80,13 @@ class SnippetDraftConfigResponse(BaseModel):
|
||||
parallel_depth_limit: int
|
||||
|
||||
|
||||
class SnippetWorkflowPaginationResponse(BaseModel):
|
||||
items: list[SnippetWorkflowResponse]
|
||||
page: int
|
||||
limit: int
|
||||
has_more: bool
|
||||
|
||||
|
||||
register_schema_models(
|
||||
console_ns,
|
||||
SnippetDraftSyncPayload,
|
||||
@ -98,6 +105,7 @@ register_response_schema_models(
|
||||
SimpleResultResponse,
|
||||
SnippetDraftConfigResponse,
|
||||
SnippetWorkflowResponse,
|
||||
SnippetWorkflowPaginationResponse,
|
||||
WorkflowPublishResponse,
|
||||
WorkflowPaginationResponse,
|
||||
WorkflowRestoreResponse,
|
||||
@ -329,7 +337,7 @@ class SnippetPublishedAllWorkflowApi(Resource):
|
||||
@console_ns.response(
|
||||
200,
|
||||
"Published workflows retrieved successfully",
|
||||
console_ns.models[WorkflowPaginationResponse.__name__],
|
||||
console_ns.models[SnippetWorkflowPaginationResponse.__name__],
|
||||
)
|
||||
@setup_required
|
||||
@login_required
|
||||
@ -350,7 +358,7 @@ class SnippetPublishedAllWorkflowApi(Resource):
|
||||
limit=args.limit,
|
||||
)
|
||||
|
||||
return WorkflowPaginationResponse.model_validate(
|
||||
response = SnippetWorkflowPaginationResponse.model_validate(
|
||||
{
|
||||
"items": workflows,
|
||||
"page": args.page,
|
||||
@ -359,6 +367,9 @@ class SnippetPublishedAllWorkflowApi(Resource):
|
||||
},
|
||||
from_attributes=True,
|
||||
).model_dump(mode="json")
|
||||
for item in response["items"]:
|
||||
item["input_fields"] = snippet.input_fields_list
|
||||
return response
|
||||
|
||||
|
||||
@console_ns.route("/snippets/<uuid:snippet_id>/workflows/<string:workflow_id>/restore")
|
||||
|
||||
@ -8159,7 +8159,7 @@ Get all published workflows for a snippet
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Published workflows retrieved successfully | **application/json**: [WorkflowPaginationResponse](#workflowpaginationresponse)<br> |
|
||||
| 200 | Published workflows retrieved successfully | **application/json**: [SnippetWorkflowPaginationResponse](#snippetworkflowpaginationresponse)<br> |
|
||||
|
||||
### [GET] /snippets/{snippet_id}/workflows/default-workflow-block-configs
|
||||
**Get default block configurations for snippet workflow**
|
||||
@ -19504,6 +19504,15 @@ Query parameters for listing snippet published workflows.
|
||||
| limit | integer, <br>**Default:** 10 | | No |
|
||||
| page | integer, <br>**Default:** 1 | | No |
|
||||
|
||||
#### SnippetWorkflowPaginationResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| has_more | boolean | | Yes |
|
||||
| items | [ [SnippetWorkflowResponse](#snippetworkflowresponse) ] | | Yes |
|
||||
| limit | integer | | Yes |
|
||||
| page | integer | | Yes |
|
||||
|
||||
#### SnippetWorkflowResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
from inspect import unwrap
|
||||
from types import SimpleNamespace
|
||||
@ -199,6 +200,54 @@ def test_default_block_configs_delegates_to_service(app: Flask, monkeypatch: pyt
|
||||
get_default_block_configs.assert_called_once()
|
||||
|
||||
|
||||
def test_list_published_snippet_workflows_includes_input_fields(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
workflow = SimpleNamespace(
|
||||
id="workflow-1",
|
||||
graph_dict={"nodes": [], "edges": []},
|
||||
features_dict={},
|
||||
unique_hash="hash-1",
|
||||
version="2024-01-01 00:00:00",
|
||||
marked_name="",
|
||||
marked_comment="",
|
||||
created_by_account=None,
|
||||
created_at=datetime(2024, 1, 1),
|
||||
updated_by_account=None,
|
||||
updated_at=datetime(2024, 1, 1),
|
||||
tool_published=False,
|
||||
environment_variables=[],
|
||||
conversation_variables=[],
|
||||
rag_pipeline_variables=[],
|
||||
)
|
||||
input_fields = [{"variable": "query", "type": "text"}]
|
||||
snippet = _snippet(input_fields=json.dumps(input_fields))
|
||||
|
||||
class SessionContext:
|
||||
def __init__(self, engine):
|
||||
self.engine = engine
|
||||
|
||||
def __enter__(self):
|
||||
return Mock()
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
return False
|
||||
|
||||
monkeypatch.setattr(snippet_workflow_module, "Session", SessionContext)
|
||||
monkeypatch.setattr(snippet_workflow_module, "db", SimpleNamespace(engine=object()))
|
||||
monkeypatch.setattr(
|
||||
snippet_workflow_module,
|
||||
"SnippetService",
|
||||
lambda: SimpleNamespace(get_all_published_workflows=Mock(return_value=([workflow], False))),
|
||||
)
|
||||
|
||||
api = snippet_workflow_module.SnippetPublishedAllWorkflowApi()
|
||||
handler = unwrap(api.get)
|
||||
|
||||
with app.test_request_context("/snippets/snippet-1/workflows?page=1&limit=20"):
|
||||
response = handler(api, snippet=snippet)
|
||||
|
||||
assert response["items"][0]["input_fields"] == input_fields
|
||||
|
||||
|
||||
def test_restore_published_snippet_workflow_to_draft_success(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
workflow = SimpleNamespace(
|
||||
unique_hash="restored-hash",
|
||||
|
||||
@ -37,9 +37,9 @@ export type WorkflowRunNodeExecutionListResponse = {
|
||||
data: Array<WorkflowRunNodeExecutionResponse>
|
||||
}
|
||||
|
||||
export type WorkflowPaginationResponse = {
|
||||
export type SnippetWorkflowPaginationResponse = {
|
||||
has_more: boolean
|
||||
items: Array<WorkflowResponse>
|
||||
items: Array<SnippetWorkflowResponse>
|
||||
limit: number
|
||||
page: number
|
||||
}
|
||||
@ -235,28 +235,6 @@ export type SimpleEndUser = {
|
||||
type: string
|
||||
}
|
||||
|
||||
export type WorkflowResponse = {
|
||||
conversation_variables: Array<WorkflowConversationVariableResponse>
|
||||
created_at: number
|
||||
created_by?: SimpleAccount | null
|
||||
environment_variables: Array<WorkflowEnvironmentVariableResponse>
|
||||
features: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
graph: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
hash: string
|
||||
id: string
|
||||
marked_comment: string
|
||||
marked_name: string
|
||||
rag_pipeline_variables: Array<PipelineVariableResponse>
|
||||
tool_published: boolean
|
||||
updated_at: number
|
||||
updated_by?: SimpleAccount | null
|
||||
version: string
|
||||
}
|
||||
|
||||
export type WorkflowConversationVariableResponse = {
|
||||
description: string
|
||||
id: string
|
||||
@ -417,7 +395,7 @@ export type GetSnippetsBySnippetIdWorkflowsData = {
|
||||
}
|
||||
|
||||
export type GetSnippetsBySnippetIdWorkflowsResponses = {
|
||||
200: WorkflowPaginationResponse
|
||||
200: SnippetWorkflowPaginationResponse
|
||||
}
|
||||
|
||||
export type GetSnippetsBySnippetIdWorkflowsResponse
|
||||
|
||||
@ -300,32 +300,11 @@ export const zSnippetWorkflowResponse = z.object({
|
||||
})
|
||||
|
||||
/**
|
||||
* WorkflowResponse
|
||||
* SnippetWorkflowPaginationResponse
|
||||
*/
|
||||
export const zWorkflowResponse = z.object({
|
||||
conversation_variables: z.array(zWorkflowConversationVariableResponse),
|
||||
created_at: z.int(),
|
||||
created_by: zSimpleAccount.nullish(),
|
||||
environment_variables: z.array(zWorkflowEnvironmentVariableResponse),
|
||||
features: z.record(z.string(), z.unknown()),
|
||||
graph: z.record(z.string(), z.unknown()),
|
||||
hash: z.string(),
|
||||
id: z.string(),
|
||||
marked_comment: z.string(),
|
||||
marked_name: z.string(),
|
||||
rag_pipeline_variables: z.array(zPipelineVariableResponse),
|
||||
tool_published: z.boolean(),
|
||||
updated_at: z.int(),
|
||||
updated_by: zSimpleAccount.nullish(),
|
||||
version: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* WorkflowPaginationResponse
|
||||
*/
|
||||
export const zWorkflowPaginationResponse = z.object({
|
||||
export const zSnippetWorkflowPaginationResponse = z.object({
|
||||
has_more: z.boolean(),
|
||||
items: z.array(zWorkflowResponse),
|
||||
items: z.array(zSnippetWorkflowResponse),
|
||||
limit: z.int(),
|
||||
page: z.int(),
|
||||
})
|
||||
@ -443,7 +422,7 @@ export const zGetSnippetsBySnippetIdWorkflowsQuery = z.object({
|
||||
/**
|
||||
* Published workflows retrieved successfully
|
||||
*/
|
||||
export const zGetSnippetsBySnippetIdWorkflowsResponse = zWorkflowPaginationResponse
|
||||
export const zGetSnippetsBySnippetIdWorkflowsResponse = zSnippetWorkflowPaginationResponse
|
||||
|
||||
export const zGetSnippetsBySnippetIdWorkflowsDefaultWorkflowBlockConfigsPath = z.object({
|
||||
snippet_id: z.uuid(),
|
||||
|
||||
15
web/app/components/main-nav/__tests__/skip-nav.spec.tsx
Normal file
15
web/app/components/main-nav/__tests__/skip-nav.spec.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { SkipNav } from '../skip-nav'
|
||||
|
||||
describe('SkipNav', () => {
|
||||
it('keeps the shadow hidden until the link is visible', () => {
|
||||
render(<SkipNav>Skip to main content</SkipNav>)
|
||||
|
||||
const link = screen.getByRole('link', { name: 'Skip to main content' })
|
||||
|
||||
expect(link).not.toHaveClass('shadow-lg')
|
||||
expect(link).not.toHaveClass('shadow-shadow-shadow-5')
|
||||
expect(link).toHaveClass('focus-visible:shadow-lg')
|
||||
expect(link).toHaveClass('focus-visible:shadow-shadow-shadow-5')
|
||||
})
|
||||
})
|
||||
@ -26,7 +26,7 @@ export function SkipNav({
|
||||
href={MAIN_CONTENT_HREF}
|
||||
onClick={handleClick}
|
||||
className={cn(
|
||||
'fixed top-2 left-2 z-60 inline-flex h-9 -translate-y-[calc(100%+0.75rem)] items-center justify-center rounded-lg border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-3 system-sm-medium text-components-button-secondary-text shadow-lg shadow-shadow-shadow-5 outline-hidden transition-transform duration-150 focus-visible:translate-y-0 focus-visible:ring-2 focus-visible:ring-state-accent-solid motion-reduce:transition-none',
|
||||
'fixed top-2 left-2 z-60 inline-flex h-9 -translate-y-[calc(100%+0.75rem)] items-center justify-center rounded-lg border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-3 system-sm-medium text-components-button-secondary-text outline-hidden transition-transform duration-150 focus-visible:translate-y-0 focus-visible:shadow-lg focus-visible:ring-2 focus-visible:shadow-shadow-shadow-5 focus-visible:ring-state-accent-solid motion-reduce:transition-none',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@ -0,0 +1,64 @@
|
||||
import type { Release } from '@dify/contracts/enterprise/types.gen'
|
||||
import { ReleaseSource } from '@dify/contracts/enterprise/types.gen'
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ReleaseMetaTooltip } from '../instance-card-sections'
|
||||
|
||||
function createRelease(overrides: Partial<Release> = {}): Release {
|
||||
return {
|
||||
id: 'release-1',
|
||||
appInstanceId: 'app-instance-1',
|
||||
displayName: 'Initial release',
|
||||
description: '',
|
||||
source: ReleaseSource.RELEASE_SOURCE_SOURCE_APP,
|
||||
sourceAppId: 'source-app-1',
|
||||
gateCommitId: 'commit-1',
|
||||
requiredSlots: [],
|
||||
createdBy: {
|
||||
id: 'user-1',
|
||||
displayName: 'Ada',
|
||||
},
|
||||
createdAt: '2026-01-01T00:00:00.000Z',
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
function closestWithClass(element: HTMLElement, className: string) {
|
||||
let current: HTMLElement | null = element
|
||||
|
||||
while (current) {
|
||||
if (current.classList.contains(className))
|
||||
return current
|
||||
current = current.parentElement
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
describe('ReleaseMetaTooltip', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('should use compact typography for the release metadata preview', async () => {
|
||||
render(
|
||||
<ReleaseMetaTooltip release={createRelease()} deployed>
|
||||
<a href="/deployments/app-instance-1/releases">
|
||||
Initial release · 14 days ago
|
||||
</a>
|
||||
</ReleaseMetaTooltip>,
|
||||
)
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.mouseEnter(screen.getByRole('link', { name: 'Initial release · 14 days ago' }))
|
||||
await vi.advanceTimersByTimeAsync(700)
|
||||
})
|
||||
|
||||
const previewValue = screen.getByText('Initial release')
|
||||
expect(closestWithClass(previewValue, 'min-w-48')).toHaveClass('system-xs-regular')
|
||||
})
|
||||
})
|
||||
@ -50,7 +50,7 @@ export function ReleaseMetaTooltip({ release, deployed, children }: {
|
||||
<PreviewCard>
|
||||
<PreviewCardTrigger render={children} />
|
||||
<PreviewCardContent popupClassName="px-3 py-2">
|
||||
<div className="flex min-w-48 flex-col gap-1">
|
||||
<div className="flex min-w-48 flex-col gap-1 system-xs-regular">
|
||||
{rows.map(row => (
|
||||
<div key={row.label} className="flex justify-between gap-4">
|
||||
<span className="shrink-0 text-text-tertiary">{row.label}</span>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user