diff --git a/packages/twenty-front/src/hooks/useCopyToClipboard.tsx b/packages/twenty-front/src/hooks/useCopyToClipboard.tsx new file mode 100644 index 000000000..59776c0e7 --- /dev/null +++ b/packages/twenty-front/src/hooks/useCopyToClipboard.tsx @@ -0,0 +1,31 @@ +import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { useTheme } from '@emotion/react'; +import { useLingui } from '@lingui/react/macro'; +import { IconCopy, IconExclamationCircle } from 'twenty-ui/display'; + +export const useCopyToClipboard = () => { + const theme = useTheme(); + const { enqueueSnackBar } = useSnackBar(); + const { t } = useLingui(); + + const copyToClipboard = async (valueAsString: string) => { + try { + await navigator.clipboard.writeText(valueAsString); + + enqueueSnackBar(t`Copied to clipboard`, { + variant: SnackBarVariant.Success, + icon: , + duration: 2000, + }); + } catch { + enqueueSnackBar(t`Couldn't copy to clipboard`, { + variant: SnackBarVariant.Error, + icon: , + duration: 2000, + }); + } + }; + + return { copyToClipboard }; +}; diff --git a/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/JsonDataIndicatorHealthStatus.tsx b/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/JsonDataIndicatorHealthStatus.tsx index c9c33bcb0..bc033aa15 100644 --- a/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/JsonDataIndicatorHealthStatus.tsx +++ b/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/JsonDataIndicatorHealthStatus.tsx @@ -2,9 +2,10 @@ import { SettingsAdminIndicatorHealthContext } from '@/settings/admin-panel/heal import styled from '@emotion/styled'; import { useLingui } from '@lingui/react/macro'; import { useContext } from 'react'; -import { AdminPanelHealthServiceStatus } from '~/generated/graphql'; import { JsonTree } from 'twenty-ui/json-visualizer'; import { Section } from 'twenty-ui/layout'; +import { AdminPanelHealthServiceStatus } from '~/generated/graphql'; +import { useCopyToClipboard } from '~/hooks/useCopyToClipboard'; const StyledDetailsContainer = styled.div` background-color: ${({ theme }) => theme.background.secondary}; @@ -23,6 +24,7 @@ const StyledErrorMessage = styled.div` export const JsonDataIndicatorHealthStatus = () => { const { t } = useLingui(); + const { copyToClipboard } = useCopyToClipboard(); const { indicatorHealth } = useContext(SettingsAdminIndicatorHealthContext); @@ -54,6 +56,7 @@ export const JsonDataIndicatorHealthStatus = () => { emptyStringLabel={t`[empty string]`} arrowButtonCollapsedLabel={t`Expand`} arrowButtonExpandedLabel={t`Collapse`} + onNodeValueClick={copyToClipboard} /> )} diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowRunStepInputDetail.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowRunStepInputDetail.tsx index 0eabbc866..6686e63a0 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowRunStepInputDetail.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowRunStepInputDetail.tsx @@ -19,11 +19,13 @@ import { JsonTreeContextProvider, ShouldExpandNodeInitiallyProps, } from 'twenty-ui/json-visualizer'; +import { useCopyToClipboard } from '~/hooks/useCopyToClipboard'; export const WorkflowRunStepInputDetail = ({ stepId }: { stepId: string }) => { const { t, i18n } = useLingui(); const { getIcon } = useIcons(); const theme = useTheme(); + const { copyToClipboard } = useCopyToClipboard(); const workflowRunId = useWorkflowRunIdOrThrow(); const workflowRun = useWorkflowRun({ workflowRunId }); @@ -123,6 +125,7 @@ export const WorkflowRunStepInputDetail = ({ stepId }: { stepId: string }) => { arrowButtonExpandedLabel: t`Collapse`, getNodeHighlighting, shouldExpandNodeInitially: isFirstNodeDepthOfPreviousStep, + onNodeValueClick: copyToClipboard, }} > { const { t, i18n } = useLingui(); const theme = useTheme(); const { getIcon } = useIcons(); + const { copyToClipboard } = useCopyToClipboard(); const workflowRunId = useWorkflowRunIdOrThrow(); const workflowRun = useWorkflowRun({ workflowRunId }); @@ -73,6 +75,7 @@ export const WorkflowRunStepOutputDetail = ({ stepId }: { stepId: string }) => { ? setRedHighlightingForEveryNode : undefined } + onNodeValueClick={copyToClipboard} /> diff --git a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts index a657ed3a4..f438b8ec6 100644 --- a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts +++ b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts @@ -125,6 +125,7 @@ export { IconDotsVertical, IconDownload, IconEditCircle, + IconExclamationCircle, IconExternalLink, IconEye, IconEyeOff, diff --git a/packages/twenty-ui/src/display/index.ts b/packages/twenty-ui/src/display/index.ts index 39a87586b..f1054e1f5 100644 --- a/packages/twenty-ui/src/display/index.ts +++ b/packages/twenty-ui/src/display/index.ts @@ -186,6 +186,7 @@ export { IconDotsVertical, IconDownload, IconEditCircle, + IconExclamationCircle, IconExternalLink, IconEye, IconEyeOff, diff --git a/packages/twenty-ui/src/json-visualizer/__stories__/JsonTree.stories.tsx b/packages/twenty-ui/src/json-visualizer/__stories__/JsonTree.stories.tsx index 25af1430e..9b5b90b4a 100644 --- a/packages/twenty-ui/src/json-visualizer/__stories__/JsonTree.stories.tsx +++ b/packages/twenty-ui/src/json-visualizer/__stories__/JsonTree.stories.tsx @@ -1,7 +1,9 @@ import { Meta, StoryObj } from '@storybook/react'; import { expect, + fn, userEvent, + waitFor, waitForElementToBeRemoved, within, } from '@storybook/test'; @@ -482,3 +484,32 @@ export const RedHighlighting: Story = { expect(ageElement).toBeVisible(); }, }; + +export const CopyJsonNodeValue: Story = { + args: { + value: { + name: 'John Doe', + age: 30, + }, + onNodeValueClick: fn(), + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + const nameValue = await canvas.findByText('John Doe'); + + await userEvent.click(nameValue); + + await waitFor(() => { + expect(args.onNodeValueClick).toHaveBeenCalledWith('John Doe'); + }); + + const ageValue = await canvas.findByText('30'); + + await userEvent.click(ageValue); + + await waitFor(() => { + expect(args.onNodeValueClick).toHaveBeenCalledWith('30'); + }); + }, +}; diff --git a/packages/twenty-ui/src/json-visualizer/components/JsonTree.tsx b/packages/twenty-ui/src/json-visualizer/components/JsonTree.tsx index 596e278b6..63166f5af 100644 --- a/packages/twenty-ui/src/json-visualizer/components/JsonTree.tsx +++ b/packages/twenty-ui/src/json-visualizer/components/JsonTree.tsx @@ -14,6 +14,7 @@ export const JsonTree = ({ emptyStringLabel, arrowButtonCollapsedLabel, arrowButtonExpandedLabel, + onNodeValueClick, }: { value: JsonValue; getNodeHighlighting?: GetJsonNodeHighlighting; @@ -25,6 +26,7 @@ export const JsonTree = ({ emptyStringLabel: string; arrowButtonCollapsedLabel: string; arrowButtonExpandedLabel: string; + onNodeValueClick?: (valueAsString: string) => void; }) => { return ( diff --git a/packages/twenty-ui/src/json-visualizer/components/internal/JsonNodeValue.tsx b/packages/twenty-ui/src/json-visualizer/components/internal/JsonNodeValue.tsx index 61940c2b8..9a8a82829 100644 --- a/packages/twenty-ui/src/json-visualizer/components/internal/JsonNodeValue.tsx +++ b/packages/twenty-ui/src/json-visualizer/components/internal/JsonNodeValue.tsx @@ -1,4 +1,5 @@ import styled from '@emotion/styled'; +import { useJsonTreeContextOrThrow } from '@ui/json-visualizer/hooks/useJsonTreeContextOrThrow'; import { JsonNodeHighlighting } from '@ui/json-visualizer/types/JsonNodeHighlighting'; const StyledText = styled.span<{ @@ -19,5 +20,15 @@ export const JsonNodeValue = ({ valueAsString: string; highlighting?: JsonNodeHighlighting | undefined; }) => { - return {valueAsString}; + const { onNodeValueClick } = useJsonTreeContextOrThrow(); + + const handleClick = () => { + onNodeValueClick?.(valueAsString); + }; + + return ( + + {valueAsString} + + ); }; diff --git a/packages/twenty-ui/src/json-visualizer/contexts/JsonTreeContext.tsx b/packages/twenty-ui/src/json-visualizer/contexts/JsonTreeContext.tsx index a5cdaea2f..540006298 100644 --- a/packages/twenty-ui/src/json-visualizer/contexts/JsonTreeContext.tsx +++ b/packages/twenty-ui/src/json-visualizer/contexts/JsonTreeContext.tsx @@ -13,6 +13,7 @@ export type JsonTreeContextType = { emptyObjectLabel: string; arrowButtonCollapsedLabel: string; arrowButtonExpandedLabel: string; + onNodeValueClick?: (valueAsString: string) => void; }; export const JsonTreeContext = createContext(