diff --git a/api/controllers/console/snippets/snippet_workflow.py b/api/controllers/console/snippets/snippet_workflow.py index 5af885ab91b..0b8dc264a68 100644 --- a/api/controllers/console/snippets/snippet_workflow.py +++ b/api/controllers/console/snippets/snippet_workflow.py @@ -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//workflows//restore") diff --git a/api/openapi/markdown/console-openapi.md b/api/openapi/markdown/console-openapi.md index f37e05f8c2c..7b235dbd31a 100644 --- a/api/openapi/markdown/console-openapi.md +++ b/api/openapi/markdown/console-openapi.md @@ -8159,7 +8159,7 @@ Get all published workflows for a snippet | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Published workflows retrieved successfully | **application/json**: [WorkflowPaginationResponse](#workflowpaginationresponse)
| +| 200 | Published workflows retrieved successfully | **application/json**: [SnippetWorkflowPaginationResponse](#snippetworkflowpaginationresponse)
| ### [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,
**Default:** 10 | | No | | page | integer,
**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 | diff --git a/api/tests/unit_tests/controllers/console/snippets/test_snippet_workflow.py b/api/tests/unit_tests/controllers/console/snippets/test_snippet_workflow.py index 11916c87b68..b20dd3e30a7 100644 --- a/api/tests/unit_tests/controllers/console/snippets/test_snippet_workflow.py +++ b/api/tests/unit_tests/controllers/console/snippets/test_snippet_workflow.py @@ -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", diff --git a/packages/contracts/generated/api/console/snippets/types.gen.ts b/packages/contracts/generated/api/console/snippets/types.gen.ts index d46a6389a52..631da7bace8 100644 --- a/packages/contracts/generated/api/console/snippets/types.gen.ts +++ b/packages/contracts/generated/api/console/snippets/types.gen.ts @@ -37,9 +37,9 @@ export type WorkflowRunNodeExecutionListResponse = { data: Array } -export type WorkflowPaginationResponse = { +export type SnippetWorkflowPaginationResponse = { has_more: boolean - items: Array + items: Array limit: number page: number } @@ -235,28 +235,6 @@ export type SimpleEndUser = { type: string } -export type WorkflowResponse = { - conversation_variables: Array - created_at: number - created_by?: SimpleAccount | null - environment_variables: Array - features: { - [key: string]: unknown - } - graph: { - [key: string]: unknown - } - hash: string - id: string - marked_comment: string - marked_name: string - rag_pipeline_variables: Array - 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 diff --git a/packages/contracts/generated/api/console/snippets/zod.gen.ts b/packages/contracts/generated/api/console/snippets/zod.gen.ts index 1c861084434..85e5b961547 100644 --- a/packages/contracts/generated/api/console/snippets/zod.gen.ts +++ b/packages/contracts/generated/api/console/snippets/zod.gen.ts @@ -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(), diff --git a/web/app/components/main-nav/__tests__/skip-nav.spec.tsx b/web/app/components/main-nav/__tests__/skip-nav.spec.tsx new file mode 100644 index 00000000000..3bd072f3c93 --- /dev/null +++ b/web/app/components/main-nav/__tests__/skip-nav.spec.tsx @@ -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(Skip to main content) + + 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') + }) +}) diff --git a/web/app/components/main-nav/skip-nav.tsx b/web/app/components/main-nav/skip-nav.tsx index eda7ad4b5f0..6bdb82c6563 100644 --- a/web/app/components/main-nav/skip-nav.tsx +++ b/web/app/components/main-nav/skip-nav.tsx @@ -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} diff --git a/web/features/deployments/list/ui/__tests__/instance-card-sections.spec.tsx b/web/features/deployments/list/ui/__tests__/instance-card-sections.spec.tsx new file mode 100644 index 00000000000..d1bdfbe5c43 --- /dev/null +++ b/web/features/deployments/list/ui/__tests__/instance-card-sections.spec.tsx @@ -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 { + 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( + + + Initial release · 14 days ago + + , + ) + + 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') + }) +}) diff --git a/web/features/deployments/list/ui/instance-card-sections.tsx b/web/features/deployments/list/ui/instance-card-sections.tsx index 821ebf63144..77502da39bc 100644 --- a/web/features/deployments/list/ui/instance-card-sections.tsx +++ b/web/features/deployments/list/ui/instance-card-sections.tsx @@ -50,7 +50,7 @@ export function ReleaseMetaTooltip({ release, deployed, children }: { -
+
{rows.map(row => (
{row.label}