Merge remote-tracking branch 'origin/main' into feat/agent-v2

This commit is contained in:
Yanli 盐粒 2026-06-23 16:54:02 +08:00
commit 8203d42233
9 changed files with 160 additions and 55 deletions

View File

@ -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")

View File

@ -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 |

View File

@ -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",

View File

@ -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

View File

@ -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(),

View 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')
})
})

View File

@ -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}

View File

@ -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')
})
})

View File

@ -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>