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 { 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}
|
||||
/>
|
||||
</StyledDetailsContainer>
|
||||
)}
|
||||
|
||||
@ -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,
|
||||
}}
|
||||
>
|
||||
<JsonNestedNode
|
||||
|
||||
@ -9,17 +9,19 @@ import { getActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-ac
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import {
|
||||
GetJsonNodeHighlighting,
|
||||
isTwoFirstDepths,
|
||||
JsonTree,
|
||||
} from 'twenty-ui/json-visualizer';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
|
||||
|
||||
export const WorkflowRunStepOutputDetail = ({ stepId }: { stepId: string }) => {
|
||||
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}
|
||||
/>
|
||||
</WorkflowRunStepJsonContainer>
|
||||
</>
|
||||
|
||||
@ -125,6 +125,7 @@ export {
|
||||
IconDotsVertical,
|
||||
IconDownload,
|
||||
IconEditCircle,
|
||||
IconExclamationCircle,
|
||||
IconExternalLink,
|
||||
IconEye,
|
||||
IconEyeOff,
|
||||
|
||||
@ -186,6 +186,7 @@ export {
|
||||
IconDotsVertical,
|
||||
IconDownload,
|
||||
IconEditCircle,
|
||||
IconExclamationCircle,
|
||||
IconExternalLink,
|
||||
IconEye,
|
||||
IconEyeOff,
|
||||
|
||||
@ -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');
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@ -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 (
|
||||
<JsonTreeContextProvider
|
||||
@ -36,6 +38,7 @@ export const JsonTree = ({
|
||||
emptyStringLabel,
|
||||
arrowButtonCollapsedLabel,
|
||||
arrowButtonExpandedLabel,
|
||||
onNodeValueClick,
|
||||
}}
|
||||
>
|
||||
<JsonList depth={0}>
|
||||
|
||||
@ -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 <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;
|
||||
arrowButtonCollapsedLabel: string;
|
||||
arrowButtonExpandedLabel: string;
|
||||
onNodeValueClick?: (valueAsString: string) => void;
|
||||
};
|
||||
|
||||
export const JsonTreeContext = createContext<JsonTreeContextType | undefined>(
|
||||
|
||||
Reference in New Issue
Block a user