Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/slick-candies-learn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"gitbook": patch
---

Add confirmation to "Copy MCP URL" page action
60 changes: 41 additions & 19 deletions packages/gitbook/src/components/PageActions/PageActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { Icon, type IconName, IconStyle } from '@gitbook/icons';
import assertNever from 'assert-never';
import QuickLRU from 'quick-lru';
import React from 'react';
import { create } from 'zustand';
import { createStore, useStore } from 'zustand';

type PageActionType = 'button' | 'dropdown-menu-item';

Expand Down Expand Up @@ -41,18 +41,13 @@ export function ActionOpenAssistant(props: { assistant: Assistant; type: PageAct
type CopiedStore = {
copied: boolean;
loading: boolean;
setLoading: (loading: boolean) => void;
copy: (data: string, opts?: { onSuccess?: () => void }) => void;
};

// We need to store everything in a store to share the state between every instance of the component.
const useCopiedStore = create<
CopiedStore & {
setLoading: (loading: boolean) => void;
copy: (data: string, opts?: { onSuccess?: () => void }) => void;
}
>((set) => {
const createCopiedStateStore = () => {
let timeoutRef: ReturnType<typeof setTimeout> | null = null;

return {
return createStore<CopiedStore>()((set) => ({
copied: false,
loading: false,
setLoading: (loading: boolean) => set({ loading }),
Expand All @@ -70,13 +65,25 @@ const useCopiedStore = create<
timeoutRef = setTimeout(() => {
set({ copied: false });
onSuccess?.();

// Reset the timeout ref to avoid multiple timeouts
timeoutRef = null;
}, 1500);
},
};
});
}));
};

const copiedStores = new Map<string, ReturnType<typeof createCopiedStateStore>>();

const getOrCreateCopiedStoreByKey = (storeKey: string) => {
const existing = copiedStores.get(storeKey);
if (existing) return existing;
const created = createCopiedStateStore();
copiedStores.set(storeKey, created);
return created;
};

function useCopiedStore(stateKey: string) {
return useStore(getOrCreateCopiedStoreByKey(stateKey));
}

/**
* Cache for the markdown versbion of the page.
Expand All @@ -96,7 +103,7 @@ export function ActionCopyMarkdown(props: {

const closeDropdown = useDropdownMenuClose();

const { copied, loading, setLoading, copy } = useCopiedStore();
const { copied, loading, setLoading, copy } = useCopiedStore('markdown');

// Fetch the markdown from the page
const fetchMarkdown = async () => {
Expand Down Expand Up @@ -293,14 +300,29 @@ export function CopyToClipboard(props: {
icon: IconName;
}) {
const { type, data, label, description, icon } = props;

const closeDropdown = useDropdownMenuClose();

const language = useLanguage();
const labelKey = label.toLowerCase().replace(/\s+/g, '_');
const { copied, copy } = useCopiedStore(labelKey);

return (
<PageActionWrapper
type={type}
icon={icon}
label={label}
icon={copied ? 'check' : icon}
label={copied ? tString(language, 'code_copied') : label}
description={description}
onClick={() => {
navigator.clipboard.writeText(data);
onClick={(e) => {
e.preventDefault();

copy(data, {
onSuccess: () => {
if (type === 'dropdown-menu-item') {
closeDropdown();
}
},
});
}}
/>
);
Expand Down