Update non-integrated intake workflow
This commit is contained in:
parent
ac7c5a81d8
commit
4ab12803f5
@ -82,12 +82,36 @@ jest.mock('./non-integrated/NonIntegratedDetailClient', () => ({
|
||||
default: ({
|
||||
productId,
|
||||
mode,
|
||||
initialDocumentWorkspaceView,
|
||||
initialDocumentWorkspaceCategory,
|
||||
onDocumentWorkspaceStateChange,
|
||||
}: {
|
||||
productId?: string
|
||||
mode: 'create' | 'edit' | 'view'
|
||||
initialDocumentWorkspaceView?: 'detail' | 'document'
|
||||
initialDocumentWorkspaceCategory?: string
|
||||
onDocumentWorkspaceStateChange?: (state: {
|
||||
view: 'detail' | 'document'
|
||||
category: string
|
||||
}) => void
|
||||
}) => (
|
||||
<div data-testid="embedded-non-integrated-detail">
|
||||
{productId}-{mode}
|
||||
<span data-testid="embedded-workspace-state">
|
||||
{(initialDocumentWorkspaceView ?? 'detail')}-
|
||||
{(initialDocumentWorkspaceCategory ?? '基础信息')}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
onDocumentWorkspaceStateChange?.({
|
||||
view: 'document',
|
||||
category: '专利申请',
|
||||
})
|
||||
}
|
||||
>
|
||||
模拟切到专利申请
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
@ -313,6 +337,9 @@ function createPatentApprovedProduct(overrides: Record<string, unknown> = {}) {
|
||||
const product = createNonIntegratedProduct(overrides)
|
||||
return {
|
||||
...product,
|
||||
patentApplication:
|
||||
(overrides.patentApplication as Record<string, unknown> | undefined) ??
|
||||
createPatentApplicationContent(),
|
||||
documentWorkspaceConfirmations: {
|
||||
...createDefaultDocumentWorkspaceConfirmations(),
|
||||
...(product.documentWorkspaceConfirmations as Record<string, boolean>),
|
||||
@ -1751,6 +1778,30 @@ describe('IntakeClient', () => {
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('keeps each product workspace on the last selected right-side node when switching top tabs', async () => {
|
||||
const actor = userEvent.setup()
|
||||
|
||||
window.localStorage.setItem(
|
||||
NON_INTEGRATED_STORAGE_KEY,
|
||||
JSON.stringify([createNonIntegratedProduct()]),
|
||||
)
|
||||
|
||||
renderSelector({ eligibleSkuList: [] })
|
||||
|
||||
await actor.click(screen.getByRole('button', { name: '修改' }))
|
||||
expect(screen.getByTestId('embedded-workspace-state')).toHaveTextContent(
|
||||
'detail-基础信息',
|
||||
)
|
||||
|
||||
await actor.click(screen.getByRole('button', { name: '模拟切到专利申请' }))
|
||||
await actor.click(screen.getByRole('tab', { name: '非集成系列产品' }))
|
||||
await actor.click(screen.getByRole('tab', { name: 'DNZ0001产品开发明细' }))
|
||||
|
||||
expect(screen.getByTestId('embedded-workspace-state')).toHaveTextContent(
|
||||
'document-专利申请',
|
||||
)
|
||||
})
|
||||
|
||||
it('opens a separate workspace tab each time product edit is clicked', async () => {
|
||||
const actor = userEvent.setup()
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@ import {
|
||||
type ChangeEvent,
|
||||
type KeyboardEvent,
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
@ -185,6 +186,10 @@ type BomLibraryKey =
|
||||
| "packagingLibrary"
|
||||
| "packagingMaterialLibrary";
|
||||
type WorkspaceProductMode = "view" | "edit";
|
||||
type WorkspaceProductDetailState = {
|
||||
view: "detail" | "document";
|
||||
category: NonIntegratedDocumentWorkspaceCategory;
|
||||
};
|
||||
type IntakeWorkspaceTab =
|
||||
| {
|
||||
id: string;
|
||||
@ -214,6 +219,7 @@ type IntakeWorkspaceTab =
|
||||
productId: string;
|
||||
product: NonIntegratedProductRecord;
|
||||
mode: WorkspaceProductMode;
|
||||
detailState?: WorkspaceProductDetailState;
|
||||
}
|
||||
| {
|
||||
id: string;
|
||||
@ -6859,6 +6865,44 @@ export default function IntakeClient(props: IntakeClientProps) {
|
||||
|
||||
const activeWorkspaceTab =
|
||||
workspaceTabs.find((tab) => tab.id === activeWorkspaceTabId) ?? null;
|
||||
const activeProductWorkspaceTabId =
|
||||
activeWorkspaceTab?.kind === "nonIntegratedProduct"
|
||||
? activeWorkspaceTab.id
|
||||
: null;
|
||||
|
||||
const updateProductWorkspaceDetailState = useCallback(
|
||||
(tabId: string, detailState: WorkspaceProductDetailState) => {
|
||||
setWorkspaceTabs((current) => {
|
||||
let changed = false;
|
||||
const nextTabs = current.map((tab) => {
|
||||
if (tab.id !== tabId || tab.kind !== "nonIntegratedProduct") {
|
||||
return tab;
|
||||
}
|
||||
|
||||
if (
|
||||
tab.detailState?.view === detailState.view &&
|
||||
tab.detailState.category === detailState.category
|
||||
) {
|
||||
return tab;
|
||||
}
|
||||
|
||||
changed = true;
|
||||
return { ...tab, detailState };
|
||||
});
|
||||
|
||||
return changed ? nextTabs : current;
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleActiveProductWorkspaceDetailStateChange = useCallback(
|
||||
(detailState: WorkspaceProductDetailState) => {
|
||||
if (!activeProductWorkspaceTabId) return;
|
||||
updateProductWorkspaceDetailState(activeProductWorkspaceTabId, detailState);
|
||||
},
|
||||
[activeProductWorkspaceTabId, updateProductWorkspaceDetailState],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setNonIntegratedProducts(props.initialNonIntegratedProducts);
|
||||
@ -7087,8 +7131,11 @@ export default function IntakeClient(props: IntakeClientProps) {
|
||||
mode={activeWorkspaceTab.mode}
|
||||
productId={activeWorkspaceTab.productId}
|
||||
initialProduct={activeWorkspaceTab.product}
|
||||
initialDocumentWorkspaceView={activeWorkspaceTab.detailState?.view}
|
||||
initialDocumentWorkspaceCategory={activeWorkspaceTab.detailState?.category}
|
||||
currentUserRole={props.user.role}
|
||||
onProductSaved={handleNonIntegratedProductSaved}
|
||||
onDocumentWorkspaceStateChange={handleActiveProductWorkspaceDetailStateChange}
|
||||
onReturnToList={activateNonIntegratedListWorkspace}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -26,6 +26,7 @@ import {
|
||||
import { parseZipFile } from "@/lib/zip-parser";
|
||||
import { extractPreviewModelFromZip } from "../../../lib/model-preview-zip";
|
||||
import { fetchSubstrates } from "@/lib/open-product/platform-api";
|
||||
import { createApprovalWorkflow } from "@/app/approval-workflows/approvalWorkflowApi";
|
||||
|
||||
const push = jest.fn();
|
||||
const modelPreviewPanelMock = jest.fn();
|
||||
@ -102,6 +103,16 @@ jest.mock("@/lib/open-product/platform-api", () => ({
|
||||
fetchSubstrates: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("@/app/approval-workflows/approvalWorkflowApi", () => ({
|
||||
buildApprovalNumber: jest.fn(() => "PD20260505175418001"),
|
||||
createApprovalWorkflow: jest.fn(async (input) => ({
|
||||
id: "workflow-test",
|
||||
accessKey: "workflow-access-key",
|
||||
status: "PENDING",
|
||||
...input,
|
||||
})),
|
||||
}));
|
||||
|
||||
const mockedPreviewSharedNonIntegratedDocumentImages = jest.mocked(
|
||||
previewSharedNonIntegratedDocumentImages,
|
||||
);
|
||||
@ -112,6 +123,7 @@ const mockedUploadSharedNonIntegratedFile = jest.mocked(
|
||||
uploadSharedNonIntegratedFile,
|
||||
);
|
||||
const mockedFetchSubstrates = jest.mocked(fetchSubstrates);
|
||||
const mockedCreateApprovalWorkflow = jest.mocked(createApprovalWorkflow);
|
||||
|
||||
function createSubstrateOption(overrides: {
|
||||
id: string;
|
||||
@ -334,6 +346,7 @@ describe("NonIntegratedDetailClient", () => {
|
||||
mockedSaveSharedNonIntegratedProduct.mockImplementation(async (product) => product);
|
||||
mockedUploadSharedNonIntegratedFile.mockReset();
|
||||
mockedFetchSubstrates.mockReset();
|
||||
mockedCreateApprovalWorkflow.mockClear();
|
||||
mockedFetchSubstrates.mockResolvedValue([
|
||||
createSubstrateOption({
|
||||
id: "substrate-ice",
|
||||
@ -357,6 +370,12 @@ describe("NonIntegratedDetailClient", () => {
|
||||
systemName: "20ENF级橡胶木",
|
||||
}),
|
||||
]);
|
||||
Object.defineProperty(navigator, "clipboard", {
|
||||
value: {
|
||||
writeText: jest.fn(async () => undefined),
|
||||
},
|
||||
configurable: true,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -367,9 +386,9 @@ describe("NonIntegratedDetailClient", () => {
|
||||
const actor = userEvent.setup();
|
||||
const { container } = render(<NonIntegratedDetailClient mode="create" />);
|
||||
|
||||
await actor.type(screen.getByLabelText("开品名称"), "测试开品");
|
||||
await actor.type(screen.getByLabelText("临时编号"), "DNZ0001");
|
||||
await actor.type(screen.getByLabelText("设计师"), "测试设计师");
|
||||
expect(screen.getByLabelText("审批编号")).toHaveValue("PD20260505175418001");
|
||||
expect(screen.getByLabelText("产品图片")).toBeInTheDocument();
|
||||
await actor.type(screen.getByLabelText("名称"), "测试开品");
|
||||
|
||||
const imageFile = new File(["image"], "preview.png", { type: "image/png" });
|
||||
const fileInputs = container.querySelectorAll('input[type="file"]');
|
||||
@ -383,17 +402,26 @@ describe("NonIntegratedDetailClient", () => {
|
||||
) as NonIntegratedProductRecord[];
|
||||
|
||||
expect(savedProducts).toHaveLength(1);
|
||||
expect(savedProducts[0]?.spu).toBe("DNZ0001");
|
||||
expect(savedProducts[0]?.spu).toBe("PD20260505175418001");
|
||||
expect(savedProducts[0]?.projectStatus).toBe("draft");
|
||||
expect(savedProducts[0]?.brand).toBe("基本盘");
|
||||
expect(savedProducts[0]?.designer).toBe("测试设计师");
|
||||
expect(savedProducts[0]?.openingReference.productName).toBe("测试开品");
|
||||
expect(savedProducts[0]?.openingReference.temporaryCode).toBe("DNZ0001");
|
||||
expect(savedProducts[0]?.openingReference.temporaryCode).toBe(
|
||||
"PD20260505175418001",
|
||||
);
|
||||
expect(savedProducts[0]?.previewImage).toContain(
|
||||
"data:image/png;base64,dGVzdA==",
|
||||
);
|
||||
expect(mockedCreateApprovalWorkflow).toHaveBeenCalledWith({
|
||||
productId: savedProducts[0]?.id,
|
||||
approvalNumber: "PD20260505175418001",
|
||||
title: "测试开品",
|
||||
businessRoles: [],
|
||||
});
|
||||
expect(push).toHaveBeenCalledWith(
|
||||
`/intake/non-integrated/${encodeURIComponent(savedProducts[0]!.id)}`,
|
||||
`/intake/non-integrated/${encodeURIComponent(
|
||||
savedProducts[0]!.id,
|
||||
)}?workspace=opening-reference`,
|
||||
);
|
||||
});
|
||||
|
||||
@ -401,7 +429,7 @@ describe("NonIntegratedDetailClient", () => {
|
||||
const actor = userEvent.setup();
|
||||
const { container } = render(<NonIntegratedDetailClient mode="create" />);
|
||||
|
||||
await actor.type(screen.getByLabelText("开品名称"), "仅填写名称的开品");
|
||||
await actor.type(screen.getByLabelText("名称"), "仅填写名称的开品");
|
||||
|
||||
const imageFile = new File(["image"], "preview.png", { type: "image/png" });
|
||||
const fileInputs = container.querySelectorAll('input[type="file"]');
|
||||
@ -418,7 +446,9 @@ describe("NonIntegratedDetailClient", () => {
|
||||
expect(savedProducts[0]?.openingReference.productName).toBe(
|
||||
"仅填写名称的开品",
|
||||
);
|
||||
expect(savedProducts[0]?.openingReference.temporaryCode).toMatch(/^KP-\d+$/);
|
||||
expect(savedProducts[0]?.openingReference.temporaryCode).toBe(
|
||||
"PD20260505175418001",
|
||||
);
|
||||
expect(savedProducts[0]?.spu).toBe(
|
||||
savedProducts[0]?.openingReference.temporaryCode,
|
||||
);
|
||||
@ -553,7 +583,23 @@ describe("NonIntegratedDetailClient", () => {
|
||||
it("keeps brand as a single value when adaptable brands change", async () => {
|
||||
const actor = userEvent.setup();
|
||||
|
||||
render(<NonIntegratedDetailClient mode="create" />);
|
||||
saveNonIntegratedProduct(
|
||||
createProductRecord({
|
||||
id: "DNZ0001-brand-single",
|
||||
spu: "DNZ0001",
|
||||
openingReference: {
|
||||
...createEmptyNonIntegratedOpeningReference(),
|
||||
productName: "测试开品",
|
||||
temporaryCode: "DNZ0001",
|
||||
brand: "基本盘",
|
||||
adaptableBrands: ["基本盘"],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
render(
|
||||
<NonIntegratedDetailClient mode="edit" productId="DNZ0001-brand-single" />,
|
||||
);
|
||||
|
||||
const brandSelect = screen.getByLabelText("品牌");
|
||||
const adaptableBrandButton = screen.getByRole("button", {
|
||||
@ -574,7 +620,6 @@ describe("NonIntegratedDetailClient", () => {
|
||||
await actor.click(adaptableBrandButton);
|
||||
await actor.click(screen.getByLabelText("基本盘"));
|
||||
expect(brandSelect).toHaveValue("顾家");
|
||||
expect(screen.getByLabelText("临时编号")).toHaveValue("");
|
||||
});
|
||||
|
||||
it("shows the document sidebar in the updated workflow order", () => {
|
||||
@ -643,8 +688,6 @@ describe("NonIntegratedDetailClient", () => {
|
||||
"document-workspace-trigger-产品定厂",
|
||||
"document-workspace-trigger-样包测量",
|
||||
"document-workspace-trigger-产品核价",
|
||||
"document-workspace-trigger-工厂质检",
|
||||
"document-workspace-trigger-检测报告",
|
||||
]);
|
||||
expect(
|
||||
screen.queryByTestId("document-workspace-trigger-立项评审"),
|
||||
@ -652,13 +695,15 @@ describe("NonIntegratedDetailClient", () => {
|
||||
|
||||
const reviewTrigger = screen.getByTestId("document-workspace-trigger-评审资料");
|
||||
const modelTrigger = screen.getByTestId("document-workspace-trigger-产品模型");
|
||||
const reportTrigger = screen.getByTestId("document-workspace-trigger-检测报告");
|
||||
|
||||
expect(reviewTrigger).toHaveAttribute("data-has-files", "true");
|
||||
expect(modelTrigger).toHaveAttribute("data-has-files", "true");
|
||||
expect(reportTrigger).toHaveAttribute("data-has-files", "false");
|
||||
expect(reportTrigger).toHaveAttribute("data-pending-confirmation", "true");
|
||||
expect(reportTrigger).toHaveClass("bg-[#fff7ea]");
|
||||
expect(
|
||||
screen.queryByTestId("document-workspace-trigger-工厂质检"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId("document-workspace-trigger-检测报告"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(reviewTrigger).not.toHaveClass("bg-[#e6ebf0]");
|
||||
expect(
|
||||
Array.from(
|
||||
@ -721,8 +766,6 @@ describe("NonIntegratedDetailClient", () => {
|
||||
"document-workspace-trigger-产品定厂",
|
||||
"document-workspace-trigger-样包测量",
|
||||
"document-workspace-trigger-产品核价",
|
||||
"document-workspace-trigger-工厂质检",
|
||||
"document-workspace-trigger-检测报告",
|
||||
]);
|
||||
expect(
|
||||
sidebar.querySelector<HTMLButtonElement>(
|
||||
@ -884,7 +927,14 @@ describe("NonIntegratedDetailClient", () => {
|
||||
|
||||
await actor.click(screen.getByTestId("document-workspace-trigger-专利申请"));
|
||||
|
||||
expect(screen.getByLabelText("是否申请专利")).toHaveValue("待判断");
|
||||
const patentApplicationDecision = screen.getByRole("radiogroup", {
|
||||
name: "是否申请专利",
|
||||
});
|
||||
expect(
|
||||
within(patentApplicationDecision).getByRole("radio", {
|
||||
name: "待判断",
|
||||
}),
|
||||
).toHaveAttribute("aria-checked", "true");
|
||||
expect(screen.getByTestId("patent-application-pending-choice")).toHaveTextContent(
|
||||
"请先选择是否申请专利",
|
||||
);
|
||||
@ -892,7 +942,9 @@ describe("NonIntegratedDetailClient", () => {
|
||||
screen.queryByTestId("patent-application-ledger-fields"),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
await actor.selectOptions(screen.getByLabelText("是否申请专利"), "申请");
|
||||
await actor.click(
|
||||
within(patentApplicationDecision).getByRole("radio", { name: "是" }),
|
||||
);
|
||||
|
||||
const ledgerFields = screen.getByTestId("patent-application-ledger-fields");
|
||||
const ledgerText = ledgerFields.textContent ?? "";
|
||||
@ -1021,7 +1073,11 @@ describe("NonIntegratedDetailClient", () => {
|
||||
);
|
||||
|
||||
await actor.click(screen.getByTestId("document-workspace-trigger-专利申请"));
|
||||
await actor.selectOptions(screen.getByLabelText("是否申请专利"), "不申请");
|
||||
await actor.click(
|
||||
within(
|
||||
screen.getByRole("radiogroup", { name: "是否申请专利" }),
|
||||
).getByRole("radio", { name: "否" }),
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("patent-application-not-needed")).toHaveTextContent(
|
||||
"产品无需申请专利",
|
||||
@ -1297,9 +1353,12 @@ describe("NonIntegratedDetailClient", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
await actor.click(screen.getByTestId("document-workspace-trigger-工厂质检"));
|
||||
await actor.click(screen.getByTestId("document-workspace-trigger-包装细节"));
|
||||
|
||||
const panel = screen.getByTestId("document-workspace-panel");
|
||||
expect(
|
||||
screen.getByTestId("packaging-factory-quality-section"),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByTestId("factory-quality-workspace")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("factory-quality-style-K01")).toHaveTextContent(
|
||||
"SZ0230K01",
|
||||
@ -1689,6 +1748,45 @@ describe("NonIntegratedDetailClient", () => {
|
||||
).toHaveAttribute("data-active", "true");
|
||||
});
|
||||
|
||||
it("keeps the sidebar on basic info instead of falling back to opening reference", async () => {
|
||||
const actor = userEvent.setup();
|
||||
|
||||
window.localStorage.setItem(
|
||||
NON_INTEGRATED_STORAGE_KEY,
|
||||
JSON.stringify([
|
||||
createProductRecord({
|
||||
id: "DNZ0001-basic-info-sidebar",
|
||||
uploadFiles: [
|
||||
{
|
||||
id: "workspace-photo-basic-info",
|
||||
name: "产品实拍图.png",
|
||||
category: "产品实拍图",
|
||||
dataUrl: "data:image/png;base64,test-image",
|
||||
mimeType: "image/png",
|
||||
},
|
||||
],
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
render(
|
||||
<NonIntegratedDetailClient
|
||||
mode="edit"
|
||||
productId="DNZ0001-basic-info-sidebar"
|
||||
/>,
|
||||
);
|
||||
|
||||
await actor.click(screen.getByTestId("document-workspace-trigger-产品实拍图"));
|
||||
await actor.click(screen.getByTestId("document-workspace-basic-info-trigger"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByTestId("document-workspace-basic-info-trigger"),
|
||||
).toHaveAttribute("data-active", "true");
|
||||
});
|
||||
expect(screen.queryByTestId("opening-reference-workspace")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("marks copied document categories as pending until the current category is confirmed", async () => {
|
||||
const actor = userEvent.setup();
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user