From 316f2ec38c2a85a372adbb3721b89fdb9cb49e4b Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras <65061890+Nabhag8848@users.noreply.github.com> Date: Wed, 30 Jul 2025 14:43:55 +0530 Subject: [PATCH] refactor: to useCopyToClipboard to catch errors - when user has disable copy clipboard permission in browser (#13330) as discussed here https://github.com/twentyhq/twenty/issues/13292#issuecomment-3092215050 with @prastoin - [x] introduce optional message param to `copyToClipboard` method in `useCopyToClipboard` and refactor it across app, as in case if user has disallowed clipboard permission in BROWSER unprotected clipboard access breaks without catch. - [x] Email copied to clipboard - [x] run lingui extract --- .../src/hooks/useCopyToClipboard.tsx | 4 +-- .../ObjectOptionsDropdownMenuContent.tsx | 26 ++++------------ .../components/LightCopyIconButton.tsx | 16 ++-------- .../input/components/EmailsFieldInput.tsx | 4 ++- .../SettingsAdminConfigCopyableText.tsx | 18 +++-------- .../developers/components/ApiKeyInput.tsx | 18 ++--------- .../components/SettingsIntegrationMCP.tsx | 16 ++++------ .../components/SSO/SettingsSSOOIDCForm.tsx | 30 +++++++------------ .../components/SSO/SettingsSSOSAMLForm.tsx | 24 ++++----------- .../WorkflowEditTriggerWebhookForm.tsx | 21 ++++--------- .../components/WorkspaceInviteLink.tsx | 17 +++-------- .../src/pages/onboarding/InviteTeam.tsx | 13 ++------ .../workspace/SettingsCustomDomainRecords.tsx | 26 ++++------------ 13 files changed, 61 insertions(+), 172 deletions(-) diff --git a/packages/twenty-front/src/hooks/useCopyToClipboard.tsx b/packages/twenty-front/src/hooks/useCopyToClipboard.tsx index 588590444db..182fd0ce184 100644 --- a/packages/twenty-front/src/hooks/useCopyToClipboard.tsx +++ b/packages/twenty-front/src/hooks/useCopyToClipboard.tsx @@ -8,12 +8,12 @@ export const useCopyToClipboard = () => { const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar(); const { t } = useLingui(); - const copyToClipboard = async (valueAsString: string) => { + const copyToClipboard = async (valueAsString: string, message?: string) => { try { await navigator.clipboard.writeText(valueAsString); enqueueSuccessSnackBar({ - message: t`Copied to clipboard`, + message: message || t`Copied to clipboard`, options: { icon: , duration: 2000, diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownMenuContent.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownMenuContent.tsx index ca07c40e21c..31253b4b29c 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownMenuContent.tsx +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownMenuContent.tsx @@ -3,7 +3,6 @@ import { OBJECT_OPTIONS_DROPDOWN_ID } from '@/object-record/object-options-dropd import { useObjectOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsDropdown'; import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard'; import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState'; -import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; @@ -16,7 +15,6 @@ import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly'; import { ViewType, viewTypeIconMapping } from '@/views/types/ViewType'; import { useDeleteViewFromCurrentState } from '@/views/view-picker/hooks/useDeleteViewFromCurrentState'; import { viewPickerReferenceViewIdComponentState } from '@/views/view-picker/states/viewPickerReferenceViewIdComponentState'; -import { useTheme } from '@emotion/react'; import { useLingui } from '@lingui/react/macro'; import { capitalize, isDefined } from 'twenty-shared/utils'; import { @@ -27,6 +25,7 @@ import { IconTrash, } from 'twenty-ui/display'; import { MenuItem } from 'twenty-ui/navigation'; +import { useCopyToClipboard } from '~/hooks/useCopyToClipboard'; export const ObjectOptionsDropdownMenuContent = () => { const { t } = useLingui(); @@ -62,9 +61,6 @@ export const ObjectOptionsDropdownMenuContent = () => { closeDropdown(); }; - const theme = useTheme(); - const { enqueueSuccessSnackBar } = useSnackBar(); - const isDefaultView = currentView?.key === 'INDEX'; const selectableItemIdArray = [ @@ -80,6 +76,8 @@ export const ObjectOptionsDropdownMenuContent = () => { OBJECT_OPTIONS_DROPDOWN_ID, ); + const { copyToClipboard } = useCopyToClipboard(); + return ( {currentView && ( @@ -167,28 +165,14 @@ export const ObjectOptionsDropdownMenuContent = () => { itemId="Copy link to view" onEnter={() => { const currentUrl = window.location.href; - navigator.clipboard.writeText(currentUrl); - enqueueSuccessSnackBar({ - message: t`Link copied to clipboard`, - options: { - icon: , - duration: 2000, - }, - }); + copyToClipboard(currentUrl, t`Link copied to clipboard`); }} > { const currentUrl = window.location.href; - navigator.clipboard.writeText(currentUrl); - enqueueSuccessSnackBar({ - message: t`Link copied to clipboard`, - options: { - icon: , - duration: 2000, - }, - }); + copyToClipboard(currentUrl, t`Link copied to clipboard`); }} LeftIcon={IconCopy} text={t`Copy link to view`} diff --git a/packages/twenty-front/src/modules/object-record/record-field/components/LightCopyIconButton.tsx b/packages/twenty-front/src/modules/object-record/record-field/components/LightCopyIconButton.tsx index b3e4d2c9a08..f1efa3ad3ff 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/components/LightCopyIconButton.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/components/LightCopyIconButton.tsx @@ -1,10 +1,8 @@ -import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; - -import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useLingui } from '@lingui/react/macro'; import { IconCopy } from 'twenty-ui/display'; import { LightIconButton } from 'twenty-ui/input'; +import { useCopyToClipboard } from '~/hooks/useCopyToClipboard'; const StyledButtonContainer = styled.div` padding: 0 ${({ theme }) => theme.spacing(1)}; @@ -15,8 +13,7 @@ export type LightCopyIconButtonProps = { }; export const LightCopyIconButton = ({ copyText }: LightCopyIconButtonProps) => { - const { enqueueSuccessSnackBar } = useSnackBar(); - const theme = useTheme(); + const { copyToClipboard } = useCopyToClipboard(); const { t } = useLingui(); return ( @@ -24,14 +21,7 @@ export const LightCopyIconButton = ({ copyText }: LightCopyIconButtonProps) => { { - enqueueSuccessSnackBar({ - message: t`Text copied to clipboard`, - options: { - icon: , - duration: 2000, - }, - }); - navigator.clipboard.writeText(copyText); + copyToClipboard(copyText, t`Text copied to clipboard`); }} aria-label="Copy to Clipboard" /> diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/EmailsFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/EmailsFieldInput.tsx index 96c829660b7..96ec9bea8f1 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/EmailsFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/EmailsFieldInput.tsx @@ -3,6 +3,7 @@ import { EmailsFieldMenuItem } from '@/object-record/record-field/meta-types/inp import { recordFieldInputIsFieldInErrorComponentState } from '@/object-record/record-field/states/recordFieldInputIsFieldInErrorComponentState'; import { emailSchema } from '@/object-record/record-field/validation-schemas/emailSchema'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; +import { useLingui } from '@lingui/react/macro'; import { useCallback, useMemo } from 'react'; import { isDefined } from 'twenty-shared/utils'; import { FieldMetadataType } from '~/generated-metadata/graphql'; @@ -20,6 +21,7 @@ export const EmailsFieldInput = ({ }: EmailsFieldInputProps) => { const { persistEmailsField, fieldValue } = useEmailsField(); const { copyToClipboard } = useCopyToClipboard(); + const { t } = useLingui(); const emails = useMemo( () => @@ -59,7 +61,7 @@ export const EmailsFieldInput = ({ }; const handleCopy = (email: string) => { - copyToClipboard(email); + copyToClipboard(email, t`Email copied to clipboard`); }; return ( diff --git a/packages/twenty-front/src/modules/settings/admin-panel/config-variables/components/SettingsAdminConfigCopyableText.tsx b/packages/twenty-front/src/modules/settings/admin-panel/config-variables/components/SettingsAdminConfigCopyableText.tsx index 310315f344d..50b2832f8d1 100644 --- a/packages/twenty-front/src/modules/settings/admin-panel/config-variables/components/SettingsAdminConfigCopyableText.tsx +++ b/packages/twenty-front/src/modules/settings/admin-panel/config-variables/components/SettingsAdminConfigCopyableText.tsx @@ -1,9 +1,7 @@ -import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; -import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { useLingui } from '@lingui/react/macro'; -import { IconCopy, OverflowingTextWithTooltip } from 'twenty-ui/display'; +import { OverflowingTextWithTooltip } from 'twenty-ui/display'; import { useDebouncedCallback } from 'use-debounce'; +import { useCopyToClipboard } from '~/hooks/useCopyToClipboard'; type SettingsAdminConfigCopyableTextProps = { text: string; @@ -32,18 +30,10 @@ export const SettingsAdminConfigCopyableText = ({ multiline = false, maxRows, }: SettingsAdminConfigCopyableTextProps) => { - const { enqueueSuccessSnackBar } = useSnackBar(); - const theme = useTheme(); - const { t } = useLingui(); + const { copyToClipboard } = useCopyToClipboard(); const copyToClipboardDebounced = useDebouncedCallback((value: string) => { - navigator.clipboard.writeText(value); - enqueueSuccessSnackBar({ - message: t`Copied to clipboard!`, - options: { - icon: , - }, - }); + copyToClipboard(value); }, 200); return ( diff --git a/packages/twenty-front/src/modules/settings/developers/components/ApiKeyInput.tsx b/packages/twenty-front/src/modules/settings/developers/components/ApiKeyInput.tsx index ca27a034d5a..fc9aad5d6d3 100644 --- a/packages/twenty-front/src/modules/settings/developers/components/ApiKeyInput.tsx +++ b/packages/twenty-front/src/modules/settings/developers/components/ApiKeyInput.tsx @@ -1,12 +1,9 @@ -import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; - -import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { TextInput } from '@/ui/input/components/TextInput'; - import { useLingui } from '@lingui/react/macro'; import { IconCopy } from 'twenty-ui/display'; import { Button } from 'twenty-ui/input'; +import { useCopyToClipboard } from '~/hooks/useCopyToClipboard'; const StyledContainer = styled.div` display: flex; @@ -21,10 +18,8 @@ const StyledLinkContainer = styled.div` type ApiKeyInputProps = { apiKey: string }; export const ApiKeyInput = ({ apiKey }: ApiKeyInputProps) => { - const theme = useTheme(); const { t } = useLingui(); - - const { enqueueSuccessSnackBar } = useSnackBar(); + const { copyToClipboard } = useCopyToClipboard(); return ( @@ -34,14 +29,7 @@ export const ApiKeyInput = ({ apiKey }: ApiKeyInputProps) => { Icon={IconCopy} title={t`Copy`} onClick={() => { - enqueueSuccessSnackBar({ - message: t`API Key copied to clipboard`, - options: { - icon: , - duration: 2000, - }, - }); - navigator.clipboard.writeText(apiKey); + copyToClipboard(apiKey, t`API Key copied to clipboard`); }} /> diff --git a/packages/twenty-front/src/modules/settings/integrations/components/SettingsIntegrationMCP.tsx b/packages/twenty-front/src/modules/settings/integrations/components/SettingsIntegrationMCP.tsx index bd0dc4d2e2b..fb2cda6df85 100644 --- a/packages/twenty-front/src/modules/settings/integrations/components/SettingsIntegrationMCP.tsx +++ b/packages/twenty-front/src/modules/settings/integrations/components/SettingsIntegrationMCP.tsx @@ -1,4 +1,3 @@ -import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { Select } from '@/ui/input/components/Select'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; @@ -7,6 +6,7 @@ import { useState } from 'react'; import { IconCopy, IconDatabase, IconSitemap } from 'twenty-ui/display'; import { Button, CodeEditor } from 'twenty-ui/input'; import { REACT_APP_SERVER_BASE_URL } from '~/config'; +import { useCopyToClipboard } from '~/hooks/useCopyToClipboard'; const StyledWrapper = styled.div` background-color: ${({ theme }) => theme.background.secondary}; @@ -57,8 +57,8 @@ const StyledEditorContainer = styled.div` export const SettingsIntegrationMCP = () => { const theme = useTheme(); - const { enqueueSuccessSnackBar } = useSnackBar(); const { t } = useLingui(); + const { copyToClipboard } = useCopyToClipboard(); const generateMcpContent = (pathSuffix: string, serverName: string) => { return JSON.stringify( @@ -125,14 +125,10 @@ export const SettingsIntegrationMCP = () => {