Skip to content

Commit 56daba5

Browse files
Merge pull request #511 from contentstack/develop_v4
2nd October release
2 parents de3a9db + e2bd6bd commit 56daba5

File tree

10 files changed

+166
-20
lines changed

10 files changed

+166
-20
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import React from "preact/compat";
2+
import { VariantIcon } from "./icons/variant";
3+
import { visualBuilderStyles } from "../visualBuilder.style";
4+
5+
export function VariantIndicator(): JSX.Element {
6+
return (
7+
<div className={visualBuilderStyles()["visual-builder__variant-indicator"]}>
8+
<VariantIcon size="18px" />
9+
</div>
10+
);
11+
12+
}

src/visualBuilder/components/__test__/fieldLabelWrapper.test.tsx

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,13 @@ vi.mock("../generators/generateCustomCursor", () => ({
118118
}));
119119

120120
vi.mock("../visualBuilder.style", () => ({
121-
visualBuilderStyles: vi.fn().mockReturnValue({}),
121+
visualBuilderStyles: vi.fn().mockReturnValue({
122+
"visual-builder__focused-toolbar--variant": "visual-builder__focused-toolbar--variant"
123+
}),
124+
}));
125+
126+
vi.mock("../VariantIndicator", () => ({
127+
VariantIndicator: () => <div data-testid="variant-indicator">Variant</div>
122128
}));
123129

124130
vi.mock("../../utils/errorHandling", () => ({
@@ -387,4 +393,78 @@ describe("FieldLabelWrapperComponent", () => {
387393
const contentTypeIcon = container.querySelector(".visual-builder__content-type-icon");
388394
expect(contentTypeIcon).not.toBeInTheDocument();
389395
});
396+
397+
test("renders VariantIndicator when field has variant", async () => {
398+
const variantFieldMetadata = {
399+
...mockFieldMetadata,
400+
variant: "variant-uid-123"
401+
};
402+
403+
const { findByTestId } = await asyncRender(
404+
<FieldLabelWrapperComponent
405+
fieldMetadata={variantFieldMetadata}
406+
eventDetails={mockEventDetails}
407+
parentPaths={[]}
408+
getParentEditableElement={mockGetParentEditable}
409+
/>
410+
);
411+
412+
const variantIndicator = await findByTestId("variant-indicator");
413+
expect(variantIndicator).toBeInTheDocument();
414+
});
415+
416+
test("does not render VariantIndicator when field has no variant", async () => {
417+
const { container } = await asyncRender(
418+
<FieldLabelWrapperComponent
419+
fieldMetadata={mockFieldMetadata}
420+
eventDetails={mockEventDetails}
421+
parentPaths={[]}
422+
getParentEditableElement={mockGetParentEditable}
423+
/>
424+
);
425+
426+
await waitFor(() => {
427+
const variantIndicator = container.querySelector("[data-testid='variant-indicator']");
428+
expect(variantIndicator).not.toBeInTheDocument();
429+
});
430+
});
431+
432+
test("applies variant CSS classes when field has variant", async () => {
433+
const variantFieldMetadata = {
434+
...mockFieldMetadata,
435+
variant: "variant-uid-123"
436+
};
437+
438+
const { findByTestId } = await asyncRender(
439+
<FieldLabelWrapperComponent
440+
fieldMetadata={variantFieldMetadata}
441+
eventDetails={mockEventDetails}
442+
parentPaths={[]}
443+
getParentEditableElement={mockGetParentEditable}
444+
/>
445+
);
446+
447+
const fieldLabelWrapper = await findByTestId("visual-builder__focused-toolbar__field-label-wrapper");
448+
449+
await waitFor(() => {
450+
expect(fieldLabelWrapper).toHaveClass("visual-builder__focused-toolbar--variant");
451+
});
452+
});
453+
454+
test("does not apply variant CSS classes when field has no variant", async () => {
455+
const { findByTestId } = await asyncRender(
456+
<FieldLabelWrapperComponent
457+
fieldMetadata={mockFieldMetadata}
458+
eventDetails={mockEventDetails}
459+
parentPaths={[]}
460+
getParentEditableElement={mockGetParentEditable}
461+
/>
462+
);
463+
464+
const fieldLabelWrapper = await findByTestId("visual-builder__focused-toolbar__field-label-wrapper");
465+
466+
await waitFor(() => {
467+
expect(fieldLabelWrapper).not.toHaveClass("visual-builder__focused-toolbar--variant");
468+
});
469+
});
390470
});

src/visualBuilder/components/fieldLabelWrapper.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { VisualBuilderPostMessageEvents } from "../utils/types/postMessage.types
1717
import { ContentTypeIcon } from "./icons";
1818
import { ToolbarTooltip } from "./Tooltip";
1919
import { fetchEntryPermissionsAndStageDetails } from "../utils/fetchEntryPermissionsAndStageDetails";
20+
import { VariantIndicator } from "./VariantIndicator";
2021

2122
interface ReferenceParentMap {
2223
[entryUid: string]: {
@@ -246,6 +247,7 @@ function FieldLabelWrapperComponent(
246247
]
247248
)}
248249
>
250+
{currentField.isVariant ? <VariantIndicator /> : null}
249251
<ToolbarTooltip data={{contentTypeName: currentField.parentContentTypeName, referenceFieldName: currentField.referenceFieldName}} disabled={!currentField.isReference || isDropdownOpen}>
250252
<div
251253
className={classNames(
@@ -266,6 +268,15 @@ function FieldLabelWrapperComponent(
266268
"field-label-dropdown-open": isDropdownOpen,
267269
[visualBuilderStyles()["field-label-dropdown-open"]]:
268270
isDropdownOpen,
271+
},
272+
{
273+
"visual-builder__focused-toolbar--variant":
274+
currentField.isVariant,
275+
},
276+
{
277+
[visualBuilderStyles()[
278+
"visual-builder__focused-toolbar--variant"
279+
]]: currentField.isVariant,
269280
}
270281
)}
271282
onClick={() => setIsDropdownOpen((prev) => !prev)}
Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,26 @@
11
import React from "preact/compat";
22

3-
export function VariantIcon(): JSX.Element {
4-
return (
3+
export function VariantIcon(props: {
4+
size?: string;
5+
}): JSX.Element {
6+
return (
57
<svg
68
width="12"
79
height="12"
810
viewBox="0 0 12 12"
911
fill="none"
1012
xmlns="http://www.w3.org/2000/svg"
13+
style={{
14+
width: props.size,
15+
height: props.size,
16+
}}
1117
>
1218
<path
1319
fill-rule="evenodd"
1420
clip-rule="evenodd"
1521
d="M4.41131 0.157165C4.34585 0.0589769 4.23565 0 4.11765 0C3.99964 0 3.88944 0.0589769 3.82398 0.157165L0.0592764 5.80422C-0.0197588 5.92278 -0.0197588 6.07722 0.0592764 6.19578L3.82398 11.8428C3.88944 11.941 3.99964 12 4.11765 12C4.23565 12 4.34585 11.941 4.41131 11.8428L6 9.4598L7.58869 11.8428C7.65415 11.941 7.76435 12 7.88235 12C8.00036 12 8.11056 11.941 8.17602 11.8428L11.9407 6.19578C12.0198 6.07722 12.0198 5.92278 11.9407 5.80422L8.17602 0.157165C8.11056 0.0589769 8.00036 0 7.88235 0C7.76435 0 7.65415 0.0589769 7.58869 0.157165L6 2.5402L4.41131 0.157165ZM5.57582 3.17647L4.11765 0.989215L0.777124 6L4.11765 11.0108L5.57582 8.82353L3.82398 6.19578C3.74495 6.07722 3.74495 5.92278 3.82398 5.80422L5.57582 3.17647ZM6 8.18726L4.54183 6L6 3.81274L7.45817 6L6 8.18726ZM6.42418 8.82353L8.17602 6.19578C8.25505 6.07722 8.25505 5.92278 8.17602 5.80422L6.42418 3.17647L7.88235 0.989215L11.2229 6L7.88235 11.0108L6.42418 8.82353Z"
1622
fill="currentColor"
1723
/>
18-
</svg>
24+
</svg>
1925
);
2026
}

src/visualBuilder/generators/__test__/generateToolbar.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ describe("appendFieldPathDropdown", () => {
161161
fireEvent.click(focusedToolbar);
162162

163163
expect(fieldLabelWrapper?.classList.toString()).toBe(
164-
"visual-builder__focused-toolbar__field-label-wrapper go3399023040"
164+
"visual-builder__focused-toolbar__field-label-wrapper go3061601331"
165165
);
166166
});
167167
});

src/visualBuilder/generators/generateHoverOutline.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import { visualBuilderStyles } from "../visualBuilder.style";
88
*/
99
export function addHoverOutline(
1010
targetElement: Element,
11-
disabled?: boolean
11+
disabled?: boolean,
12+
isVariant?: boolean
1213
): void {
1314
const targetElementDimension = targetElement.getBoundingClientRect();
1415

@@ -29,6 +30,15 @@ export function addHoverOutline(
2930
hoverOutline.classList.remove(
3031
visualBuilderStyles()["visual-builder__hover-outline--disabled"]
3132
);
33+
if (isVariant) {
34+
hoverOutline.classList.add(
35+
visualBuilderStyles()["visual-builder__hover-outline--variant"]
36+
);
37+
} else {
38+
hoverOutline.classList.remove(
39+
visualBuilderStyles()["visual-builder__hover-outline--variant"]
40+
);
41+
}
3242
}
3343

3444
hoverOutline.style.top = `${

src/visualBuilder/listeners/mouseHover.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@ async function addOutline(params?: AddOutlineParams): Promise<void> {
8181
fieldDisabled,
8282
} = params;
8383
if (!editableElement) return;
84-
addHoverOutline(editableElement as HTMLElement, fieldDisabled);
84+
const isVariant = !!fieldMetadata.variant;
85+
addHoverOutline(editableElement as HTMLElement, fieldDisabled, isVariant);
8586
const fieldSchema = await FieldSchemaMap.getFieldSchema(
8687
content_type_uid,
8788
fieldPath
@@ -100,7 +101,7 @@ async function addOutline(params?: AddOutlineParams): Promise<void> {
100101
entryAcl,
101102
entryWorkflowStageDetails
102103
);
103-
addHoverOutline(editableElement, fieldDisabled || isDisabled);
104+
addHoverOutline(editableElement, fieldDisabled || isDisabled, isVariant);
104105
}
105106

106107
const debouncedAddOutline = debounce(addOutline, 50, { trailing: true });
@@ -333,15 +334,16 @@ const throttledMouseHover = throttle(async (params: HandleMouseHoverParams) => {
333334
const isFocussed= VisualBuilder.VisualBuilderGlobalState.value.isFocussed;
334335
if(!isFocussed) {
335336
showHoverToolbar({
336-
event: params.event,
337-
overlayWrapper: params.overlayWrapper,
338-
visualBuilderContainer: params.visualBuilderContainer,
339-
previousSelectedEditableDOM:
340-
VisualBuilder.VisualBuilderGlobalState.value
341-
.previousSelectedEditableDOM,
342-
focusedToolbar: params.focusedToolbar,
343-
resizeObserver: params.resizeObserver,
344-
});
337+
event: params.event,
338+
overlayWrapper: params.overlayWrapper,
339+
visualBuilderContainer: params.visualBuilderContainer,
340+
previousSelectedEditableDOM:
341+
VisualBuilder.VisualBuilderGlobalState.value
342+
.previousSelectedEditableDOM,
343+
focusedToolbar: params.focusedToolbar,
344+
resizeObserver: params.resizeObserver,
345+
}
346+
);
345347
}
346348
}
347349

src/visualBuilder/utils/__test__/isFieldDisabled.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ describe("isFieldDisabled", () => {
123123
const result = isFieldDisabled(fieldSchemaMap, eventFieldDetails);
124124
expect(result.isDisabled).toBe(true);
125125
expect(result.reason).toBe(
126-
"Open an Experience from Audience widget to start editing"
126+
"To edit an experience, open the Audience widget and click the Edit icon."
127127
);
128128
});
129129

src/visualBuilder/utils/isFieldDisabled.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const DisableReason = {
1010
LocalizedEntry: "Editing this field is restricted in localized entries",
1111
UnlinkedVariant:
1212
"This field is not editable as it is not linked to the selected variant",
13-
AudienceMode: "Open an Experience from Audience widget to start editing",
13+
AudienceMode: "To edit an experience, open the Audience widget and click the Edit icon.",
1414
DisabledVariant:
1515
"This field is not editable as it doesn't match the selected variant",
1616
UnlocalizedVariant: "This field is not editable as it is not localized",

src/visualBuilder/visualBuilder.style.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,21 @@ export function visualBuilderStyles() {
291291
"visual-builder__cursor-icon--loader": css`
292292
animation: visual-builder__spinner 1s linear infinite;
293293
`,
294+
"visual-builder__variant-indicator": css`
295+
height: calc(100% - 1px);
296+
aspect-ratio: 1;
297+
background: white;
298+
border-radius: 2px;
299+
border-width: 2px;
300+
border-style: solid;
301+
align-content: center;
302+
text-align: center;
303+
border-color: #BD59FA;
304+
305+
svg {
306+
color: #BD59FA;
307+
}
308+
`,
294309
"visual-builder__focused-toolbar": css`
295310
position: absolute;
296311
transform: translateY(-100%);
@@ -347,10 +362,11 @@ export function visualBuilderStyles() {
347362
display: flex;
348363
flex-direction: column-reverse;
349364
position: relative;
365+
margin-right: 0.5rem;
350366
`,
351367
"visual-builder__focused-toolbar__field-label-container": css`
352368
display: flex;
353-
column-gap: 0.5rem;
369+
height: 1.75rem;
354370
align-items: center;
355371
`,
356372
"visual-builder__button": css`
@@ -514,6 +530,12 @@ export function visualBuilderStyles() {
514530
background: #909090;
515531
}
516532
`,
533+
"visual-builder__focused-toolbar--variant": css`
534+
.visual-builder__focused-toolbar__field-label-wrapper__current-field {
535+
background: #BD59FA;
536+
}
537+
538+
`,
517539
"visual-builder__cursor-disabled": css`
518540
.visual-builder__cursor-icon {
519541
background: #909090;
@@ -612,6 +634,9 @@ export function visualBuilderStyles() {
612634
"visual-builder__hover-outline--disabled": css`
613635
outline: 2px dashed #909090;
614636
`,
637+
"visual-builder__hover-outline--variant": css`
638+
outline: 2px dashed #BD59FA;
639+
`,
615640
"visual-builder__default-cursor--disabled": css`
616641
cursor: none;
617642
`,

0 commit comments

Comments
 (0)