fix(intake): isolate node confirmation updates

This commit is contained in:
WuYongJie 2026-05-08 11:17:01 +08:00
parent 8761242931
commit 368b4427f0
2 changed files with 203 additions and 59 deletions

View File

@ -1920,6 +1920,69 @@ describe("NonIntegratedDetailClient", () => {
).not.toBeInTheDocument();
});
it("keeps product data approved when adding a SKU in basic info", async () => {
const actor = userEvent.setup();
const approvedAt = "2026-04-28T06:30:00.000Z";
window.localStorage.setItem(
NON_INTEGRATED_STORAGE_KEY,
JSON.stringify([
createProductRecord({
id: "DNZ0001-sku-independent-confirmation",
skuItems: [createSkuItem()],
uploadFiles: [
{
id: "product-data-file-1",
name: "model.pdf",
category: "产品模型",
dataUrl: "data:application/pdf;base64,test-pdf",
mimeType: "application/pdf",
},
],
documentWorkspaceConfirmations: {
...createDefaultDocumentWorkspaceConfirmations(),
[NON_INTEGRATED_BASIC_INFO_WORKSPACE_CATEGORY]: true,
产品模型: true,
},
documentWorkspaceApprovedAt: {
[NON_INTEGRATED_BASIC_INFO_WORKSPACE_CATEGORY]: approvedAt,
产品模型: approvedAt,
},
}),
]),
);
render(
<NonIntegratedDetailClient
mode="edit"
productId="DNZ0001-sku-independent-confirmation"
currentUserRole="SUPER_ADMIN"
/>,
);
const productDataTrigger = screen.getByTestId(
"document-workspace-trigger-产品模型",
);
expect(productDataTrigger).toHaveAttribute(
"data-pending-confirmation",
"false",
);
await actor.click(screen.getByTestId("document-workspace-basic-info-trigger"));
await actor.click(screen.getByTestId("non-integrated-add-sku"));
await waitFor(() => {
expect(productDataTrigger).toHaveAttribute(
"data-pending-confirmation",
"false",
);
expect(
screen.getByTestId("document-workspace-basic-info-trigger"),
).toHaveAttribute("data-pending-confirmation", "true");
expect(screen.getByTestId("workflow-approve-current-node")).toBeEnabled();
});
});
it("counts review data as zero until the first round has a filled issue", () => {
window.localStorage.setItem(
NON_INTEGRATED_STORAGE_KEY,

View File

@ -114,7 +114,6 @@ import {
getNonIntegratedFormalSpu,
getNonIntegratedProductById,
getNonIntegratedProductDisplayName,
hasPendingDocumentWorkspaceConfirmation,
normalizeNonIntegratedOpeningReference,
normalizeNonIntegratedPatentApplication,
normalizeNonIntegratedProjectReview,
@ -935,6 +934,24 @@ const DEFAULT_DOCUMENT_WORKSPACE_CATEGORY: DocumentWorkspaceCategory =
const PRODUCT_PHOTO_CATEGORY: DocumentWorkspaceCategory = "产品实拍图";
const AUTO_SAVE_DELAY_MS = 500;
function getChangedWorkflowNodeCategories(
previousSignatures: Record<WorkflowNodeCategory, string> | null | undefined,
currentSignatures: Record<WorkflowNodeCategory, string>,
projectStatus: NonIntegratedProjectStatus,
) {
if (!previousSignatures) return [];
return NON_INTEGRATED_WORKFLOW_NODE_CATEGORIES.filter((category) => {
const changed = previousSignatures[category] !== currentSignatures[category];
if (!changed) return false;
return !(
category === PROJECT_REVIEW_WORKSPACE_CATEGORY &&
projectStatus === "approved"
);
});
}
const PATENT_APPLICATION_STATUS_OPTIONS: NonIntegratedPatentApplication["patentStatus"][] =
["待确认", "已有专利", "申请中", "申请失败", "无专利", "放弃维护"];
const PATENT_APPLICATION_DECISION_OPTIONS: Array<{
@ -1810,6 +1827,40 @@ function hasPackagingStyleStructuredContent(detail: {
);
}
function buildInstallationRevisionSignature(
styleDetails: Record<string, StyleDetailForm>,
) {
return Object.fromEntries(
Object.entries(styleDetails).map(([styleKey, detail]) => [
styleKey,
{
installMethod: detail.installMethod,
installSteps: detail.installSteps,
},
]),
);
}
function buildPackagingRevisionSignature(
styleDetails: Record<string, StyleDetailForm>,
) {
return Object.fromEntries(
Object.entries(styleDetails).map(([styleKey, detail]) => [
styleKey,
{
packageDetail: detail.packageDetail,
craftDetail: detail.craftDetail,
outerPackagingType: detail.outerPackagingType,
innerPackagingItems: detail.innerPackagingItems,
packagingModel3dFileName: detail.packagingModel3dFileName,
packagingModel3dDataUrl: detail.packagingModel3dDataUrl,
packagingModel3dNodeOffsets: detail.packagingModel3dNodeOffsets,
packagingModel3dNodeRotations: detail.packagingModel3dNodeRotations,
},
]),
);
}
function createEmptySkuItem(): NonIntegratedSkuItem {
return {
id: buildNonIntegratedSkuId(),
@ -7127,8 +7178,7 @@ export default function NonIntegratedDetailClient({
files: filesForCategory(OPENING_REFERENCE_WORKSPACE_CATEGORY),
});
signatures[PRODUCT_DATA_WORKSPACE_CATEGORY] = JSON.stringify({
formValues,
skuItems,
note: documentWorkspaceNotes[PRODUCT_DATA_WORKSPACE_CATEGORY],
files: filesForCategory(PRODUCT_DATA_WORKSPACE_CATEGORY),
});
signatures["设计卖点"] = JSON.stringify({
@ -7152,20 +7202,20 @@ export default function NonIntegratedDetailClient({
files: filesForCategory(PRODUCT_PHOTO_CATEGORY),
});
signatures[INSTALLATION_WORKSPACE_CATEGORY] = JSON.stringify({
styleDetails,
styleDetails: buildInstallationRevisionSignature(styleDetails),
files: filesForCategory(INSTALLATION_WORKSPACE_CATEGORY),
});
signatures[PACKAGING_WORKSPACE_CATEGORY] = JSON.stringify({
styleDetails,
styleDetails: buildPackagingRevisionSignature(styleDetails),
files: filesForCategory(PACKAGING_WORKSPACE_CATEGORY),
});
signatures[PRODUCT_FACTORY_WORKSPACE_CATEGORY] =
JSON.stringify(productFactoryRows);
JSON.stringify(factoryRows);
signatures[SAMPLE_MEASUREMENT_WORKSPACE_CATEGORY] = JSON.stringify(
productSampleMeasurementRows,
sampleMeasurementRows,
);
signatures[PRODUCT_PRICING_WORKSPACE_CATEGORY] =
JSON.stringify(productPricingRows);
JSON.stringify(pricingRows);
signatures[DETECTION_WORKSPACE_CATEGORY] = JSON.stringify({
files: filesForCategory(DETECTION_WORKSPACE_CATEGORY).filter(
(file) => resolveDetectionWorkspaceAssetType(file) === "quality-standard",
@ -7182,16 +7232,16 @@ export default function NonIntegratedDetailClient({
return signatures;
}, [
documentWorkspaceNotes,
factoryRows,
formValues,
openingReference,
patentApplication,
productFactoryRows,
productPricingRows,
productSampleMeasurementRows,
projectReview,
projectStatus,
qualityInspectionSheet,
pricingRows,
reviewRounds,
sampleMeasurementRows,
skuItems,
styleDetails,
uploadFiles,
@ -7199,6 +7249,21 @@ export default function NonIntegratedDetailClient({
const activeDocumentWorkspaceHasContent =
(documentWorkspaceCountByCategory[activeDocumentWorkspaceCategory] ?? 0) >
0;
const dirtyWorkflowNodeCategories = new Set(
getChangedWorkflowNodeCategories(
documentWorkspaceRevisionSignatureRef.current,
documentWorkspaceRevisionSignatures,
projectStatus,
),
);
const isWorkflowNodeConfirmed = (
category: WorkflowNodeCategory,
hasContent: boolean,
) =>
hasContent &&
documentWorkspaceConfirmations[category] === true &&
Boolean(documentWorkspaceApprovedAt[category]) &&
!dirtyWorkflowNodeCategories.has(category);
const activeWorkflowNodeCategory: WorkflowNodeCategory =
documentWorkspaceView === "detail"
? NON_INTEGRATED_BASIC_INFO_WORKSPACE_CATEGORY
@ -7209,16 +7274,15 @@ export default function NonIntegratedDetailClient({
: activeDocumentWorkspaceHasContent;
const activeWorkflowNodeApprovedAt =
documentWorkspaceApprovedAt[activeWorkflowNodeCategory] ?? "";
const activeWorkflowNodeIsApproved =
activeWorkflowNodeHasContent &&
documentWorkspaceConfirmations[activeWorkflowNodeCategory] === true &&
Boolean(activeWorkflowNodeApprovedAt);
const activeWorkflowNodeIsApproved = isWorkflowNodeConfirmed(
activeWorkflowNodeCategory,
activeWorkflowNodeHasContent,
);
const activeWorkflowNodeNeedsApproval = !activeWorkflowNodeIsApproved;
const basicInfoIsApproved =
basicInfoHasContent &&
documentWorkspaceConfirmations[NON_INTEGRATED_BASIC_INFO_WORKSPACE_CATEGORY] ===
true &&
Boolean(documentWorkspaceApprovedAt[NON_INTEGRATED_BASIC_INFO_WORKSPACE_CATEGORY]);
const basicInfoIsApproved = isWorkflowNodeConfirmed(
NON_INTEGRATED_BASIC_INFO_WORKSPACE_CATEGORY,
basicInfoHasContent,
);
const basicInfoDisplayCount = basicInfoIsApproved ? 1 : 0;
const basicInfoNeedsConfirmation = !basicInfoIsApproved;
const isWorkflowCompleted = false;
@ -7232,18 +7296,10 @@ export default function NonIntegratedDetailClient({
return;
}
const changedCategories = NON_INTEGRATED_WORKFLOW_NODE_CATEGORIES.filter(
(category) => {
const changed =
previousSignatures[category] !==
documentWorkspaceRevisionSignatures[category];
if (!changed) return false;
return !(
category === PROJECT_REVIEW_WORKSPACE_CATEGORY &&
projectStatus === "approved"
);
},
const changedCategories = getChangedWorkflowNodeCategories(
previousSignatures,
documentWorkspaceRevisionSignatures,
projectStatus,
);
documentWorkspaceRevisionSignatureRef.current =
documentWorkspaceRevisionSignatures;
@ -7273,19 +7329,20 @@ export default function NonIntegratedDetailClient({
return changed ? next : current;
});
}, [documentWorkspaceRevisionSignatures, isWorkflowCompleted, projectStatus]);
const pendingDocumentWorkspaceCategories = useMemo(
() =>
UPLOAD_CATEGORY_PANEL_ORDER.filter(
(category) => !documentWorkspaceConfirmations[category],
const pendingDocumentWorkspaceCategories = UPLOAD_CATEGORY_PANEL_ORDER.filter(
(category) =>
!isWorkflowNodeConfirmed(
category,
(documentWorkspaceCountByCategory[category] ?? 0) > 0,
),
[documentWorkspaceConfirmations],
);
const hasPendingDocumentWorkspaceCategories =
hasPendingDocumentWorkspaceConfirmation({
documentWorkspaceConfirmations,
});
pendingDocumentWorkspaceCategories.length > 0;
const activeDocumentWorkspaceNeedsConfirmation =
!documentWorkspaceConfirmations[activeDocumentWorkspaceCategory];
!isWorkflowNodeConfirmed(
activeDocumentWorkspaceCategory,
activeDocumentWorkspaceHasContent,
);
const activeDocumentWorkspaceCopy =
DOCUMENT_WORKSPACE_COPY[activeDocumentWorkspaceCategory];
const detectionAttachmentFiles = useMemo(
@ -10369,10 +10426,10 @@ export default function NonIntegratedDetailClient({
? basicInfoHasContent
: (documentWorkspaceCountByCategory[category as DocumentWorkspaceCategory] ??
0) > 0;
const isCategoryApproved =
hasCategoryContent &&
documentWorkspaceConfirmations[category] === true &&
Boolean(documentWorkspaceApprovedAt[category]);
const isCategoryApproved = isWorkflowNodeConfirmed(
category,
hasCategoryContent,
);
if (isReadOnly || isCategoryApproved) return;
if (!hasCategoryContent) {
@ -10560,7 +10617,33 @@ export default function NonIntegratedDetailClient({
}
function renderDocumentWorkspaceConfirmationAction() {
return null;
const category = activeDocumentWorkspaceCategory;
const hasContent = activeDocumentWorkspaceHasContent;
const isApproved = isWorkflowNodeConfirmed(category, hasContent);
const label = getDocumentWorkspaceCategoryLabel(category);
if (isApproved) {
return (
<span
data-testid={`document-workspace-confirmed-${category}`}
className="inline-flex min-h-8 items-center rounded-apple-md border border-[#cdebd9] bg-[#e8f6ee] px-3 text-xs font-apple-medium text-[#156b3f]"
>
已确认
</span>
);
}
return (
<Button
size="small"
data-testid={`document-workspace-confirm-${category}`}
disabled={isReadOnly || !hasContent}
title={hasContent ? undefined : `${label}还没有资料,暂不能确认`}
onClick={() => void handleConfirmDocumentWorkspaceCategory(category)}
>
确认{label}
</Button>
);
}
async function handleApproveCurrentWorkflowNode() {
@ -10626,7 +10709,7 @@ export default function NonIntegratedDetailClient({
: "无资料";
return (
<div className="mb-3 shrink-0">
<div className="sticky top-0 z-20 mb-3 shrink-0 bg-[var(--color-surface-container)] pb-1">
<div
className={`grid min-h-[34px] grid-cols-[minmax(0,1fr)_auto] items-center gap-2 rounded-apple-lg border px-3 py-1.5 shadow-apple-xs ${
isFlowAbandoned
@ -14710,11 +14793,7 @@ function renderFactoryQualityWorkspace() {
hasContent: boolean;
label: string;
}) {
const approvedAt = documentWorkspaceApprovedAt[category] ?? "";
const isApproved =
hasContent &&
documentWorkspaceConfirmations[category] === true &&
Boolean(approvedAt);
const isApproved = isWorkflowNodeConfirmed(category, hasContent);
if (isApproved) {
return (
@ -16580,7 +16659,11 @@ function renderFactoryQualityWorkspace() {
<div className="space-y-3">
<div className="flex items-center gap-2">
{isReadOnly ? null : (
<Button size="small" onClick={handleAddSku}>
<Button
size="small"
data-testid="non-integrated-add-sku"
onClick={handleAddSku}
>
新增SKU
</Button>
)}
@ -18905,12 +18988,10 @@ function renderFactoryQualityWorkspace() {
documentWorkspaceCountByCategory[category] ??
files.length;
const hasFiles = fileCount > 0;
const needsConfirmation =
!(
hasFiles &&
documentWorkspaceConfirmations[category] === true &&
Boolean(documentWorkspaceApprovedAt[category])
);
const needsConfirmation = !isWorkflowNodeConfirmed(
category,
hasFiles,
);
const shouldInsertBasicInfoTrigger =
index === BASIC_INFO_INSERT_INDEX;