Skip to content

Commit 734b4f1

Browse files
committed
refactor: update code to display user errors
1 parent ba26ef3 commit 734b4f1

File tree

5 files changed

+73
-42
lines changed

5 files changed

+73
-42
lines changed

site/src/components/CopyButton/CopyButton.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ export const CopyButton = forwardRef<HTMLButtonElement, CopyButtonProps>(
3232
buttonStyles,
3333
tooltipTitle = Language.tooltipTitle,
3434
} = props;
35-
const { showCopiedSuccess, copyToClipboard } = useClipboard(text);
35+
const { showCopiedSuccess, copyToClipboard } = useClipboard({
36+
textToCopy: text,
37+
});
3638

3739
return (
3840
<Tooltip title={tooltipTitle} placement="top">

site/src/components/CopyableValue/CopyableValue.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ export const CopyableValue: FC<CopyableValueProps> = ({
1616
children,
1717
...attrs
1818
}) => {
19-
const { showCopiedSuccess, copyToClipboard } = useClipboard(value);
19+
const { showCopiedSuccess, copyToClipboard } = useClipboard({
20+
textToCopy: value,
21+
});
2022
const clickableProps = useClickable<HTMLSpanElement>(copyToClipboard);
2123

2224
return (

site/src/hooks/useClipboard.test-setup.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,11 @@
4141
* order of operations involving closure, but you have no idea why the code
4242
* is working, and it's impossible to debug.
4343
*/
44-
import { type UseClipboardResult, useClipboard } from "./useClipboard";
44+
import {
45+
type UseClipboardInput,
46+
type UseClipboardResult,
47+
useClipboard,
48+
} from "./useClipboard";
4549
import { act, renderHook } from "@testing-library/react";
4650

4751
const initialExecCommand = global.document.execCommand;
@@ -95,10 +99,10 @@ function makeMockClipboard(isSecureContext: boolean): MockClipboard {
9599
};
96100
}
97101

98-
function renderUseClipboard(textToCopy: string) {
99-
return renderHook<UseClipboardResult, { hookText: string }>(
100-
({ hookText }) => useClipboard(hookText),
101-
{ initialProps: { hookText: textToCopy } },
102+
function renderUseClipboard(textToCopy: string, displayErrors: boolean) {
103+
return renderHook<UseClipboardResult, UseClipboardInput>(
104+
(props) => useClipboard(props),
105+
{ initialProps: { textToCopy, displayErrors } },
102106
);
103107
}
104108

@@ -154,13 +158,13 @@ export function scheduleClipboardTests({ isHttps }: ScheduleConfig) {
154158
*/
155159
it("Copies the current text to the user's clipboard", async () => {
156160
const hookText = "dogs";
157-
const { result } = renderUseClipboard(hookText);
161+
const { result } = renderUseClipboard(hookText, false);
158162
await assertClipboardTextUpdate(result, hookText);
159163
});
160164

161165
it("Should indicate to components not to show successful copy after a set period of time", async () => {
162166
const hookText = "cats";
163-
const { result } = renderUseClipboard(hookText);
167+
const { result } = renderUseClipboard(hookText, false);
164168
await assertClipboardTextUpdate(result, hookText);
165169

166170
setTimeout(() => {

site/src/hooks/useClipboard.ts

Lines changed: 40 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
11
import { useEffect, useRef, useState } from "react";
2+
import { displayError as dispatchError } from "components/GlobalSnackbar/utils";
23

34
const CLIPBOARD_TIMEOUT_MS = 1_000;
4-
const COPY_FAILED_MESSAGE = "copyToClipboard: failed to copy text to clipboard";
5+
const COPY_FAILED_MESSAGE = "Failed to copy text to clipboard";
6+
7+
export type UseClipboardInput = Readonly<{
8+
textToCopy: string;
9+
10+
/**
11+
* Indicates whether the user should be notified of an error if the copy
12+
* operation fails for whatever reason. Defaults to true.
13+
*/
14+
displayErrors?: boolean;
15+
}>;
516

617
export type UseClipboardResult = Readonly<{
718
copyToClipboard: () => Promise<void>;
@@ -27,7 +38,8 @@ export type UseClipboardResult = Readonly<{
2738
showCopiedSuccess: boolean;
2839
}>;
2940

30-
export const useClipboard = (textToCopy: string): UseClipboardResult => {
41+
export const useClipboard = (input: UseClipboardInput): UseClipboardResult => {
42+
const { textToCopy, displayErrors = true } = input;
3143
const [showCopiedSuccess, setShowCopiedSuccess] = useState(false);
3244
const timeoutIdRef = useRef<number | undefined>();
3345

@@ -43,27 +55,31 @@ export const useClipboard = (textToCopy: string): UseClipboardResult => {
4355
}, CLIPBOARD_TIMEOUT_MS);
4456
};
4557

46-
return {
47-
showCopiedSuccess,
48-
copyToClipboard: async () => {
49-
try {
50-
await window.navigator.clipboard.writeText(textToCopy);
58+
const copyToClipboard = async () => {
59+
try {
60+
await window.navigator.clipboard.writeText(textToCopy);
61+
handleSuccessfulCopy();
62+
} catch (err) {
63+
const fallbackCopySuccessful = simulateClipboardWrite(textToCopy);
64+
65+
if (fallbackCopySuccessful) {
5166
handleSuccessfulCopy();
52-
} catch (err) {
53-
const copySuccessful = simulateClipboardWrite(textToCopy);
54-
if (copySuccessful) {
55-
handleSuccessfulCopy();
56-
} else {
57-
const wrappedErr = new Error(COPY_FAILED_MESSAGE);
58-
if (err instanceof Error) {
59-
wrappedErr.stack = err.stack;
60-
}
61-
62-
console.error(wrappedErr);
63-
}
67+
return;
6468
}
65-
},
69+
70+
const wrappedErr = new Error(COPY_FAILED_MESSAGE);
71+
if (err instanceof Error) {
72+
wrappedErr.stack = err.stack;
73+
}
74+
75+
console.error(wrappedErr);
76+
if (displayErrors) {
77+
dispatchError(COPY_FAILED_MESSAGE);
78+
}
79+
}
6680
};
81+
82+
return { showCopiedSuccess, copyToClipboard };
6783
};
6884

6985
/**
@@ -112,17 +128,17 @@ function simulateClipboardWrite(textToCopy: string): boolean {
112128
*
113129
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Clipboard}
114130
*/
115-
let isCopied: boolean;
131+
let copySuccessful: boolean;
116132
try {
117-
isCopied = document?.execCommand("copy") ?? false;
133+
copySuccessful = document?.execCommand("copy") ?? false;
118134
} catch {
119-
isCopied = false;
135+
copySuccessful = false;
120136
}
121137

122138
dummyInput.remove();
123139
if (previousFocusTarget instanceof HTMLElement) {
124140
previousFocusTarget.focus();
125141
}
126142

127-
return isCopied;
143+
return copySuccessful;
128144
}

site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage.tsx

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,19 +47,26 @@ interface TemplateEmbedPageViewProps {
4747
templateParameters?: TemplateVersionParameter[];
4848
}
4949

50+
function getClipboardCopyContent(
51+
templateName: string,
52+
buttonValues: ButtonValues | undefined,
53+
): string {
54+
const deploymentUrl = `${window.location.protocol}//${window.location.host}`;
55+
const createWorkspaceUrl = `${deploymentUrl}/templates/${templateName}/workspace`;
56+
const createWorkspaceParams = new URLSearchParams(buttonValues);
57+
const buttonUrl = `${createWorkspaceUrl}?${createWorkspaceParams.toString()}`;
58+
59+
return `[![Open in Coder](${deploymentUrl}/open-in-coder.svg)](${buttonUrl})`;
60+
}
61+
5062
export const TemplateEmbedPageView: FC<TemplateEmbedPageViewProps> = ({
5163
template,
5264
templateParameters,
5365
}) => {
54-
const [buttonValues, setButtonValues] = useState<ButtonValues | undefined>(
55-
undefined,
56-
);
57-
const deploymentUrl = `${window.location.protocol}//${window.location.host}`;
58-
const createWorkspaceUrl = `${deploymentUrl}/templates/${template.name}/workspace`;
59-
const createWorkspaceParams = new URLSearchParams(buttonValues);
60-
const buttonUrl = `${createWorkspaceUrl}?${createWorkspaceParams.toString()}`;
61-
const buttonMkdCode = `[![Open in Coder](${deploymentUrl}/open-in-coder.svg)](${buttonUrl})`;
62-
const clipboard = useClipboard(buttonMkdCode);
66+
const [buttonValues, setButtonValues] = useState<ButtonValues | undefined>();
67+
const clipboard = useClipboard({
68+
textToCopy: getClipboardCopyContent(template.name, buttonValues),
69+
});
6370

6471
// template parameters is async so we need to initialize the values after it
6572
// is loaded

0 commit comments

Comments
 (0)