Copy JSON values on click (#11382)
https://github.com/user-attachments/assets/1638c196-fb9c-4f2b-910c-6d8b0693a37a Closes https://github.com/twentyhq/core-team-issues/issues/568
This commit is contained in:
committed by
GitHub
parent
bb40bc9929
commit
9353e777ea
31
packages/twenty-front/src/hooks/useCopyToClipboard.tsx
Normal file
31
packages/twenty-front/src/hooks/useCopyToClipboard.tsx
Normal file
@ -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: <IconCopy size={theme.icon.size.md} />,
|
||||||
|
duration: 2000,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
enqueueSnackBar(t`Couldn't copy to clipboard`, {
|
||||||
|
variant: SnackBarVariant.Error,
|
||||||
|
icon: <IconExclamationCircle size={16} color="red" />,
|
||||||
|
duration: 2000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { copyToClipboard };
|
||||||
|
};
|
||||||
@ -2,9 +2,10 @@ import { SettingsAdminIndicatorHealthContext } from '@/settings/admin-panel/heal
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useLingui } from '@lingui/react/macro';
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import { AdminPanelHealthServiceStatus } from '~/generated/graphql';
|
|
||||||
import { JsonTree } from 'twenty-ui/json-visualizer';
|
import { JsonTree } from 'twenty-ui/json-visualizer';
|
||||||
import { Section } from 'twenty-ui/layout';
|
import { Section } from 'twenty-ui/layout';
|
||||||
|
import { AdminPanelHealthServiceStatus } from '~/generated/graphql';
|
||||||
|
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
|
||||||
|
|
||||||
const StyledDetailsContainer = styled.div`
|
const StyledDetailsContainer = styled.div`
|
||||||
background-color: ${({ theme }) => theme.background.secondary};
|
background-color: ${({ theme }) => theme.background.secondary};
|
||||||
@ -23,6 +24,7 @@ const StyledErrorMessage = styled.div`
|
|||||||
|
|
||||||
export const JsonDataIndicatorHealthStatus = () => {
|
export const JsonDataIndicatorHealthStatus = () => {
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
|
const { copyToClipboard } = useCopyToClipboard();
|
||||||
|
|
||||||
const { indicatorHealth } = useContext(SettingsAdminIndicatorHealthContext);
|
const { indicatorHealth } = useContext(SettingsAdminIndicatorHealthContext);
|
||||||
|
|
||||||
@ -54,6 +56,7 @@ export const JsonDataIndicatorHealthStatus = () => {
|
|||||||
emptyStringLabel={t`[empty string]`}
|
emptyStringLabel={t`[empty string]`}
|
||||||
arrowButtonCollapsedLabel={t`Expand`}
|
arrowButtonCollapsedLabel={t`Expand`}
|
||||||
arrowButtonExpandedLabel={t`Collapse`}
|
arrowButtonExpandedLabel={t`Collapse`}
|
||||||
|
onNodeValueClick={copyToClipboard}
|
||||||
/>
|
/>
|
||||||
</StyledDetailsContainer>
|
</StyledDetailsContainer>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -19,11 +19,13 @@ import {
|
|||||||
JsonTreeContextProvider,
|
JsonTreeContextProvider,
|
||||||
ShouldExpandNodeInitiallyProps,
|
ShouldExpandNodeInitiallyProps,
|
||||||
} from 'twenty-ui/json-visualizer';
|
} from 'twenty-ui/json-visualizer';
|
||||||
|
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
|
||||||
|
|
||||||
export const WorkflowRunStepInputDetail = ({ stepId }: { stepId: string }) => {
|
export const WorkflowRunStepInputDetail = ({ stepId }: { stepId: string }) => {
|
||||||
const { t, i18n } = useLingui();
|
const { t, i18n } = useLingui();
|
||||||
const { getIcon } = useIcons();
|
const { getIcon } = useIcons();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const { copyToClipboard } = useCopyToClipboard();
|
||||||
|
|
||||||
const workflowRunId = useWorkflowRunIdOrThrow();
|
const workflowRunId = useWorkflowRunIdOrThrow();
|
||||||
const workflowRun = useWorkflowRun({ workflowRunId });
|
const workflowRun = useWorkflowRun({ workflowRunId });
|
||||||
@ -123,6 +125,7 @@ export const WorkflowRunStepInputDetail = ({ stepId }: { stepId: string }) => {
|
|||||||
arrowButtonExpandedLabel: t`Collapse`,
|
arrowButtonExpandedLabel: t`Collapse`,
|
||||||
getNodeHighlighting,
|
getNodeHighlighting,
|
||||||
shouldExpandNodeInitially: isFirstNodeDepthOfPreviousStep,
|
shouldExpandNodeInitially: isFirstNodeDepthOfPreviousStep,
|
||||||
|
onNodeValueClick: copyToClipboard,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<JsonNestedNode
|
<JsonNestedNode
|
||||||
|
|||||||
@ -9,17 +9,19 @@ import { getActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-ac
|
|||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import { useLingui } from '@lingui/react/macro';
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
import { useIcons } from 'twenty-ui/display';
|
||||||
import {
|
import {
|
||||||
GetJsonNodeHighlighting,
|
GetJsonNodeHighlighting,
|
||||||
isTwoFirstDepths,
|
isTwoFirstDepths,
|
||||||
JsonTree,
|
JsonTree,
|
||||||
} from 'twenty-ui/json-visualizer';
|
} from 'twenty-ui/json-visualizer';
|
||||||
import { useIcons } from 'twenty-ui/display';
|
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
|
||||||
|
|
||||||
export const WorkflowRunStepOutputDetail = ({ stepId }: { stepId: string }) => {
|
export const WorkflowRunStepOutputDetail = ({ stepId }: { stepId: string }) => {
|
||||||
const { t, i18n } = useLingui();
|
const { t, i18n } = useLingui();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { getIcon } = useIcons();
|
const { getIcon } = useIcons();
|
||||||
|
const { copyToClipboard } = useCopyToClipboard();
|
||||||
|
|
||||||
const workflowRunId = useWorkflowRunIdOrThrow();
|
const workflowRunId = useWorkflowRunIdOrThrow();
|
||||||
const workflowRun = useWorkflowRun({ workflowRunId });
|
const workflowRun = useWorkflowRun({ workflowRunId });
|
||||||
@ -73,6 +75,7 @@ export const WorkflowRunStepOutputDetail = ({ stepId }: { stepId: string }) => {
|
|||||||
? setRedHighlightingForEveryNode
|
? setRedHighlightingForEveryNode
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
onNodeValueClick={copyToClipboard}
|
||||||
/>
|
/>
|
||||||
</WorkflowRunStepJsonContainer>
|
</WorkflowRunStepJsonContainer>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -125,6 +125,7 @@ export {
|
|||||||
IconDotsVertical,
|
IconDotsVertical,
|
||||||
IconDownload,
|
IconDownload,
|
||||||
IconEditCircle,
|
IconEditCircle,
|
||||||
|
IconExclamationCircle,
|
||||||
IconExternalLink,
|
IconExternalLink,
|
||||||
IconEye,
|
IconEye,
|
||||||
IconEyeOff,
|
IconEyeOff,
|
||||||
|
|||||||
@ -186,6 +186,7 @@ export {
|
|||||||
IconDotsVertical,
|
IconDotsVertical,
|
||||||
IconDownload,
|
IconDownload,
|
||||||
IconEditCircle,
|
IconEditCircle,
|
||||||
|
IconExclamationCircle,
|
||||||
IconExternalLink,
|
IconExternalLink,
|
||||||
IconEye,
|
IconEye,
|
||||||
IconEyeOff,
|
IconEyeOff,
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import { Meta, StoryObj } from '@storybook/react';
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
import {
|
import {
|
||||||
expect,
|
expect,
|
||||||
|
fn,
|
||||||
userEvent,
|
userEvent,
|
||||||
|
waitFor,
|
||||||
waitForElementToBeRemoved,
|
waitForElementToBeRemoved,
|
||||||
within,
|
within,
|
||||||
} from '@storybook/test';
|
} from '@storybook/test';
|
||||||
@ -482,3 +484,32 @@ export const RedHighlighting: Story = {
|
|||||||
expect(ageElement).toBeVisible();
|
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');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
@ -14,6 +14,7 @@ export const JsonTree = ({
|
|||||||
emptyStringLabel,
|
emptyStringLabel,
|
||||||
arrowButtonCollapsedLabel,
|
arrowButtonCollapsedLabel,
|
||||||
arrowButtonExpandedLabel,
|
arrowButtonExpandedLabel,
|
||||||
|
onNodeValueClick,
|
||||||
}: {
|
}: {
|
||||||
value: JsonValue;
|
value: JsonValue;
|
||||||
getNodeHighlighting?: GetJsonNodeHighlighting;
|
getNodeHighlighting?: GetJsonNodeHighlighting;
|
||||||
@ -25,6 +26,7 @@ export const JsonTree = ({
|
|||||||
emptyStringLabel: string;
|
emptyStringLabel: string;
|
||||||
arrowButtonCollapsedLabel: string;
|
arrowButtonCollapsedLabel: string;
|
||||||
arrowButtonExpandedLabel: string;
|
arrowButtonExpandedLabel: string;
|
||||||
|
onNodeValueClick?: (valueAsString: string) => void;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<JsonTreeContextProvider
|
<JsonTreeContextProvider
|
||||||
@ -36,6 +38,7 @@ export const JsonTree = ({
|
|||||||
emptyStringLabel,
|
emptyStringLabel,
|
||||||
arrowButtonCollapsedLabel,
|
arrowButtonCollapsedLabel,
|
||||||
arrowButtonExpandedLabel,
|
arrowButtonExpandedLabel,
|
||||||
|
onNodeValueClick,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<JsonList depth={0}>
|
<JsonList depth={0}>
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { useJsonTreeContextOrThrow } from '@ui/json-visualizer/hooks/useJsonTreeContextOrThrow';
|
||||||
import { JsonNodeHighlighting } from '@ui/json-visualizer/types/JsonNodeHighlighting';
|
import { JsonNodeHighlighting } from '@ui/json-visualizer/types/JsonNodeHighlighting';
|
||||||
|
|
||||||
const StyledText = styled.span<{
|
const StyledText = styled.span<{
|
||||||
@ -19,5 +20,15 @@ export const JsonNodeValue = ({
|
|||||||
valueAsString: string;
|
valueAsString: string;
|
||||||
highlighting?: JsonNodeHighlighting | undefined;
|
highlighting?: JsonNodeHighlighting | undefined;
|
||||||
}) => {
|
}) => {
|
||||||
return <StyledText highlighting={highlighting}>{valueAsString}</StyledText>;
|
const { onNodeValueClick } = useJsonTreeContextOrThrow();
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
onNodeValueClick?.(valueAsString);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledText highlighting={highlighting} onClick={handleClick}>
|
||||||
|
{valueAsString}
|
||||||
|
</StyledText>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -13,6 +13,7 @@ export type JsonTreeContextType = {
|
|||||||
emptyObjectLabel: string;
|
emptyObjectLabel: string;
|
||||||
arrowButtonCollapsedLabel: string;
|
arrowButtonCollapsedLabel: string;
|
||||||
arrowButtonExpandedLabel: string;
|
arrowButtonExpandedLabel: string;
|
||||||
|
onNodeValueClick?: (valueAsString: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const JsonTreeContext = createContext<JsonTreeContextType | undefined>(
|
export const JsonTreeContext = createContext<JsonTreeContextType | undefined>(
|
||||||
|
|||||||
Reference in New Issue
Block a user