PromucFlow_constructor/app/client/src/components/featureWalkthrough/walkthroughRenderer.tsx
Ankita Kinger 6244e28fed
chore: Update analytics to pass the correct source information in identify user call (#32591)
## Description

Updating analytics to pass the correct source information

Fixes [#32266](https://github.com/appsmithorg/appsmith/issues/32266)

## Automation

/ok-to-test tags="@tag.Sanity"

### 🔍 Cypress test results
<!-- This is an auto-generated comment: Cypress test results  -->
> [!TIP]
> 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
> Workflow run:
<https://github.com/appsmithorg/appsmith/actions/runs/8750877755>
> Commit: 6fedefebd3867aee79877b7ed105c90888005cfd
> Cypress dashboard url: <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=8750877755&attempt=1"
target="_blank">Click here!</a>

<!-- end of auto-generated comment: Cypress test results  -->
2024-04-19 15:06:50 +05:30

335 lines
9.9 KiB
TypeScript

import { Icon, Text, Button, Divider } from "design-system";
import React, { useContext, useEffect, useState } from "react";
import styled from "styled-components";
import { PADDING_HIGHLIGHT, getPosition } from "./utils";
import type {
FeatureDetails,
FeatureParams,
OffsetType,
} from "./walkthroughContext";
import WalkthroughContext, {
isFeatureFooterDetails,
} from "./walkthroughContext";
import AnalyticsUtil from "@appsmith/utils/AnalyticsUtil";
import { showIndicator } from "components/utils/Indicator";
const CLIPID = "clip__feature";
const Z_INDEX = 1000;
const WalkthroughDescription = styled(Text)`
// CSS to add new line for each \n in the description
white-space: pre-line;
`;
const WalkthroughWrapper = styled.div<{ overlayColor?: string }>`
left: 0px;
top: 0px;
position: fixed;
width: 100%;
height: 100%;
color: ${(props) => props.overlayColor ?? "rgb(0, 0, 0, 0.7)"};
z-index: ${Z_INDEX};
// This allows the user to click on the target element rather than the overlay div
pointer-events: none;
`;
const SvgWrapper = styled.svg`
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
`;
const InstructionsWrapper = styled.div`
padding: var(--ads-v2-spaces-4);
position: absolute;
background: white;
display: flex;
flex-direction: column;
width: 296px;
pointer-events: auto;
border-radius: var(--ads-radius-1);
`;
const ImageWrapper = styled.div`
border-radius: var(--ads-radius-1);
background: #f1f5f9;
display: flex;
align-items: center;
justify-content: center;
margin-top: 8px;
padding: var(--ads-v2-spaces-7);
img {
max-height: 220px;
}
`;
const InstructionsHeaderWrapper = styled.div`
display: flex;
p {
flex-grow: 1;
}
span {
align-self: flex-start;
margin-top: 5px;
cursor: pointer;
}
`;
const FeatureFooterDivider = styled(Divider)`
margin-top: 8px;
`;
const FeatureFooterWrapper = styled.div`
height: 36px;
margin-top: 8px;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
`;
interface RefRectParams {
// body params
bh: number;
bw: number;
// target params
th: number;
tw: number;
tx: number;
ty: number;
}
/*
* Clip Path Polygon for single target with bounding rect :
* 1) 0 0 ----> (body start) (body start)
* 2) 0 ${boundingRect.bh} ----> (body start) (body end)
* 3) ${boundingRect.tx} ${boundingRect.bh} ----> (target start) (body end)
* 4) ${boundingRect.tx} ${boundingRect.ty} ----> (target start) (target start)
* 5) ${boundingRect.tx + boundingRect.tw} ${boundingRect.ty} ----> (target end) (target start)
* 6) ${boundingRect.tx + boundingRect.tw} ${boundingRect.ty + boundingRect.th} ----> (target end) (target end)
* 7) ${boundingRect.tx} ${boundingRect.ty + boundingRect.th} ----> (target start) (target end)
* 8) ${boundingRect.tx} ${boundingRect.bh} ----> (target start) (body end)
* 9) ${boundingRect.bw} ${boundingRect.bh} ----> (body end) (body end)
* 10) ${boundingRect.bw} 0 ----> (body end) (body start)
*
*
* 1 ← ← ← ← ← ← ← ← ← ← ← ← ← ← ← ← ←10
* ↓ ↑
* ↓ Body ↑
* ↓ ↑
* ↓ ↑
* ↓ 4 → → → → → → → 5 ↑
* ↓ ↑ / / / / / / / ↓ ↑
* ↓ ↑ / / /Target/ /↓ ↑
* ↓ ↑ / / / / / / / ↓ ↑
* ↓ 7 ← ← ← ← ← ← ← 6 ↑
* ↓ ↑↓ ↑
* ↓ ↑↓ ↑
* 2 → → → → 3,8 → → → → → → → → → → → 9
*
* Repeat steps 3 to 8 for each element if there are multiple highlighting elements.
*
*/
/**
* Creates a Highlighting Clipping mask around a target container
* @param targetId Id for the target container to show highlighting around it
*/
type BoundingRectTargets = Record<string, RefRectParams>;
const WalkthroughRenderer = ({
details,
dismissOnOverlayClick,
eventParams = {},
multipleHighlights,
offset,
overlayColor,
targetId,
}: FeatureParams) => {
const [boundingRects, setBoundingRects] =
useState<BoundingRectTargets | null>(null);
const { popFeature } = useContext(WalkthroughContext) || {};
const multipleHighlightsIds = multipleHighlights?.length
? multipleHighlights
: [targetId];
if (multipleHighlightsIds.indexOf(targetId) === -1)
multipleHighlightsIds.push(targetId);
const updateBoundingRect = () => {
const mainTarget = document.querySelector(targetId);
if (mainTarget) {
const data: BoundingRectTargets = {};
multipleHighlightsIds.forEach((id) => {
const highlightArea = document.querySelector(id);
if (highlightArea) {
const boundingRect = highlightArea.getBoundingClientRect();
const bodyRect = document.body.getBoundingClientRect();
const offsetHighlightPad =
typeof offset?.highlightPad === "number"
? offset?.highlightPad
: PADDING_HIGHLIGHT;
data[id] = {
bw: bodyRect.width,
bh: bodyRect.height,
tw: boundingRect.width + 2 * offsetHighlightPad,
th: boundingRect.height + 2 * offsetHighlightPad,
tx: boundingRect.x - offsetHighlightPad,
ty: boundingRect.y - offsetHighlightPad,
};
}
});
if (Object.keys(data).length > 0) {
setBoundingRects(data);
showIndicator(`${targetId}`, offset?.position, {
top: offset?.indicatorTop || 0,
left: offset?.indicatorLeft || 0,
zIndex: Z_INDEX + 1,
});
}
}
};
useEffect(() => {
updateBoundingRect();
const highlightArea = document.querySelector(targetId);
window.addEventListener("resize", updateBoundingRect);
const resizeObserver = new ResizeObserver(updateBoundingRect);
if (highlightArea) {
AnalyticsUtil.logEvent("WALKTHROUGH_SHOWN", eventParams);
resizeObserver.observe(highlightArea);
}
return () => {
window.removeEventListener("resize", updateBoundingRect);
if (highlightArea) resizeObserver.unobserve(highlightArea);
};
}, [targetId]);
const onDismissWalkthrough = () => {
popFeature && popFeature("WALKTHROUGH_CROSS_ICON");
};
if (!boundingRects || Object.keys(boundingRects).length === 0) return null;
const targetBounds = boundingRects[targetId];
if (!targetBounds) return null;
return (
<WalkthroughWrapper
className="t--walkthrough-overlay"
overlayColor={overlayColor}
>
<SvgWrapper
height={targetBounds.bh}
onClick={dismissOnOverlayClick ? onDismissWalkthrough : () => null}
width={targetBounds.bw}
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<clipPath id={CLIPID}>
<polygon
// See the comments above the component declaration to understand the below points assignment.
points={`0 0,
0 ${targetBounds.bh},
${multipleHighlightsIds.reduce((acc, id) => {
const boundingRect = boundingRects[id];
if (boundingRect) {
acc = `${acc} ${boundingRect.tx} ${boundingRect.bh},
${boundingRect.tx} ${boundingRect.ty},
${boundingRect.tx + boundingRect.tw} ${boundingRect.ty},
${boundingRect.tx + boundingRect.tw} ${
boundingRect.ty + boundingRect.th
},
${boundingRect.tx} ${
boundingRect.ty + boundingRect.th
},
${boundingRect.tx} ${boundingRect.bh},`;
}
return acc;
}, "")}
${targetBounds.bw} ${targetBounds.bh},
${targetBounds.bw} 0
`}
/>
</clipPath>
</defs>
<rect
style={{
clipPath: 'url("#' + CLIPID + '")',
fill: "currentcolor",
height: targetBounds.bh,
pointerEvents: "auto",
width: targetBounds.bw,
}}
/>
</SvgWrapper>
<InstructionsComponent
details={details}
offset={offset}
onClose={onDismissWalkthrough}
targetId={targetId}
/>
</WalkthroughWrapper>
);
};
const InstructionsComponent = ({
details,
offset,
onClose,
targetId,
}: {
details?: FeatureDetails;
offset?: OffsetType;
targetId: string;
onClose: () => void;
}) => {
if (!details) return null;
const positionAttr = getPosition({
targetId,
offset,
});
return (
<InstructionsWrapper style={{ ...positionAttr }}>
<InstructionsHeaderWrapper>
<Text kind="heading-s" renderAs="p">
{details.title}
</Text>
<Icon
className="t--walkthrough-close"
color="black"
name="close"
onClick={onClose}
size="md"
/>
</InstructionsHeaderWrapper>
<WalkthroughDescription>{details.description}</WalkthroughDescription>
{details.imageURL && (
<ImageWrapper>
<img src={details.imageURL} />
</ImageWrapper>
)}
{!!details.footerDetails &&
isFeatureFooterDetails(details.footerDetails) && (
<>
<FeatureFooterDivider />
<FeatureFooterWrapper>
<Text kind="body-s">{details.footerDetails.footerText}</Text>
<Button onClick={details.footerDetails.onClickHandler} size="sm">
{details.footerDetails.footerButtonText}
</Button>
</FeatureFooterWrapper>
</>
)}
</InstructionsWrapper>
);
};
export default WalkthroughRenderer;