From fc2ade2782fba1540463e9e6006fdded66727c0d Mon Sep 17 00:00:00 2001 From: Corina Gum <> Date: Wed, 26 Mar 2025 15:28:34 -0700 Subject: [PATCH 01/12] Update cursor rules and prevent logger re-renders --- .cursor/rules/devtools-import-rules.mdc | 10 ++++-- .cursor/rules/react-teams-sdk.mdc | 36 ++++++++++++++----- .../ActivityDetails/ActivityDetails.tsx | 3 +- .../components/Card/Medias/CodeBlockCard.tsx | 5 +-- .../CardDesigner/CardDesignerEditor.tsx | 18 +++++----- .../components/ChatMessage/ChatMessage.tsx | 18 +++++----- .../src/components/ComposeBox/ComposeBox.tsx | 3 +- .../ComposeBoxToolbar/ComposeBoxToolbar.tsx | 5 +-- .../src/components/PageNav/PageNav.tsx | 1 + .../src/screens/ChatScreen/ChatScreen.tsx | 12 ++----- .../devtools/src/screens/NewCardsScreen.tsx | 12 ++++--- packages/devtools/src/utils/devUtils.ts | 3 +- 12 files changed, 79 insertions(+), 47 deletions(-) diff --git a/.cursor/rules/devtools-import-rules.mdc b/.cursor/rules/devtools-import-rules.mdc index 41ec7be4..8819b564 100644 --- a/.cursor/rules/devtools-import-rules.mdc +++ b/.cursor/rules/devtools-import-rules.mdc @@ -3,9 +3,13 @@ description: devtools import rules globs: packages/devtools/, *.tsx, *.ts alwaysApply: true --- - -# Devtools import rules - +--- +description: Import rules in Spark +globs: *.tsx, packages/devtools, *.ts +alwaysApply: true +--- +--- +The linter error "Import order needs to be fixed" can be quick resolved by running `npm run lint:fix`. See below for a summary of those rules. Sort imports using the convention shown below. See [App.tsx](mdc:packages/devtools/src/App.tsx) as an example. ` // imports from external packages are first, sorted alphabetically by package name, except react imports should always be the first import diff --git a/.cursor/rules/react-teams-sdk.mdc b/.cursor/rules/react-teams-sdk.mdc index 2474614f..97005b23 100644 --- a/.cursor/rules/react-teams-sdk.mdc +++ b/.cursor/rules/react-teams-sdk.mdc @@ -5,7 +5,8 @@ alwaysApply: true --- --- description: React development in Spark -globs: packages/devtools/**/*.tsx,.ts +globs: *.tsx, packages/devtools, *.ts +alwaysApply: true --- # Summary - You are an expert AI programming assistant that primarily focuses on producing clear, readable React and TypeScript code. @@ -18,24 +19,44 @@ globs: packages/devtools/**/*.tsx,.ts - Keep <> React components when they exist. ## FluentUI +- Never use `shorthand` from FluentUI. - For colors, fonts, spacing, typography styles etc, use fluent ui's tokens object, found at https://github.com/microsoft/fluentui/tree/master/packages/tokens/src/global and https://react.fluentui.dev/?path=/docs/theme-colors--docs - Use FluentUI for all components like - + handleMessageReaction(value.id, reaction)} + /> ))} )} diff --git a/packages/devtools/src/components/ChatMessage/ChatMessageDeleted.tsx b/packages/devtools/src/components/ChatMessage/ChatMessageDeleted.tsx index 11d0a8a9..53e6d46b 100644 --- a/packages/devtools/src/components/ChatMessage/ChatMessageDeleted.tsx +++ b/packages/devtools/src/components/ChatMessage/ChatMessageDeleted.tsx @@ -3,7 +3,7 @@ import { Link, mergeClasses } from '@fluentui/react-components'; import { MessageActionUIPayload } from '../../types/MessageActionUI'; -import { useChatMessageStyles } from './ChatMessage.styles'; +import useChatMessageStyles from './ChatMessage.styles'; interface ChatMessageDeletedProps { id: string; diff --git a/packages/devtools/src/components/ChatMessage/MessageAttachments.tsx b/packages/devtools/src/components/ChatMessage/MessageAttachments.tsx new file mode 100644 index 00000000..9d38f533 --- /dev/null +++ b/packages/devtools/src/components/ChatMessage/MessageAttachments.tsx @@ -0,0 +1,81 @@ +import { FC, memo, useCallback, useMemo } from 'react'; +import { Image } from '@fluentui/react-components'; +import { Attachment } from '@microsoft/spark.api'; + +import { AttachmentType } from '../../types/Attachment'; +import AttachmentsContainer from '../AttachmentsContainer/AttachmentsContainer'; + +export interface MessageAttachmentsProps { + attachments: Attachment[]; + classes: Record; +} + +const MessageAttachments: FC = memo(({ attachments, classes }) => { + const renderAttachment = useCallback( + (attachment: Attachment) => { + if (!attachment) return null; + + switch (attachment.contentType) { + case 'image/png': + case 'image/jpeg': + case 'image/gif': + case 'image/jpg': + return ( + {attachment.name + ); + default: + return null; + } + }, + [classes.attachmentImage] + ); + + const imageAttachments = useMemo( + () => attachments.filter((a) => a.contentType?.startsWith('image/')), + [attachments] + ); + + const nonImageAttachments = useMemo(() => { + return attachments + .filter((a) => !a.contentType?.startsWith('image/')) + .map((attachment): AttachmentType => { + if (attachment.contentType?.startsWith('application/vnd.microsoft.card.')) { + return { + type: 'card', + content: attachment.content, + name: attachment.name, + }; + } + + return { + type: 'file', + content: attachment.contentUrl || attachment.content, + name: attachment.name, + }; + }); + }, [attachments]); + + return ( + <> + {imageAttachments.length > 0 && ( +
{imageAttachments.map(renderAttachment)}
+ )} + {nonImageAttachments.length > 0 && ( + {}} + showRemoveButtons={false} + /> + )} + + ); +}); + +MessageAttachments.displayName = 'MessageAttachments'; + +export default MessageAttachments; diff --git a/packages/devtools/src/components/ChatMessage/MessageReactionButton.tsx b/packages/devtools/src/components/ChatMessage/MessageReactionButton.tsx new file mode 100644 index 00000000..27db8a22 --- /dev/null +++ b/packages/devtools/src/components/ChatMessage/MessageReactionButton.tsx @@ -0,0 +1,44 @@ +import { FC, memo } from 'react'; +import { Button, mergeClasses, Tooltip } from '@fluentui/react-components'; +import { MessageReaction } from '@microsoft/spark.api'; + +import { messageReactions } from '../../types/MessageReactionsEmoji'; + +import useChatMessageStyles from './ChatMessage.styles'; + +export interface MessageReactionButtonProps { + reaction: MessageReaction; + isFromUser: boolean; + onReactionClick: () => void; +} + +const MessageReactionButton: FC = memo( + ({ reaction, isFromUser, onReactionClick }) => { + const classes = useChatMessageStyles(); + const reactionEmoji = messageReactions.find((r) => r.reaction === reaction.type)?.label; + + return ( + {reaction.type}} + relationship="label" + positioning={{ align: 'center', position: 'below' }} + > + + + ); + } +); + +MessageReactionButton.displayName = 'MessageReactionButton'; + +export default MessageReactionButton; From aeda2e81e8f7731d9b7d56e3e1b3007d5ea42e88 Mon Sep 17 00:00:00 2001 From: Corina Gum <> Date: Wed, 26 Mar 2025 18:38:34 -0700 Subject: [PATCH 03/12] Some fixes & refactor --- .../ActivitiesGrid/ActivitiesGrid.styles.ts | 6 +- .../AttachmentsContainer.styles.ts | 7 +- .../AttachmentsContainer.tsx | 37 ++- .../src/components/Card/AdaptiveCard.tsx | 6 +- .../Card/Containers/ActionSetCard.tsx | 1 + .../ChatMessage/ChatAvatarWrapper.tsx | 4 +- .../ChatMessage/ChatMessageContainer.tsx | 4 +- .../ComposeBoxToolbar.styles.ts | 9 +- .../ComposeBoxToolbar/ComposeBoxToolbar.tsx | 247 +++++++++--------- .../ActivitiesScreen.styles.ts | 6 +- .../screens/ChatScreen/ChatScreen.styles.ts | 17 +- .../src/screens/ChatScreen/ChatScreen.tsx | 9 +- 12 files changed, 184 insertions(+), 169 deletions(-) diff --git a/packages/devtools/src/components/ActivitiesGrid/ActivitiesGrid.styles.ts b/packages/devtools/src/components/ActivitiesGrid/ActivitiesGrid.styles.ts index 2e3fec85..3d474615 100644 --- a/packages/devtools/src/components/ActivitiesGrid/ActivitiesGrid.styles.ts +++ b/packages/devtools/src/components/ActivitiesGrid/ActivitiesGrid.styles.ts @@ -15,7 +15,7 @@ const useActivitiesGridClasses = makeStyles({ padding: '0.5rem', }, row: { - borderBottom: `1px solid ${tokens.colorNeutralStrokeAccessible}`, + borderBottom: `${tokens.strokeWidthThin} solid ${tokens.colorNeutralStrokeAccessible}`, '&:hover': { backgroundColor: tokens.colorBrandBackground2Hover, cursor: 'pointer', @@ -32,7 +32,7 @@ const useActivitiesGridClasses = makeStyles({ tableLayout: 'auto', boxShadow: tokens.shadow16, '&:last-child': { - borderBottom: `1px solid ${tokens.colorTransparentStroke}`, + borderBottom: `${tokens.strokeWidthThin} solid ${tokens.colorTransparentStroke}`, }, }, header: { @@ -91,7 +91,7 @@ const useActivitiesGridClasses = makeStyles({ padding: tokens.spacingVerticalL, color: tokens.colorNeutralForeground3, fontSize: tokens.fontSizeBase300, - borderBottom: `1px solid ${tokens.colorNeutralStrokeAccessible}`, + borderBottom: `${tokens.strokeWidthThin} solid ${tokens.colorNeutralStrokeAccessible}`, }, hideSelection: { visibility: 'hidden', diff --git a/packages/devtools/src/components/AttachmentsContainer/AttachmentsContainer.styles.ts b/packages/devtools/src/components/AttachmentsContainer/AttachmentsContainer.styles.ts index 1e09fe64..7eafcdb2 100644 --- a/packages/devtools/src/components/AttachmentsContainer/AttachmentsContainer.styles.ts +++ b/packages/devtools/src/components/AttachmentsContainer/AttachmentsContainer.styles.ts @@ -17,6 +17,10 @@ const useAttachmentsContainerClasses = makeStyles({ position: 'relative', }, removeAttachmentButton: { + borderRadius: tokens.borderRadiusSmall, + backgroundColor: tokens.colorNeutralBackground6, + border: `${tokens.strokeWidthThin} solid ${tokens.colorNeutralStroke1}`, + boxShadow: tokens.shadow4, position: 'absolute', top: tokens.spacingVerticalXXS, right: tokens.spacingHorizontalXXS, @@ -31,7 +35,6 @@ const useAttachmentsContainerClasses = makeStyles({ overflow: 'auto', }, attachmentImage: { - maxWidth: '300px', borderRadius: tokens.borderRadiusSmall, }, fileAttachment: { @@ -39,7 +42,7 @@ const useAttachmentsContainerClasses = makeStyles({ padding: `${tokens.spacingVerticalXXS} ${tokens.spacingHorizontalS}`, backgroundColor: tokens.colorNeutralBackground1, borderRadius: tokens.borderRadiusSmall, - border: `1px solid ${tokens.colorNeutralStroke1}`, + border: `${tokens.strokeWidthThin} solid ${tokens.colorNeutralStroke1}`, color: tokens.colorNeutralForeground1, }, }); diff --git a/packages/devtools/src/components/AttachmentsContainer/AttachmentsContainer.tsx b/packages/devtools/src/components/AttachmentsContainer/AttachmentsContainer.tsx index 085b5d11..55a653c9 100644 --- a/packages/devtools/src/components/AttachmentsContainer/AttachmentsContainer.tsx +++ b/packages/devtools/src/components/AttachmentsContainer/AttachmentsContainer.tsx @@ -1,12 +1,19 @@ import { FC, memo, useCallback } from 'react'; -import { Button, Image } from '@fluentui/react-components'; -import { Dismiss20Regular } from '@fluentui/react-icons/lib/fonts'; +import { Button, Image, Tooltip } from '@fluentui/react-components'; +import { + bundleIcon, + DismissFilled, + DismissRegular, + FluentIcon, +} from '@fluentui/react-icons/lib/fonts'; import { AttachmentType } from '../../types/Attachment'; import AdaptiveCard from '../Card/AdaptiveCard'; import useAttachmentsContainerClasses from './AttachmentsContainer.styles'; +const DismissIcon = bundleIcon(DismissFilled as FluentIcon, DismissRegular as FluentIcon); + interface AttachmentItemProps { attachment: AttachmentType; index: number; @@ -40,17 +47,21 @@ const AttachmentItem: FC = memo( }, [attachment.content, attachment.type, attachment.name, classes]); return ( -
+
{showRemoveButton && ( -
); } @@ -73,6 +84,7 @@ const AttachmentsContainer: FC = ({ return null; } + // TODO: attachmentLayout return (
{attachments.map((attachment, index) => ( @@ -88,7 +100,4 @@ const AttachmentsContainer: FC = ({ ); }; -AttachmentItem.displayName = 'AttachmentItem'; -AttachmentsContainer.displayName = 'AttachmentsContainer'; - export default AttachmentsContainer; diff --git a/packages/devtools/src/components/Card/AdaptiveCard.tsx b/packages/devtools/src/components/Card/AdaptiveCard.tsx index 02ae9d83..b4147822 100644 --- a/packages/devtools/src/components/Card/AdaptiveCard.tsx +++ b/packages/devtools/src/components/Card/AdaptiveCard.tsx @@ -13,10 +13,10 @@ const useAdaptiveCardStyles = makeStyles({ root: { display: 'flex', flexDirection: 'column', - borderRadius: '0.5rem', - padding: '0.75rem', + borderRadius: tokens.borderRadiusLarge, + padding: tokens.spacingVerticalL, backgroundColor: tokens.colorNeutralBackground6, - boxShadow: tokens.shadow16, + boxShadow: tokens.shadow4, gap: tokens.spacingVerticalS, }, }); diff --git a/packages/devtools/src/components/Card/Containers/ActionSetCard.tsx b/packages/devtools/src/components/Card/Containers/ActionSetCard.tsx index 56f68f2b..d813c4fe 100644 --- a/packages/devtools/src/components/Card/Containers/ActionSetCard.tsx +++ b/packages/devtools/src/components/Card/Containers/ActionSetCard.tsx @@ -27,4 +27,5 @@ const ActionSetCard: FC = ({ value }) => { ); }; +ActionSetCard.displayName = 'ActionSetCard'; export default ActionSetCard; diff --git a/packages/devtools/src/components/ChatMessage/ChatAvatarWrapper.tsx b/packages/devtools/src/components/ChatMessage/ChatAvatarWrapper.tsx index cb71be31..3d3c24c5 100644 --- a/packages/devtools/src/components/ChatMessage/ChatAvatarWrapper.tsx +++ b/packages/devtools/src/components/ChatMessage/ChatAvatarWrapper.tsx @@ -1,5 +1,5 @@ import { FC } from 'react'; -import { Avatar, makeStyles, tokens } from '@fluentui/react-components'; +import { Avatar, makeStyles, tokens, mergeClasses } from '@fluentui/react-components'; interface ChatAvatarProps { id: string; @@ -19,7 +19,7 @@ const ChatAvatarWrapper: FC = ({ id, isConnected }) => { const classes = useClasses(); return ( -
+
); diff --git a/packages/devtools/src/components/ChatMessage/ChatMessageContainer.tsx b/packages/devtools/src/components/ChatMessage/ChatMessageContainer.tsx index 3fc70c96..7daf8872 100644 --- a/packages/devtools/src/components/ChatMessage/ChatMessageContainer.tsx +++ b/packages/devtools/src/components/ChatMessage/ChatMessageContainer.tsx @@ -32,7 +32,9 @@ const ChatMessageContainer: FC = memo(({ value, isConnected = fals >
- {sendDirection === 'received' && } + {sendDirection === 'received' && ( + + )}
{value.createdDateTime ? ( = ({ - onSend, - onAttachment, - hasContent = false, - ...props -}) => { - const classes = useCBToolbarClasses(); - const navigate = useNavigate(); - const [isDialogOpen, setIsDialogOpen] = useState(false); - const [jsonInput, setJsonInput] = useState(''); - const [menuOpen, setMenuOpen] = useState(false); - const dialogTitleId = useId('dialog-title'); - const textareaId = useId('json-input'); - const jsonInputRef = useRef(null); - const { currentCard } = useCardStore(); - const { dispatchToast } = useToastController(); +const ComposeBoxToolbar: FC = memo( + ({ onSend, onAttachment, hasContent = false, ...props }) => { + const classes = useCBToolbarClasses(); + const navigate = useNavigate(); + const [isDialogOpen, setIsDialogOpen] = useState(false); + const [jsonInput, setJsonInput] = useState(''); + const [menuOpen, setMenuOpen] = useState(false); + const dialogTitleId = useId('dialog-title'); + const textareaId = useId('json-input'); + const jsonInputRef = useRef(null); + const { currentCard } = useCardStore(); + const { dispatchToast } = useToastController(); - const handleNavigateToCards = () => { - navigate('/cards'); - setMenuOpen(false); - }; + const handleNavigateToCards = () => { + navigate('/cards'); + setMenuOpen(false); + }; - const handleSaveJson = useCallback(() => { - try { - const card = JSON.parse(jsonInput) as Card; + const handleSaveJson = useCallback(() => { + try { + const card = JSON.parse(jsonInput) as Card; - if (onAttachment) { - onAttachment({ - type: 'card', - content: card, - contentType: 'application/vnd.microsoft.card.adaptive', - }); - } else if (onSend) { - onSend([ - { + if (onAttachment) { + onAttachment({ type: 'card', content: card, contentType: 'application/vnd.microsoft.card.adaptive', - }, - ]); - } + }); + } else if (onSend) { + onSend([ + { + type: 'card', + content: card, + contentType: 'application/vnd.microsoft.card.adaptive', + }, + ]); + } - setIsDialogOpen(false); - setJsonInput(''); - } catch (error) { - childLog.debug('Failed to parse JSON:', error); - dispatchToast( - - Failed to parse JSON. Please check your input and try again. - , - { intent: 'error' } - ); - } - }, [jsonInput, onAttachment, onSend, dispatchToast]); + setIsDialogOpen(false); + setJsonInput(''); + } catch (error) { + childLog.debug('Failed to parse JSON:', error); + dispatchToast( + + Failed to parse JSON. Please check your input and try again. + , + { intent: 'error' } + ); + } + }, [jsonInput, onAttachment, onSend, dispatchToast]); - const handleSend = useCallback(() => { - if (onSend) { - if (currentCard) { - onSend([ - { - contentType: 'application/vnd.microsoft.card.adaptive', - content: currentCard, - }, - ]); - } else if (hasContent) { - onSend(); + const handleSend = useCallback(() => { + if (onSend) { + if (currentCard) { + onSend([ + { + contentType: 'application/vnd.microsoft.card.adaptive', + content: currentCard, + }, + ]); + } else if (hasContent) { + onSend(); + } } - } - }, [onSend, hasContent, currentCard]); + }, [onSend, hasContent, currentCard]); - return ( - - setMenuOpen(data.open)}> - - - } - className={classes.toolbarButton} - /> - - - - - { - setIsDialogOpen(true); - setMenuOpen(false); - }} - > - Paste custom JSON - - Open card designer - - - - setIsDialogOpen(data.open)}> - - - Paste Card JSON - -