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(