@@ -1051,7 +1577,7 @@ exports[`page/WorkflowTemplate/UpdateWorkflowTemplate no review node in template
diff --git a/packages/sqle/src/page/WorkflowTemplate/UpdateWorkflowTemplate/index.test.tsx b/packages/sqle/src/page/WorkflowTemplate/UpdateWorkflowTemplate/index.test.tsx
index ecbc1dda15..3eb473e0dc 100644
--- a/packages/sqle/src/page/WorkflowTemplate/UpdateWorkflowTemplate/index.test.tsx
+++ b/packages/sqle/src/page/WorkflowTemplate/UpdateWorkflowTemplate/index.test.tsx
@@ -1,6 +1,10 @@
import { sqleSuperRender } from '../../../testUtils/superRender';
import UpdateWorkflowTemplate from '.';
import { workflowTemplateData } from '@actiontech/shared/lib/testUtil/mockApi/sqle/workflowTemplate/data';
+import {
+ workflowTemplateOutOfOrderData,
+ dataExportWorkflowTemplateData
+} from '@actiontech/shared/lib/testUtil/mockApi/sqle/workflowTemplate/data';
import { act, fireEvent, screen, cleanup } from '@testing-library/react';
import {
getAllBySelector,
@@ -224,6 +228,36 @@ describe('page/WorkflowTemplate/UpdateWorkflowTemplate', () => {
expect(getAllBySelector('.ant-card').length).toBe(3);
});
+ it('should extract exec and review steps by type regardless of position in API response', async () => {
+ const getInfoRequest = workflowTemplate.getWorkflowTemplate();
+ // workflowTemplateOutOfOrderData: [sql_execute(3), sql_review(2, desc='step desc'), sql_review(1)]
+ // Without type-based filter (old pop() logic), pop() would give sql_review(1) as exec step
+ // With new filter logic, sql_execute(3) is correctly identified as exec step
+ getInfoRequest.mockImplementation(() =>
+ createSpySuccessResponse({
+ data: cloneDeep(workflowTemplateOutOfOrderData)
+ })
+ );
+ user.getUserTipList();
+ customRender();
+ await act(async () => jest.advanceTimersByTime(3000));
+ await act(async () => jest.advanceTimersByTime(3000));
+
+ // 2 review steps + exec step = 3 cards (+ create card + title card)
+ expect(getAllBySelector('.ant-card').length).toBe(5);
+
+ // Navigate to exec step (step index 3: after 2 review steps)
+ fireEvent.click(screen.getByText('下一步'));
+ await act(async () => jest.advanceTimersByTime(3000));
+ fireEvent.click(screen.getByText('下一步'));
+ await act(async () => jest.advanceTimersByTime(3000));
+ fireEvent.click(screen.getByText('下一步'));
+ await act(async () => jest.advanceTimersByTime(3000));
+
+ // Exec step is sql_execute (execute_by_authorized: true) → checkbox should be checked
+ expect(getBySelector('#execute_by_authorized')).toBeChecked();
+ });
+
it('no review node in template', async () => {
const getInfoRequest = workflowTemplate.getWorkflowTemplate();
const tempData = cloneDeep(workflowTemplateData);
@@ -294,5 +328,48 @@ describe('page/WorkflowTemplate/UpdateWorkflowTemplate', () => {
})
);
});
+
+ it('should correctly identify export_execute as exec step when API returns steps in wrong order', async () => {
+ const getInfoRequest = workflowTemplate.getWorkflowTemplate();
+ // Override: return dataExportWorkflowTemplateData but with steps in wrong order
+ // [export_execute(2) first, export_review(1) second]
+ getInfoRequest.mockImplementation(() =>
+ createSpySuccessResponse({
+ data: {
+ ...cloneDeep(dataExportWorkflowTemplateData),
+ workflow_step_template_list: [
+ {
+ approved_by_authorized: false,
+ assignee_user_id_list: [],
+ execute_by_authorized: true,
+ number: 2,
+ type: 'export_execute'
+ },
+ {
+ approved_by_authorized: true,
+ assignee_user_id_list: [],
+ execute_by_authorized: false,
+ number: 1,
+ type: 'export_review'
+ }
+ ]
+ }
+ })
+ );
+ user.getUserTipList();
+ const { baseElement } = customRender();
+ await act(async () => jest.advanceTimersByTime(3000));
+ await act(async () => jest.advanceTimersByTime(3000));
+
+ // 1 export_review step + exec step = 2 step cards (+ create card)
+ expect(getAllBySelector('.ant-card').length).toBe(3);
+
+ // Navigate to exec step
+ fireEvent.click(screen.getByText('下一步'));
+ await act(async () => jest.advanceTimersByTime(3000));
+ fireEvent.click(screen.getByText('下一步'));
+ await act(async () => jest.advanceTimersByTime(3000));
+ expect(baseElement).toMatchSnapshot();
+ });
});
});
diff --git a/packages/sqle/src/page/WorkflowTemplate/UpdateWorkflowTemplate/index.tsx b/packages/sqle/src/page/WorkflowTemplate/UpdateWorkflowTemplate/index.tsx
index 82818363b4..aae6262cb1 100644
--- a/packages/sqle/src/page/WorkflowTemplate/UpdateWorkflowTemplate/index.tsx
+++ b/packages/sqle/src/page/WorkflowTemplate/UpdateWorkflowTemplate/index.tsx
@@ -1,9 +1,5 @@
import { BasicButton, BasicResult, PageHeader } from '@actiontech/dms-kit';
-import {
- ActionButton,
- useTypedParams,
- useTypedQuery
-} from '@actiontech/shared';
+import { ActionButton, useTypedQuery } from '@actiontech/shared';
import { ArrowLeftOutlined } from '@ant-design/icons';
import { Col, Row, Space, Spin } from 'antd';
import React, { useEffect, useState } from 'react';
@@ -38,6 +34,7 @@ import {
getWorkflowTemplateV1WorkflowTypeEnum,
updateWorkflowTemplateV1WorkflowTypeEnum
} from '@actiontech/shared/lib/api/sqle/service/workflow/index.enum';
+import { WorkflowStepTypeEnum } from '../WorkflowTemplateDetail/enum';
const UpdateWorkflowTemplate: React.FC = () => {
const { t } = useTranslation();
@@ -150,9 +147,30 @@ const UpdateWorkflowTemplate: React.FC = () => {
if (stepList.length <= 1) {
setExecSteps(stepList[0]);
} else {
- const execStep = stepList.pop();
- setReviewSteps(stepList);
- if (execStep) setExecSteps(execStep);
+ const execSteps = stepList.filter(
+ (v) =>
+ v.type ===
+ (workflowType ===
+ getWorkflowTemplateV1WorkflowTypeEnum.workflow
+ ? WorkflowStepTypeEnum.sql_execute
+ : WorkflowStepTypeEnum.export_execute)
+ );
+ const reviewSteps = stepList
+ .filter(
+ (v) =>
+ v.type ===
+ (workflowType ===
+ getWorkflowTemplateV1WorkflowTypeEnum.workflow
+ ? WorkflowStepTypeEnum.sql_review
+ : WorkflowStepTypeEnum.export_review)
+ )
+ .sort((a, b) => (a.number ?? 0) - (b.number ?? 0));
+ setReviewSteps(reviewSteps);
+ if (execSteps.length === 1) {
+ setExecSteps(execSteps[0]);
+ } else {
+ setExecSteps({ assignee_user_id_list: [], desc: '' });
+ }
}
}
return res.data.data;
@@ -327,7 +345,10 @@ const UpdateWorkflowTemplate: React.FC = () => {
exchangeReviewNode={handleExchangeReviewNode}
clickReviewNode={handleClickReviewNode}
usernameList={usernameList}
- isDataExport={workflowType === 'data_export'}
+ isDataExport={
+ workflowType ===
+ getWorkflowTemplateV1WorkflowTypeEnum.data_export
+ }
/>
{
expect(screen.getByText('上线工单')).toBeInTheDocument();
expect(screen.getByText('数据导出')).toBeInTheDocument();
});
+
+ it('should sort sql review steps by number ascending when API returns steps in wrong order', async () => {
+ const getTemplateRequest = workflowTemplate.getWorkflowTemplate();
+ getTemplateRequest.mockImplementation((params) => {
+ if (params.workflow_type === 'data_export') {
+ return createSpySuccessResponse({
+ data: cloneDeep(dataExportWorkflowTemplateData)
+ });
+ }
+ return createSpySuccessResponse({
+ data: cloneDeep(workflowTemplateOutOfOrderData)
+ });
+ });
+ customRender();
+ await act(async () => jest.advanceTimersByTime(3000));
+
+ // workflowTemplateOutOfOrderData has steps in order: sql_execute(3), sql_review(2, desc='step desc'), sql_review(1, approved_by_authorized)
+ // After filter + sort by number: reviewSteps = [sql_review(1), sql_review(2)], execStep = sql_execute(3)
+ // Rendered order: create → review#1(approved_by_authorized) → review#2(desc='step desc') → exec
+ const reviewStep1Indicator =
+ screen.getByText('匹配拥有数据源审核权限的成员');
+ const reviewStep2Desc = screen.getByText('step desc');
+
+ // review#1 (number:1, approved_by_authorized) must appear before review#2 (number:2, desc='step desc')
+ expect(
+ reviewStep1Indicator.compareDocumentPosition(reviewStep2Desc) &
+ Node.DOCUMENT_POSITION_FOLLOWING
+ ).toBeTruthy();
+
+ // exec step correctly identified as sql_execute (execute_by_authorized:true)
+ expect(
+ screen.getByText('匹配拥有数据源上线权限的成员')
+ ).toBeInTheDocument();
+ });
+
+ it('should sort export review steps by number ascending when API returns steps in wrong order', async () => {
+ const getTemplateRequest = workflowTemplate.getWorkflowTemplate();
+ getTemplateRequest.mockImplementation((params) => {
+ if (params.workflow_type === 'data_export') {
+ return createSpySuccessResponse({
+ data: cloneDeep(dataExportWorkflowTemplateOutOfOrderData)
+ });
+ }
+ return createSpySuccessResponse({
+ data: cloneDeep(workflowTemplateOutOfOrderData)
+ });
+ });
+ customRender();
+ await act(async () => jest.advanceTimersByTime(3000));
+
+ fireEvent.click(screen.getByText('数据导出'));
+ await act(async () => jest.advanceTimersByTime(3000));
+
+ // dataExportWorkflowTemplateOutOfOrderData has steps: export_execute(2) first, export_review(1) second
+ // After filter + sort: reviewSteps = [export_review(1)], execStep = export_execute(2)
+ // exec step is correctly export_execute (execute_by_authorized:true, isDataExport=true)
+ // → shows "数据导出的执行人默认为工单创建者,不可修改。"
+ expect(
+ screen.getByText('数据导出的执行人默认为工单创建者,不可修改。')
+ ).toBeInTheDocument();
+
+ // review step is correctly export_review (approved_by_authorized:true)
+ // → shows "匹配拥有数据源审核权限的成员"
+ // exec step (执行导出) must appear after the review step
+ const reviewStep = screen.getAllByText('匹配拥有数据源审核权限的成员')[0];
+ const execStepUser = screen.getByText(
+ '数据导出的执行人默认为工单创建者,不可修改。'
+ );
+ expect(
+ reviewStep.compareDocumentPosition(execStepUser) &
+ Node.DOCUMENT_POSITION_FOLLOWING
+ ).toBeTruthy();
+ });
});
diff --git a/packages/sqle/src/page/WorkflowTemplate/WorkflowTemplateDetail/index.tsx b/packages/sqle/src/page/WorkflowTemplate/WorkflowTemplateDetail/index.tsx
index 40885f6891..28dfd967aa 100644
--- a/packages/sqle/src/page/WorkflowTemplate/WorkflowTemplateDetail/index.tsx
+++ b/packages/sqle/src/page/WorkflowTemplate/WorkflowTemplateDetail/index.tsx
@@ -14,6 +14,7 @@ import { WorkflowTemplateStyleWrapper } from './style';
import useUsername from '../../../hooks/useUsername';
import { getWorkflowTemplateV1WorkflowTypeEnum } from '@actiontech/shared/lib/api/sqle/service/workflow/index.enum';
import { workflowTemplateDetailAction } from './actions';
+import { WorkflowStepTypeEnum } from './enum';
const WorkflowTemplateDetail: React.FC = () => {
const { t } = useTranslation();
@@ -70,9 +71,18 @@ const WorkflowTemplateDetail: React.FC = () => {
);
setWorkflowReviewSteps([]);
} else {
- const execStep = stepList.pop();
- setWorkflowReviewSteps(stepList);
- if (execStep) setWorkflowExecStep(execStep);
+ const execSteps = stepList.filter(
+ (v) => v.type === WorkflowStepTypeEnum.sql_execute
+ );
+ const reviewSteps = stepList
+ .filter((v) => v.type === WorkflowStepTypeEnum.sql_review)
+ .sort((a, b) => (a.number ?? 0) - (b.number ?? 0));
+ setWorkflowReviewSteps(reviewSteps);
+ if (execSteps.length === 1) {
+ setWorkflowExecStep(execSteps[0]);
+ } else {
+ setWorkflowExecStep({ assignee_user_id_list: [], desc: '' });
+ }
}
return res.data.data;
}),
@@ -107,9 +117,20 @@ const WorkflowTemplateDetail: React.FC = () => {
);
setExportReviewSteps([]);
} else {
- const execStep = stepList.pop();
- setExportReviewSteps(stepList);
- if (execStep) setExportExecStep(execStep);
+ const execSteps = stepList.filter(
+ (v) => v.type === WorkflowStepTypeEnum.export_execute
+ );
+ const reviewSteps = stepList
+ .filter((v) => v.type === WorkflowStepTypeEnum.export_review)
+ .sort((a, b) => (a.number ?? 0) - (b.number ?? 0));
+ setExportReviewSteps(reviewSteps);
+ if (execSteps.length === 1) {
+ setExportExecStep(execSteps[0]);
+ } else {
+ setExportExecStep(
+ stepList[0] ?? { assignee_user_id_list: [], desc: '' }
+ );
+ }
}
return res.data.data;
}),