Skip to content

Commit

Permalink
feat: add analytics for handoff checklist (#2413)
Browse files Browse the repository at this point in the history
* feat: add analytics for handoff checklist

* fix: coverage plugin bug

* fix: add widget analytics

* fix: add more data for analytics

* fix: snowflake widget bug

* fix: snowflake typo
  • Loading branch information
kamleshchandnani authored Nov 27, 2024
1 parent a04b605 commit 5f6bdb5
Show file tree
Hide file tree
Showing 12 changed files with 1,470 additions and 96 deletions.
88 changes: 50 additions & 38 deletions packages/plugin-figma-blade-coverage/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ const calculateCoverage = (node: SceneNode): CoverageMetrics | null => {
let nonBladeColorStyles = 0;
// let nonBladeEffectStyles = 0;
let totalLayers = 0;
let bladeCoverage = 0;

try {
// if there are non-frame nodes as direct children of a page, ignore them
Expand Down Expand Up @@ -408,12 +409,6 @@ const calculateCoverage = (node: SceneNode): CoverageMetrics | null => {
}
}

/** check if frame is being used as a custom component
* has fills?
* has strokes?
* has effects?
* if any of the above is true then it's a custom component
* */
const ignoreInstanceFrameNodeNames = [
'root',
'wrapper',
Expand All @@ -422,33 +417,42 @@ const calculateCoverage = (node: SceneNode): CoverageMetrics | null => {
'overlay', // Drawer Overlay
'Marker', // Step Marker
'Summary Row',
'card-body',
'card-content-holder',
];
if (
traversedNode.type === 'FRAME' &&
!ignoreInstanceFrameNodeNames.includes(traversedNode.name) &&
getParentNode(traversedNode)?.type !== 'PAGE'
) {
const hasStrokes =
traversedNode?.boundVariables?.strokes?.length ?? traversedNode.strokes.length;
const hasEffects = traversedNode.effects?.length || traversedNode.effectStyleId;
const hasNonMixedFills =
traversedNode.fills !== figma.mixed && traversedNode.fills.length;
const hasFills =
traversedNode?.boundVariables?.fills?.length ??
hasNonMixedFills ??
traversedNode.fillStyleId;
if (
Boolean(hasStrokes || hasEffects || hasFills) &&
!Boolean(traversedNode.fills === figma.mixed)
) {
// this is non-blade component error
// push the frame layer to be included in component count
nonBladeComponents++;
highlightNonBladeNode(traversedNode, 'Use relevant Blade component');
} else {
NODES_SKIP_FROM_COVERAGE.push('FRAME');
}
}

/** check if frame is being used as a custom component
* has fills?
* has strokes?
* has effects?
* if any of the above is true then it's a custom component
* */
// if (
// traversedNode.type === 'FRAME' &&
// !ignoreInstanceFrameNodeNames.includes(traversedNode.name) &&
// getParentNode(traversedNode)?.type !== 'PAGE'
// ) {
// const hasStrokes =
// traversedNode?.boundVariables?.strokes?.length ?? traversedNode.strokes.length;
// const hasEffects = traversedNode.effects?.length || traversedNode.effectStyleId;
// const hasNonMixedFills =
// traversedNode.fills !== figma.mixed && traversedNode.fills.length;
// const hasFills =
// traversedNode?.boundVariables?.fills?.length ??
// hasNonMixedFills ??
// traversedNode.fillStyleId;
// if (
// Boolean(hasStrokes || hasEffects || hasFills) &&
// !Boolean(traversedNode.fills === figma.mixed)
// ) {
// // this is non-blade component error
// // push the frame layer to be included in component count
// nonBladeComponents++;
// highlightNonBladeNode(traversedNode, 'Use relevant Blade component');
// } else {
// NODES_SKIP_FROM_COVERAGE.push('FRAME');
// }
// }

if (
![...NODES_SKIP_FROM_COVERAGE, 'TEXT', 'LINE', 'RECTANGLE', 'FRAME'].includes(
Expand All @@ -472,6 +476,7 @@ const calculateCoverage = (node: SceneNode): CoverageMetrics | null => {
// remove rectangle node index for next iteration because we don't want to remove all the rectangle nodes, only the image ones
// remove frame node index for next iteration because we don't want to remove layout frame nodes, only the one that has being used as card
const nodesToBeRemoved = ['RECTANGLE', 'FRAME'];
// const nodesToBeRemoved = ['RECTANGLE'];
nodesToBeRemoved.forEach((nodeName) => {
const nodeIndex = NODES_SKIP_FROM_COVERAGE.findIndex((node) => node === nodeName);
if (nodeIndex !== -1) {
Expand Down Expand Up @@ -508,6 +513,15 @@ const calculateCoverage = (node: SceneNode): CoverageMetrics | null => {
figma.closePlugin();
}

if (bladeComponents === 0 && totalLayers === 0) {
bladeCoverage = 0;
} else if (nonBladeComponents === 0) {
// we need to do this because when everything is from blade there are still outer frames and things like that are non-blade and not flagged as well
bladeCoverage = 100;
} else {
bladeCoverage = Number((bladeComponents / totalLayers) * 100);
}

return {
bladeComponents,
bladeTextStyles,
Expand All @@ -516,10 +530,7 @@ const calculateCoverage = (node: SceneNode): CoverageMetrics | null => {
nonBladeTextStyles,
nonBladeColorStyles,
totalLayers,
bladeCoverage:
bladeComponents === 0 && totalLayers === 0
? 0
: Number((bladeComponents / totalLayers) * 100),
bladeCoverage,
};
};

Expand Down Expand Up @@ -600,9 +611,10 @@ const main = async (): Promise<void> => {
eventName: 'Blade Coverage Plugin Used',
properties: {
fileName: figma.root.name,
pageName: mainFrameNode.parent?.name,
pageName: figma.currentPage.name,
pagePath: `${figma.root.name}/${figma.currentPage.name}`,
nodeName: mainFrameNode.name,
nodePath: `${figma.root.name}/${mainFrameNode.parent?.name}/${mainFrameNode.name}`,
nodePath: `${figma.root.name}/${figma.currentPage.name}/${mainFrameNode.name}`,
nodeUrlPath: `https://www.figma.com/file/${figma.fileKey}/${
figma.root.name
}?node-id=${encodeURIComponent(mainFrameNode.id)}`,
Expand Down
35 changes: 35 additions & 0 deletions packages/widget-figma-dev-handoff-checklist/build-dev-handoff.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const esbuild = require('esbuild');
require('dotenv').config();

async function build() {
// Check if the `--watch` argument is passed in the command line
const isWatchMode = process.argv.includes('--watch');

// Create the build context
const ctx = await esbuild.context({
entryPoints: ['dev-handoff-checklist/code.tsx'],
bundle: true,
outfile: 'dev-handoff-checklist/dist/code.js',
target: 'es6',
define: {
'process.env.SEGMENT_WRITE_KEY': JSON.stringify(btoa(process.env.SEGMENT_WRITE_KEY ?? '')),
},
});

if (isWatchMode) {
// Enable watch mode
await ctx.watch();
console.log('Watching for changes...');
} else {
// Perform a single build
await ctx.rebuild();
console.log('Build completed.');
await ctx.dispose();
}
}

// Execute the build function
build().catch((error) => {
console.error(error);
process.exit(1);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const esbuild = require('esbuild');
require('dotenv').config();

async function build() {
// Check if the `--watch` argument is passed in the command line
const isWatchMode = process.argv.includes('--watch');

// Create the build context
const ctx = await esbuild.context({
entryPoints: ['snowflake-handoff-checklist/code.tsx'],
bundle: true,
outfile: 'snowflake-handoff-checklist/dist/code.js',
target: 'es6',
define: {
'process.env.SEGMENT_WRITE_KEY': JSON.stringify(btoa(process.env.SEGMENT_WRITE_KEY ?? '')),
},
});

if (isWatchMode) {
// Enable watch mode
await ctx.watch();
console.log('Watching for changes...');
} else {
// Perform a single build
await ctx.rebuild();
console.log('Build completed.');
await ctx.dispose();
}
}

// Execute the build function
build().catch((error) => {
console.error(error);
process.exit(1);
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ interface CheckboxProps {
isEditable?: boolean;
isEditablePlaceholderText?: string;
isEditableInputWithDateField?: boolean;
onCheckboxClick: (checkedState: true | false) => void;
onCheckboxClick: ({ isChecked, optionText }: { isChecked: boolean; optionText: string }) => void;
}

function Checkbox({
Expand All @@ -31,9 +31,9 @@ function Checkbox({
</svg>
`;

const handleOnClick = () => {
const handleOnClick = ({ optionText }: { optionText: string }) => {
setChecked(!isChecked);
onCheckboxClick(!isChecked);
onCheckboxClick({ isChecked: !isChecked, optionText });
};

return (
Expand All @@ -42,7 +42,7 @@ function Checkbox({
cornerRadius={4}
direction="vertical"
width="fill-parent"
onClick={isEditable ? () => {} : handleOnClick}
onClick={isEditable ? () => {} : () => handleOnClick({ optionText })}
hoverStyle={{ fill: { r: 0, g: 0, b: 0, a: 0.04 } }}
>
<AutoLayout padding={{ vertical: 2 }} direction="horizontal" spacing={4} width="fill-parent">
Expand All @@ -61,7 +61,7 @@ function Checkbox({
fill={isChecked ? '#305EFF' : '#FFFFFF'}
stroke={isChecked ? '#305EFF' : '#CBD5E2'}
strokeWidth={1.5}
onClick={isEditable ? handleOnClick : () => {}}
onClick={isEditable ? () => handleOnClick({ optionText }) : () => {}}
>
{isChecked && <Svg src={checkIcon} width={12} height={12} />}
</AutoLayout>
Expand All @@ -74,7 +74,7 @@ function Checkbox({
fontWeight={400}
lineHeight={20}
fill={isChecked ? '#768EA7' : '#40566D'}
onClick={isEditable ? handleOnClick : () => {}}
onClick={isEditable ? () => handleOnClick({ optionText }) : () => {}}
>
{optionText}
</Text>
Expand All @@ -97,7 +97,7 @@ function Checkbox({
lineHeight={20}
fill={isChecked ? '#768EA7' : '#40566D'}
width="fill-parent"
onClick={isEditable ? handleOnClick : () => {}}
onClick={isEditable ? () => handleOnClick({ optionText }) : () => {}}
>
{optionText}
</Text>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,55 @@
import Checkbox from '../components/Checkbox';
import SectionHeader from '../components/SectionHeader';
import ProgressBar from '../components/ProgressBar';
const { AutoLayout, Text, useSyncedState } = figma.widget;
import { sendAnalytics } from '../utils/sendAnalytics';
const { AutoLayout, Text, useSyncedState, useEffect, waitForTask } = figma.widget;

function Widget() {
const [checkedItems, setCheckedItems] = useSyncedState('checkedStates', 0);
const [isAnalyticsLoadEventSent, setIsAnalyticsLoadEventSent] = useSyncedState(
'analytics',
false,
);

useEffect(() => {
if (!isAnalyticsLoadEventSent) {
// send analytics
waitForTask(
sendAnalytics({
eventName: 'Blade Dev Handoff Checklist Used',
}),
);
setIsAnalyticsLoadEventSent(true);
}
});

const updateChecklist = (checkedState: true | false): void => {
if (checkedState) {
setCheckedItems((prevState: number) => prevState + 1);
const updateChecklist = ({
isChecked,
optionText,
}: {
isChecked: boolean;
optionText: string;
}): void => {
if (isChecked) {
setCheckedItems((prevState: number) => {
waitForTask(
sendAnalytics({
eventName: 'Dev Checklist Item Toggled',
properties: { checkedItems: prevState + 1, checkedItemName: optionText },
}),
);
return prevState + 1;
});
} else {
setCheckedItems((prevState: number) => prevState - 1);
setCheckedItems((prevState: number) => {
waitForTask(
sendAnalytics({
eventName: 'Dev Checklist Item Toggled',
properties: { checkedItems: prevState - 1, unCheckedItemName: optionText },
}),
);
return prevState - 1;
});
}
};

Expand All @@ -28,7 +67,7 @@ function Widget() {
<Text fontSize={28} fontWeight={700} fill="#192839">
✏️ Dev handoff checklist
</Text>
<ProgressBar cardWidgetWidth={401} numberOfCheckboxes={14} checkedItems={checkedItems} />
<ProgressBar cardWidgetWidth={401} numberOfCheckboxes={15} checkedItems={checkedItems} />
</AutoLayout>
<AutoLayout direction="vertical" spacing={4} width="fill-parent">
<SectionHeader title="Reviewers" />
Expand Down Expand Up @@ -96,27 +135,32 @@ function Widget() {
/>
<Checkbox
id="state2"
optionText="Accounted for edge cases and error states"
optionText="Created end-to-end prototype for the flow"
onCheckboxClick={updateChecklist}
/>
<Checkbox
id="state3"
optionText="Used browser header and footer frames"
optionText="Accounted for edge cases and error states"
onCheckboxClick={updateChecklist}
/>
<Checkbox
id="state4"
optionText="Designs are light and dark mode compatible"
optionText="Used browser header and footer frames"
onCheckboxClick={updateChecklist}
/>
<Checkbox
id="state5"
optionText="Added Figma's annotations to explain behaviours"
optionText="Designs are light and dark mode compatible"
onCheckboxClick={updateChecklist}
/>
<Checkbox
id="state6"
optionText="Ensured responsive design for both desktop and mobile flows, considering standard screen sizes, safe areas, and keyboard layouts on mobile devices."
optionText="Added Figma's annotations to explain behaviours"
onCheckboxClick={updateChecklist}
/>
<Checkbox
id="state7"
optionText="Ensured responsive design for both desktop and mobile flows, considering standard screen sizes, safe areas, and keyboard layouts on mobile devices"
onCheckboxClick={updateChecklist}
/>
</AutoLayout>
Expand Down
Loading

0 comments on commit 5f6bdb5

Please sign in to comment.