@decocms/blocks 7.20.8 → 7.20.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cms/resolve.test.ts +70 -0
- package/src/cms/resolve.ts +13 -3
package/package.json
CHANGED
package/src/cms/resolve.test.ts
CHANGED
|
@@ -638,3 +638,73 @@ describe("resolveDecoPage — #277 client-side navigation disables deferral", ()
|
|
|
638
638
|
expect(result?.deferredSections).toHaveLength(0);
|
|
639
639
|
});
|
|
640
640
|
});
|
|
641
|
+
|
|
642
|
+
// ---------------------------------------------------------------------------
|
|
643
|
+
// Hidden array items — a `never`-gated multivariate wrapper must vanish from
|
|
644
|
+
// the array, not survive as a `null` hole the section renders as empty.
|
|
645
|
+
//
|
|
646
|
+
// The admin (Studio) hides an array item by wrapping it in
|
|
647
|
+
// `{ __resolveType: "website/flags/multivariate.ts", variants: [{ value, rule:
|
|
648
|
+
// { __resolveType: "website/matchers/never.ts" } }] }`. `never` never matches,
|
|
649
|
+
// so the wrapper resolves to "not present" and the item is dropped.
|
|
650
|
+
// ---------------------------------------------------------------------------
|
|
651
|
+
|
|
652
|
+
describe("hidden array items (never matcher)", () => {
|
|
653
|
+
const NEVER = "website/matchers/never.ts";
|
|
654
|
+
const ALWAYS = "website/matchers/always.ts";
|
|
655
|
+
const MULTIVARIATE = "website/flags/multivariate.ts";
|
|
656
|
+
|
|
657
|
+
const hidden = (value: unknown) => ({
|
|
658
|
+
__resolveType: MULTIVARIATE,
|
|
659
|
+
variants: [{ value, rule: { __resolveType: NEVER } }],
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
it("drops a hidden item from an array instead of leaving a null hole", async () => {
|
|
663
|
+
const benefits = [
|
|
664
|
+
{ label: "Parcelamento" },
|
|
665
|
+
{ label: "Frete" },
|
|
666
|
+
hidden({ label: "RetiradaLoja" }),
|
|
667
|
+
hidden({ label: "EntregaExpressa" }),
|
|
668
|
+
];
|
|
669
|
+
|
|
670
|
+
const result = (await resolveValue({ benefits }, undefined, {})) as {
|
|
671
|
+
benefits: unknown[];
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
expect(result.benefits).toEqual([{ label: "Parcelamento" }, { label: "Frete" }]);
|
|
675
|
+
// No null holes.
|
|
676
|
+
expect(result.benefits).not.toContain(null);
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
it("keeps a visible (always-gated) item in the array", async () => {
|
|
680
|
+
const benefits = [
|
|
681
|
+
{
|
|
682
|
+
__resolveType: MULTIVARIATE,
|
|
683
|
+
variants: [{ value: { label: "Shown" }, rule: { __resolveType: ALWAYS } }],
|
|
684
|
+
},
|
|
685
|
+
hidden({ label: "Hidden" }),
|
|
686
|
+
];
|
|
687
|
+
|
|
688
|
+
const result = (await resolveValue({ benefits }, undefined, {})) as {
|
|
689
|
+
benefits: unknown[];
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
expect(result.benefits).toEqual([{ label: "Shown" }]);
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
it("preserves a legitimate null value in an array (matched variant resolving to null)", async () => {
|
|
696
|
+
// A matched variant whose value is literally null stays — only the
|
|
697
|
+
// no-match sentinel is filtered, not every null.
|
|
698
|
+
const items = [
|
|
699
|
+
{
|
|
700
|
+
__resolveType: MULTIVARIATE,
|
|
701
|
+
variants: [{ value: null, rule: { __resolveType: ALWAYS } }],
|
|
702
|
+
},
|
|
703
|
+
{ label: "kept" },
|
|
704
|
+
];
|
|
705
|
+
|
|
706
|
+
const result = (await resolveValue({ items }, undefined, {})) as { items: unknown[] };
|
|
707
|
+
|
|
708
|
+
expect(result.items).toEqual([null, { label: "kept" }]);
|
|
709
|
+
});
|
|
710
|
+
});
|
package/src/cms/resolve.ts
CHANGED
|
@@ -780,7 +780,14 @@ async function resolveProps(
|
|
|
780
780
|
async function internalResolve(value: unknown, rctx: ResolveContext): Promise<unknown> {
|
|
781
781
|
if (!value || typeof value !== "object") return value;
|
|
782
782
|
if (Array.isArray(value)) {
|
|
783
|
-
|
|
783
|
+
const resolved = await Promise.all(value.map((item) => internalResolve(item, rctx)));
|
|
784
|
+
// Drop items that resolved to "not present" (`undefined`) — e.g. a hidden
|
|
785
|
+
// array item (a multivariate wrapped in a `never` matcher, see the admin's
|
|
786
|
+
// hideArrayItem) where no variant matched. A `never`-gated item must vanish
|
|
787
|
+
// from the array, NOT become a `null` hole the section then renders as an
|
|
788
|
+
// empty card. Legitimate `null` values (a matched variant that resolved to
|
|
789
|
+
// null, a failed loader) are kept — only the no-match sentinel is filtered.
|
|
790
|
+
return resolved.filter((item) => item !== undefined);
|
|
784
791
|
}
|
|
785
792
|
|
|
786
793
|
const obj = value as Record<string, unknown>;
|
|
@@ -833,7 +840,10 @@ async function internalResolve(value: unknown, rctx: ResolveContext): Promise<un
|
|
|
833
840
|
resolveType === WELL_KNOWN_TYPES.MULTIVARIATE_SECTION
|
|
834
841
|
) {
|
|
835
842
|
const variants = obj.variants as Array<{ value: unknown; rule?: unknown }> | undefined;
|
|
836
|
-
|
|
843
|
+
// No variant matched (incl. a single `never`-gated variant, i.e. a hidden
|
|
844
|
+
// item) → resolve to `undefined`, the "not present" sentinel the array
|
|
845
|
+
// branch filters out, rather than `null`, which would survive as a hole.
|
|
846
|
+
if (!variants || variants.length === 0) return undefined;
|
|
837
847
|
|
|
838
848
|
for (const variant of variants) {
|
|
839
849
|
const rule = variant.rule as Record<string, unknown> | undefined;
|
|
@@ -841,7 +851,7 @@ async function internalResolve(value: unknown, rctx: ResolveContext): Promise<un
|
|
|
841
851
|
return internalResolve(variant.value, childCtx);
|
|
842
852
|
}
|
|
843
853
|
}
|
|
844
|
-
return
|
|
854
|
+
return undefined;
|
|
845
855
|
}
|
|
846
856
|
|
|
847
857
|
// Commerce loaders
|