Skip to content

Commit 5496044

Browse files
authored
Add copy button to environment variable values (#209)
- Add a per-row copy action with copied state feedback - Keep value visibility toggle alongside copy controls
1 parent 210b6c9 commit 5496044

1 file changed

Lines changed: 61 additions & 26 deletions

File tree

apps/web/src/components/EnvironmentVariablesEditor.tsx

Lines changed: 61 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { EyeIcon, EyeOffIcon, PlusIcon, Trash2Icon } from "lucide-react";
1+
import { CheckIcon, CopyIcon, EyeIcon, EyeOffIcon, PlusIcon, Trash2Icon } from "lucide-react";
22
import { type CSSProperties, type ReactNode, useEffect, useState } from "react";
33
import {
44
ENVIRONMENT_VARIABLE_KEY_MAX_LENGTH,
@@ -11,6 +11,7 @@ import { Button } from "./ui/button";
1111
import { Input } from "./ui/input";
1212
import { Textarea } from "./ui/textarea";
1313
import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";
14+
import { useCopyToClipboard } from "~/hooks/useCopyToClipboard";
1415
import { cn } from "~/lib/utils";
1516

1617
type DraftRow = {
@@ -125,6 +126,14 @@ export function EnvironmentVariablesEditor({
125126
const [visibleValueRowIds, setVisibleValueRowIds] = useState<Set<string>>(() => new Set());
126127
const [isSaving, setIsSaving] = useState(false);
127128
const [saveError, setSaveError] = useState<string | null>(null);
129+
const [copiedRowId, setCopiedRowId] = useState<string | null>(null);
130+
const { copyToClipboard } = useCopyToClipboard<string>({
131+
timeout: 2000,
132+
onCopy: (rowId) => {
133+
setCopiedRowId(rowId);
134+
setTimeout(() => setCopiedRowId(null), 2000);
135+
},
136+
});
128137

129138
useEffect(() => {
130139
setRows(rowsFromEntries(entries));
@@ -278,31 +287,57 @@ export function EnvironmentVariablesEditor({
278287
>
279288
Value
280289
</label>
281-
<Tooltip>
282-
<TooltipTrigger
283-
render={
284-
<Button
285-
type="button"
286-
size="icon-xs"
287-
variant="ghost"
288-
className="size-6 rounded-md text-muted-foreground hover:text-foreground"
289-
aria-label={isValueVisible ? "Hide value" : "Show value"}
290-
aria-pressed={isValueVisible}
291-
disabled={isReadonly}
292-
onClick={() => toggleValueVisibility(row.id)}
293-
>
294-
{isValueVisible ? (
295-
<EyeOffIcon className="size-3.5" />
296-
) : (
297-
<EyeIcon className="size-3.5" />
298-
)}
299-
</Button>
300-
}
301-
/>
302-
<TooltipPopup side="top">
303-
{isValueVisible ? "Hide value" : "Show value"}
304-
</TooltipPopup>
305-
</Tooltip>
290+
<div className="flex items-center gap-0.5">
291+
<Tooltip>
292+
<TooltipTrigger
293+
render={
294+
<Button
295+
type="button"
296+
size="icon-xs"
297+
variant="ghost"
298+
className="size-6 rounded-md text-muted-foreground hover:text-foreground"
299+
aria-label={isValueVisible ? "Hide value" : "Show value"}
300+
aria-pressed={isValueVisible}
301+
disabled={isReadonly}
302+
onClick={() => toggleValueVisibility(row.id)}
303+
>
304+
{isValueVisible ? (
305+
<EyeOffIcon className="size-3.5" />
306+
) : (
307+
<EyeIcon className="size-3.5" />
308+
)}
309+
</Button>
310+
}
311+
/>
312+
<TooltipPopup side="top">
313+
{isValueVisible ? "Hide value" : "Show value"}
314+
</TooltipPopup>
315+
</Tooltip>
316+
<Tooltip>
317+
<TooltipTrigger
318+
render={
319+
<Button
320+
type="button"
321+
size="icon-xs"
322+
variant="ghost"
323+
className="size-6 rounded-md text-muted-foreground hover:text-foreground"
324+
aria-label="Copy value"
325+
disabled={isReadonly || row.value.length === 0}
326+
onClick={() => copyToClipboard(row.value, row.id)}
327+
>
328+
{copiedRowId === row.id ? (
329+
<CheckIcon className="size-3.5 text-green-500" />
330+
) : (
331+
<CopyIcon className="size-3.5" />
332+
)}
333+
</Button>
334+
}
335+
/>
336+
<TooltipPopup side="top">
337+
{copiedRowId === row.id ? "Copied!" : "Copy value"}
338+
</TooltipPopup>
339+
</Tooltip>
340+
</div>
306341
</div>
307342
<Textarea
308343
id={valueFieldId}

0 commit comments

Comments
 (0)