mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-12-24 14:08:45 +00:00
chore(ui): remove react-copy-to-clipboard and add custom hook for copy text (#10534)
* chore(ui): remove react-copy-to-clipboard and add custom hook for copy text * test: add unit test * test: update test imports * test: improve the copy to clipboard button component test
This commit is contained in:
parent
d03b06daf6
commit
60d5285059
@ -70,7 +70,6 @@
|
||||
"react-awesome-query-builder": "5.1.2",
|
||||
"react-codemirror2": "^7.2.1",
|
||||
"react-context-mutex": "^2.0.0",
|
||||
"react-copy-to-clipboard": "^5.0.4",
|
||||
"react-dnd": "14.0.2",
|
||||
"react-dnd-html5-backend": "14.0.2",
|
||||
"react-dom": "^16.14.0",
|
||||
@ -159,7 +158,6 @@
|
||||
"@types/node": "^15.6.1",
|
||||
"@types/pako": "^2.0.0",
|
||||
"@types/react": "^17.0.8",
|
||||
"@types/react-copy-to-clipboard": "^5.0.2",
|
||||
"@types/react-dom": "^17.0.11",
|
||||
"@types/react-lazylog": "^4.5.1",
|
||||
"@types/react-router-dom": "^5.1.7",
|
||||
|
||||
@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="448" height="512" fill="none"><path fill="#37352F" d="M336 0H144c-33.856 0-64 31.056-64 64l-18.496.432C27.664 64.432 0 95.056 0 128v320c0 32.945 30.144 64 64 64h240c33.856 0 64-31.055 64-64h16c33.856 0 64-31.055 64-63.999V128.384L336 0Zm-32 480.001H64c-16.8 0-32-15.695-32-32.001V128c0-16.304 13.68-31.472 30.48-31.472L80 96v288.001C80 416.945 110.144 448 144 448h192c0 16.305-15.2 32.001-32 32.001Zm112.001-96C416.001 400.304 400.8 416 384 416H144c-16.8 0-32-15.696-32-31.999v-320c0-16.304 15.2-32 32-32h160c-.256 36.848 0 64.4 0 64.4 0 33.248 29.92 63.6 64 63.6h48.001v224ZM368 128c-17.039 0-32-30.96-32-47.568V32.464L416.001 128H368Zm-32 96.289H192c-8.832 0-16 7.152-16 15.983 0 8.832 7.168 15.984 16 15.984h144c8.832 0 16-7.152 16-15.984 0-8.831-7.168-15.983-16-15.983Zm0 79.919H192c-8.832 0-16 7.152-16 15.983 0 8.833 7.168 15.985 16 15.985h144c8.832 0 16-7.152 16-15.985 0-8.831-7.168-15.983-16-15.983Z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" fill="none"><path fill="#37352F" d="M336 0H144c-33.856 0-64 31.056-64 64l-18.496.432C27.664 64.432 0 95.056 0 128v320c0 32.945 30.144 64 64 64h240c33.856 0 64-31.055 64-64h16c33.856 0 64-31.055 64-63.999V128.384L336 0Zm-32 480.001H64c-16.8 0-32-15.695-32-32.001V128c0-16.304 13.68-31.472 30.48-31.472L80 96v288.001C80 416.945 110.144 448 144 448h192c0 16.305-15.2 32.001-32 32.001Zm112.001-96C416.001 400.304 400.8 416 384 416H144c-16.8 0-32-15.696-32-31.999v-320c0-16.304 15.2-32 32-32h160c-.256 36.848 0 64.4 0 64.4 0 33.248 29.92 63.6 64 63.6h48.001v224ZM368 128c-17.039 0-32-30.96-32-47.568V32.464L416.001 128H368Zm-32 96.289H192c-8.832 0-16 7.152-16 15.983 0 8.832 7.168 15.984 16 15.984h144c8.832 0 16-7.152 16-15.984 0-8.831-7.168-15.983-16-15.983Zm0 79.919H192c-8.832 0-16 7.152-16 15.983 0 8.833 7.168 15.985 16 15.985h144c8.832 0 16-7.152 16-15.985 0-8.831-7.168-15.983-16-15.983Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 964 B After Width: | Height: | Size: 961 B |
@ -11,23 +11,83 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { getByTestId, render } from '@testing-library/react';
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import CopyToClipboardButton from './CopyToClipboardButton';
|
||||
|
||||
const mockProps = {
|
||||
copyText: 'mock-copy',
|
||||
const clipboardWriteTextMock = jest.fn();
|
||||
const clipboardMock = {
|
||||
writeText: clipboardWriteTextMock,
|
||||
};
|
||||
|
||||
const value = 'Test Value';
|
||||
const callBack = jest.fn();
|
||||
|
||||
Object.defineProperty(window.navigator, 'clipboard', {
|
||||
value: clipboardMock,
|
||||
writable: true,
|
||||
});
|
||||
|
||||
describe('Test CopyToClipboardButton Component', () => {
|
||||
it('Should render all child elements', () => {
|
||||
const { container } = render(<CopyToClipboardButton {...mockProps} />, {
|
||||
wrapper: MemoryRouter,
|
||||
it('Should render all child elements', async () => {
|
||||
render(<CopyToClipboardButton copyText={value} />);
|
||||
|
||||
expect(screen.getByTestId('copy-icon')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('copy-secret')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should calls onCopy callback when clicked', async () => {
|
||||
render(<CopyToClipboardButton copyText={value} onCopy={callBack} />);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByTestId('copy-secret'));
|
||||
});
|
||||
|
||||
const copyIcon = getByTestId(container, 'copy-icon');
|
||||
expect(callBack).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
expect(copyIcon).toBeInTheDocument();
|
||||
it('Should show success message and hide after timeout', async () => {
|
||||
jest.useFakeTimers();
|
||||
await act(async () => {
|
||||
render(<CopyToClipboardButton copyText={value} />);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByTestId('copy-secret'));
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('copy-success')).toBeInTheDocument();
|
||||
|
||||
jest.advanceTimersByTime(1500);
|
||||
|
||||
// success message should not be in the dom after timeout
|
||||
expect(screen.queryByTestId('copy-success')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should have copied text in clipboard', async () => {
|
||||
render(<CopyToClipboardButton copyText={value} />);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByTestId('copy-secret'));
|
||||
});
|
||||
|
||||
// clipboard should have the copied text
|
||||
expect(clipboardWriteTextMock).toHaveBeenCalledWith(value);
|
||||
});
|
||||
|
||||
it('Should handles error when cannot access clipboard API', async () => {
|
||||
Object.defineProperty(window.navigator, 'clipboard', {
|
||||
value: undefined,
|
||||
writable: true,
|
||||
});
|
||||
|
||||
render(<CopyToClipboardButton copyText={value} />);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByTestId('copy-secret'));
|
||||
});
|
||||
|
||||
// not show the success message if clipboard API has error
|
||||
expect(screen.queryByTestId('copy-success')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@ -11,13 +11,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Popover, PopoverProps } from 'antd';
|
||||
import React, { FunctionComponent, useState } from 'react';
|
||||
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||
import { Button, Popover, PopoverProps } from 'antd';
|
||||
import { ReactComponent as CopyIcon } from 'assets/svg/icon-copy.svg';
|
||||
import { useClipboard } from 'hooks/useClipBoard';
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import SVGIcons, { Icons } from '../../../utils/SvgUtils';
|
||||
|
||||
import { Button } from '../Button/Button';
|
||||
|
||||
interface Props {
|
||||
copyText: string;
|
||||
@ -33,44 +31,33 @@ export const CopyToClipboardButton: FunctionComponent<Props> = ({
|
||||
onCopy,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const [copied, setCopied] = useState<boolean>(false);
|
||||
|
||||
const handleCopying = () => {
|
||||
setCopied(true);
|
||||
onCopy?.();
|
||||
setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, copyTimer);
|
||||
};
|
||||
const { hasCopied, onCopyToClipBoard } = useClipboard(
|
||||
copyText,
|
||||
copyTimer,
|
||||
onCopy
|
||||
);
|
||||
|
||||
return (
|
||||
<CopyToClipboard text={copyText} onCopy={handleCopying}>
|
||||
<Popover
|
||||
destroyTooltipOnHide
|
||||
content={
|
||||
<span
|
||||
className="tw-text-grey-body tw-text-xs tw-font-medium tw-italic"
|
||||
data-testid="copy-success">
|
||||
{t('message.copied-to-clipboard')}
|
||||
</span>
|
||||
}
|
||||
open={hasCopied}
|
||||
placement={position}
|
||||
trigger="click">
|
||||
<Button
|
||||
className="tw-h-8 tw-ml-4 tw-relative"
|
||||
data-testid="copy-secret"
|
||||
size="custom"
|
||||
theme="default"
|
||||
variant="text">
|
||||
<Popover
|
||||
content={
|
||||
<span
|
||||
className="tw-text-grey-body tw-text-xs tw-font-medium tw-italic"
|
||||
data-testid="copy-success">
|
||||
{t('message.copied-to-clipboard')}
|
||||
</span>
|
||||
}
|
||||
open={copied}
|
||||
placement={position}
|
||||
trigger="click">
|
||||
<SVGIcons
|
||||
alt="Copy"
|
||||
data-testid="copy-icon"
|
||||
icon={Icons.COPY}
|
||||
width="16px"
|
||||
/>
|
||||
</Popover>
|
||||
</Button>
|
||||
</CopyToClipboard>
|
||||
icon={<CopyIcon data-testid="copy-icon" width="16" />}
|
||||
type="text"
|
||||
onClick={onCopyToClipBoard}
|
||||
/>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2023 Collate.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { act } from '@testing-library/react';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { useClipboard } from './useClipBoard';
|
||||
|
||||
const clipboardWriteTextMock = jest.fn();
|
||||
const clipboardMock = {
|
||||
writeText: clipboardWriteTextMock,
|
||||
};
|
||||
|
||||
Object.defineProperty(window.navigator, 'clipboard', {
|
||||
value: clipboardMock,
|
||||
writable: true,
|
||||
});
|
||||
|
||||
const value = 'Test Value';
|
||||
const callBack = jest.fn();
|
||||
const timeout = 1000;
|
||||
|
||||
describe('useClipboard hook', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('Should copy to clipboard', async () => {
|
||||
clipboardWriteTextMock.mockResolvedValue(value);
|
||||
const { result } = renderHook(() => useClipboard(value, timeout, callBack));
|
||||
|
||||
await act(async () => {
|
||||
result.current.onCopyToClipBoard();
|
||||
});
|
||||
|
||||
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(value);
|
||||
expect(result.current.hasCopied).toBe(true);
|
||||
expect(callBack).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Should handle error while copying to clipboard', async () => {
|
||||
clipboardWriteTextMock.mockRejectedValue('Error');
|
||||
const { result } = renderHook(() => useClipboard(value));
|
||||
|
||||
await act(async () => {
|
||||
result.current.onCopyToClipBoard();
|
||||
});
|
||||
|
||||
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(value);
|
||||
expect(result.current.hasCopied).toBe(false);
|
||||
});
|
||||
|
||||
it('Should reset hasCopied after the timeout', async () => {
|
||||
clipboardWriteTextMock.mockResolvedValue(value);
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
const { result, rerender } = renderHook(
|
||||
({ value, timeout, callBack }) => useClipboard(value, timeout, callBack),
|
||||
{ initialProps: { value, timeout, callBack } }
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
result.current.onCopyToClipBoard();
|
||||
});
|
||||
|
||||
expect(result.current.hasCopied).toBe(true);
|
||||
|
||||
jest.advanceTimersByTime(timeout);
|
||||
|
||||
rerender({ value, timeout, callBack });
|
||||
|
||||
expect(result.current.hasCopied).toBe(false);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2023 Collate.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { toNumber } from 'lodash';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
/**
|
||||
* React hook to copy text to clipboard
|
||||
* @param value the text to copy
|
||||
* @param timeout delay (in ms) to switch back to initial state once copied.
|
||||
* @param callBack execute when content is copied to clipboard
|
||||
*/
|
||||
export const useClipboard = (
|
||||
value: string,
|
||||
timeout = 1500,
|
||||
callBack?: () => void
|
||||
) => {
|
||||
// local state
|
||||
const [hasCopied, setHasCopied] = useState(false);
|
||||
const [valueState, setValueState] = useState(value);
|
||||
|
||||
// handlers
|
||||
const handleCopy = useCallback(async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(valueState);
|
||||
setHasCopied(true);
|
||||
callBack && callBack();
|
||||
} catch (error) {
|
||||
setHasCopied(false);
|
||||
}
|
||||
}, [valueState]);
|
||||
|
||||
// side effects
|
||||
useEffect(() => setValueState(value), [value]);
|
||||
|
||||
useEffect(() => {
|
||||
let timeoutId: number | null = null;
|
||||
|
||||
if (hasCopied) {
|
||||
timeoutId = toNumber(
|
||||
setTimeout(() => {
|
||||
setHasCopied(false);
|
||||
}, timeout)
|
||||
);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
};
|
||||
}, [timeout, hasCopied]);
|
||||
|
||||
return {
|
||||
onCopyToClipBoard: handleCopy,
|
||||
hasCopied,
|
||||
};
|
||||
};
|
||||
@ -3701,13 +3701,6 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
|
||||
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
|
||||
|
||||
"@types/react-copy-to-clipboard@^5.0.2":
|
||||
version "5.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.2.tgz#c29690b472a54edff35916f0d1c6c797ad0fd34b"
|
||||
integrity sha512-O29AThfxrkUFRsZXjfSWR2yaWo0ppB1yLEnHA+Oh24oNetjBAwTDu1PmolIqdJKzsZiO4J1jn6R6TmO96uBvGg==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-dom@*", "@types/react-dom@>=16.9.0":
|
||||
version "17.0.9"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.9.tgz#441a981da9d7be117042e1a6fd3dac4b30f55add"
|
||||
@ -5762,13 +5755,6 @@ copy-descriptor@^0.1.0:
|
||||
resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
|
||||
integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
|
||||
|
||||
copy-to-clipboard@^3:
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz#115aa1a9998ffab6196f93076ad6da3b913662ae"
|
||||
integrity sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw==
|
||||
dependencies:
|
||||
toggle-selection "^1.0.6"
|
||||
|
||||
copy-to-clipboard@^3.2.0:
|
||||
version "3.3.3"
|
||||
resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz#55ac43a1db8ae639a4bd99511c148cdd1b83a1b0"
|
||||
@ -11399,7 +11385,7 @@ prompts@^2.0.1:
|
||||
kleur "^3.0.3"
|
||||
sisteransi "^1.0.5"
|
||||
|
||||
prop-types@^15.5.0, prop-types@^15.5.8, prop-types@^15.6.2, prop-types@^15.7.2:
|
||||
prop-types@^15.5.0, prop-types@^15.6.2, prop-types@^15.7.2:
|
||||
version "15.7.2"
|
||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
|
||||
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
|
||||
@ -12077,14 +12063,6 @@ react-context-mutex@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/react-context-mutex/-/react-context-mutex-2.0.0.tgz#4060bc8fbced6d2680ab093139c727201d402b88"
|
||||
integrity sha512-Qss0YfJAKQ/+CYdVjHB3w7wRLdeetSrp2IMEEouXEpoTdohaCXIKZtFyNMMKDytLVWqHiiXg0RDA2kj/DQbeMQ==
|
||||
|
||||
react-copy-to-clipboard@^5.0.4:
|
||||
version "5.0.4"
|
||||
resolved "https://registry.yarnpkg.com/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.4.tgz#42ec519b03eb9413b118af92d1780c403a5f19bf"
|
||||
integrity sha512-IeVAiNVKjSPeGax/Gmkqfa/+PuMTBhutEvFUaMQLwE2tS0EXrAdgOpWDX26bWTXF3HrioorR7lr08NqeYUWQCQ==
|
||||
dependencies:
|
||||
copy-to-clipboard "^3"
|
||||
prop-types "^15.5.8"
|
||||
|
||||
react-dnd-html5-backend@14.0.2:
|
||||
version "14.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-14.0.2.tgz#25019388f6abdeeda3a6fea835dff155abb2085c"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user