fix(intake): isolate node confirmation updates
This commit is contained in:
parent
8761242931
commit
368b4427f0
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user