From ce195826f5fec9144e72f726b5d829c35271e7f7 Mon Sep 17 00:00:00 2001 From: Anchit Sinha Date: Tue, 14 May 2024 20:32:53 +0530 Subject: [PATCH] 4599-feat(front): Add Copy Button to Floating Inputs (#4789) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #4599 **Changes:** - Added copy button to floating inputs of Text, Number, Phone, Link and Email fields. --------- Co-authored-by: Félix Malfait Co-authored-by: Lucas Bordeau Co-authored-by: Weiko Co-authored-by: Charles Bochet --- .../components/LightCopyIconButton.tsx | 37 ++++++++++++++ .../__stories__/EmailFieldInput.stories.tsx | 3 +- .../__stories__/NumberFieldInput.stories.tsx | 3 +- .../__stories__/PhoneFieldInput.stories.tsx | 3 +- .../__stories__/TextFieldInput.stories.tsx | 3 +- .../input/hooks/useRegisterInputEvents.ts | 5 +- .../input/components/FieldInputOverlay.tsx | 4 +- .../input/components/FieldTextAreaOverlay.tsx | 5 +- .../ui/field/input/components/PhoneInput.tsx | 24 ++++++++- .../field/input/components/TextAreaInput.tsx | 51 ++++++++++++++----- .../ui/field/input/components/TextInput.tsx | 28 +++++++--- .../button/components/LightIconButton.tsx | 2 +- 12 files changed, 137 insertions(+), 31 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/record-field/components/LightCopyIconButton.tsx 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 new file mode 100644 index 000000000000..fe2db91ac3e0 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/components/LightCopyIconButton.tsx @@ -0,0 +1,37 @@ +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import { IconCopy } from 'twenty-ui'; + +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { LightIconButton } from '@/ui/input/button/components/LightIconButton'; + +const StyledButtonContainer = styled.div` + padding: 0 ${({ theme }) => theme.spacing(1)}; +`; + +export type LightCopyIconButtonProps = { + copyText: string; +}; + +export const LightCopyIconButton = ({ copyText }: LightCopyIconButtonProps) => { + const { enqueueSnackBar } = useSnackBar(); + const theme = useTheme(); + + return ( + + { + enqueueSnackBar('Text copied to clipboard', { + variant: 'success', + icon: , + duration: 2000, + }); + navigator.clipboard.writeText(copyText); + }} + aria-label="Copy to Clipboard" + /> + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/EmailFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/EmailFieldInput.stories.tsx index b96085ee8dd5..e8dc0f17f795 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/EmailFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/EmailFieldInput.stories.tsx @@ -4,6 +4,7 @@ import { expect, fn, userEvent, waitFor, within } from '@storybook/test'; import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; import { FieldMetadataType } from '~/generated/graphql'; +import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; import { useEmailField } from '../../../hooks/useEmailField'; @@ -104,7 +105,7 @@ const meta: Meta = { onTab: { control: false }, onShiftTab: { control: false }, }, - decorators: [clearMocksDecorator], + decorators: [clearMocksDecorator, SnackBarDecorator], parameters: { clearMocks: true, }, diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/NumberFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/NumberFieldInput.stories.tsx index 832797657de1..27b128a7c120 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/NumberFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/NumberFieldInput.stories.tsx @@ -4,6 +4,7 @@ import { expect, fn, userEvent, waitFor, within } from '@storybook/test'; import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; import { FieldMetadataType } from '~/generated/graphql'; +import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; import { useNumberField } from '../../../hooks/useNumberField'; @@ -105,7 +106,7 @@ const meta: Meta = { onTab: { control: false }, onShiftTab: { control: false }, }, - decorators: [clearMocksDecorator], + decorators: [clearMocksDecorator, SnackBarDecorator], parameters: { clearMocks: true, }, diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/PhoneFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/PhoneFieldInput.stories.tsx index 761ed49ce005..b6b715636199 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/PhoneFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/PhoneFieldInput.stories.tsx @@ -4,6 +4,7 @@ import { expect, fn, userEvent, waitFor, within } from '@storybook/test'; import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; import { FieldMetadataType } from '~/generated/graphql'; +import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; import { usePhoneField } from '../../../hooks/usePhoneField'; @@ -105,7 +106,7 @@ const meta: Meta = { onTab: { control: false }, onShiftTab: { control: false }, }, - decorators: [clearMocksDecorator], + decorators: [clearMocksDecorator, SnackBarDecorator], parameters: { clearMocks: true, }, diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/TextFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/TextFieldInput.stories.tsx index 09d400028b6e..f5cee3d7a0b7 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/TextFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/TextFieldInput.stories.tsx @@ -4,6 +4,7 @@ import { expect, fn, userEvent, waitFor, within } from '@storybook/test'; import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; import { FieldMetadataType } from '~/generated/graphql'; +import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; import { useTextField } from '../../../hooks/useTextField'; @@ -104,7 +105,7 @@ const meta: Meta = { onTab: { control: false }, onShiftTab: { control: false }, }, - decorators: [clearMocksDecorator], + decorators: [clearMocksDecorator, SnackBarDecorator], parameters: { clearMocks: true, }, diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents.ts index c4aa0b308519..c4ce8f03a7be 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents.ts @@ -6,6 +6,7 @@ import { isDefined } from '~/utils/isDefined'; export const useRegisterInputEvents = ({ inputRef, + copyRef, inputValue, onEscape, onEnter, @@ -15,6 +16,7 @@ export const useRegisterInputEvents = ({ hotkeyScope, }: { inputRef: React.RefObject; + copyRef?: React.RefObject; inputValue: T; onEscape: (inputValue: T) => void; onEnter: (inputValue: T) => void; @@ -24,10 +26,9 @@ export const useRegisterInputEvents = ({ hotkeyScope: string; }) => { useListenClickOutside({ - refs: [inputRef], + refs: [inputRef, copyRef].filter(isDefined), callback: (event) => { event.stopImmediatePropagation(); - onClickOutside?.(event, inputValue); }, enabled: isDefined(onClickOutside), diff --git a/packages/twenty-front/src/modules/ui/field/input/components/FieldInputOverlay.tsx b/packages/twenty-front/src/modules/ui/field/input/components/FieldInputOverlay.tsx index bd5376c32f45..e6fa09f17eca 100644 --- a/packages/twenty-front/src/modules/ui/field/input/components/FieldInputOverlay.tsx +++ b/packages/twenty-front/src/modules/ui/field/input/components/FieldInputOverlay.tsx @@ -3,11 +3,13 @@ import styled from '@emotion/styled'; import { OVERLAY_BACKGROUND } from '@/ui/theme/constants/OverlayBackground'; const StyledFieldInputOverlay = styled.div` + align-items: center; border: ${({ theme }) => `1px solid ${theme.border.color.light}`}; - border-radius: ${({ theme }) => theme.border.radius.sm}; ${OVERLAY_BACKGROUND} + border-radius: ${({ theme }) => theme.border.radius.sm}; display: flex; height: 32px; + justify-content: space-between; margin: -1px; width: 100%; `; diff --git a/packages/twenty-front/src/modules/ui/field/input/components/FieldTextAreaOverlay.tsx b/packages/twenty-front/src/modules/ui/field/input/components/FieldTextAreaOverlay.tsx index 62e1f4fd8224..96fbd18680fa 100644 --- a/packages/twenty-front/src/modules/ui/field/input/components/FieldTextAreaOverlay.tsx +++ b/packages/twenty-front/src/modules/ui/field/input/components/FieldTextAreaOverlay.tsx @@ -1,9 +1,10 @@ import styled from '@emotion/styled'; const StyledFieldTextAreaOverlay = styled.div` - border-radius: ${({ theme }) => theme.border.radius.sm}; - background: ${({ theme }) => theme.background.transparent.secondary}; + align-items: center; backdrop-filter: blur(8px); + background: ${({ theme }) => theme.background.transparent.secondary}; + border-radius: ${({ theme }) => theme.border.radius.sm}; display: flex; height: 32px; margin: -1px; diff --git a/packages/twenty-front/src/modules/ui/field/input/components/PhoneInput.tsx b/packages/twenty-front/src/modules/ui/field/input/components/PhoneInput.tsx index 36b7650c03d2..2b8a406c8765 100644 --- a/packages/twenty-front/src/modules/ui/field/input/components/PhoneInput.tsx +++ b/packages/twenty-front/src/modules/ui/field/input/components/PhoneInput.tsx @@ -1,7 +1,9 @@ import { useEffect, useRef, useState } from 'react'; import ReactPhoneNumberInput from 'react-phone-number-input'; import styled from '@emotion/styled'; +import { TEXT_INPUT_STYLE } from 'twenty-ui'; +import { LightCopyIconButton } from '@/object-record/record-field/components/LightCopyIconButton'; import { useRegisterInputEvents } from '@/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents'; import { PhoneCountryPickerDropdownButton } from '@/ui/input/components/internal/phone/components/PhoneCountryPickerDropdownButton'; @@ -13,14 +15,17 @@ const StyledContainer = styled.div` border: none; border-radius: ${({ theme }) => theme.border.radius.sm}; box-shadow: ${({ theme }) => theme.boxShadow.strong}; + width: 100%; display: flex; - justify-content: center; + justify-content: start; `; const StyledCustomPhoneInput = styled(ReactPhoneNumberInput)` font-family: ${({ theme }) => theme.font.family}; height: 32px; + ${TEXT_INPUT_STYLE} + padding: 0; .PhoneInputInput { background: ${({ theme }) => theme.background.transparent.secondary}; @@ -43,6 +48,14 @@ const StyledCustomPhoneInput = styled(ReactPhoneNumberInput)` border-radius: ${({ theme }) => theme.border.radius.xs}; height: 12px; } + width: calc(100% - ${({ theme }) => theme.spacing(8)}); +`; + +const StyledLightIconButtonContainer = styled.div` + position: absolute; + top: 50%; + transform: translateY(-50%); + right: 0; `; export type PhoneInputProps = { @@ -56,6 +69,7 @@ export type PhoneInputProps = { onClickOutside: (event: MouseEvent | TouchEvent, inputValue: string) => void; onChange?: (newText: string) => void; hotkeyScope: string; + copyButton?: boolean; }; export const PhoneInput = ({ @@ -68,10 +82,12 @@ export const PhoneInput = ({ onClickOutside, hotkeyScope, onChange, + copyButton = true, }: PhoneInputProps) => { const [internalValue, setInternalValue] = useState(value); const wrapperRef = useRef(null); + const copyRef = useRef(null); const handleChange = (newValue: string) => { setInternalValue(newValue); @@ -84,6 +100,7 @@ export const PhoneInput = ({ useRegisterInputEvents({ inputRef: wrapperRef, + copyRef: copyRef, inputValue: internalValue ?? '', onEnter, onEscape, @@ -104,6 +121,11 @@ export const PhoneInput = ({ withCountryCallingCode={true} countrySelectComponent={PhoneCountryPickerDropdownButton} /> + {copyButton && ( + + + + )} ); }; diff --git a/packages/twenty-front/src/modules/ui/field/input/components/TextAreaInput.tsx b/packages/twenty-front/src/modules/ui/field/input/components/TextAreaInput.tsx index 29b754391362..27c5791c4cfc 100644 --- a/packages/twenty-front/src/modules/ui/field/input/components/TextAreaInput.tsx +++ b/packages/twenty-front/src/modules/ui/field/input/components/TextAreaInput.tsx @@ -2,6 +2,7 @@ import { ChangeEvent, useEffect, useRef, useState } from 'react'; import TextareaAutosize from 'react-textarea-autosize'; import styled from '@emotion/styled'; +import { LightCopyIconButton } from '@/object-record/record-field/components/LightCopyIconButton'; import { useRegisterInputEvents } from '@/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents'; import { TEXT_INPUT_STYLE } from '@/ui/theme/constants/TextInputStyle'; import { isDefined } from '~/utils/isDefined'; @@ -20,19 +21,35 @@ export type TextAreaInputProps = { hotkeyScope: string; onChange?: (newText: string) => void; maxRows?: number; + copyButton?: boolean; }; const StyledTextArea = styled(TextareaAutosize)` ${TEXT_INPUT_STYLE} - width: 100%; + align-items: center; + display: flex; + justify-content: center; resize: none; + width: calc(100% - ${({ theme }) => theme.spacing(7)}); +`; + +const StyledTextAreaContainer = styled.div` box-shadow: ${({ theme }) => theme.boxShadow.strong}; border: ${({ theme }) => `1px solid ${theme.border.color.light}`}; - padding: ${({ theme }) => theme.spacing(2)}; + position: relative; + width: 100%; + padding: ${({ theme }) => theme.spacing(2)} ${({ theme }) => theme.spacing(1)}; background-color: ${({ theme }) => theme.background.primary}; border-radius: ${({ theme }) => theme.border.radius.sm}; `; +const StyledLightIconButtonContainer = styled.div` + position: absolute; + top: 50%; + transform: translateY(-50%); + right: 0; +`; + export const TextAreaInput = ({ disabled, className, @@ -47,6 +64,7 @@ export const TextAreaInput = ({ onClickOutside, onChange, maxRows, + copyButton = true, }: TextAreaInputProps) => { const [internalText, setInternalText] = useState(value); @@ -56,6 +74,7 @@ export const TextAreaInput = ({ }; const wrapperRef = useRef(null); + const copyRef = useRef(null); useEffect(() => { if (isDefined(wrapperRef.current)) { @@ -68,6 +87,7 @@ export const TextAreaInput = ({ useRegisterInputEvents({ inputRef: wrapperRef, + copyRef: copyRef, inputValue: internalText, onEnter, onEscape, @@ -78,15 +98,22 @@ export const TextAreaInput = ({ }); return ( - + + + {copyButton && ( + + + + )} + ); }; diff --git a/packages/twenty-front/src/modules/ui/field/input/components/TextInput.tsx b/packages/twenty-front/src/modules/ui/field/input/components/TextInput.tsx index 382897f96f47..26ab63bb3610 100644 --- a/packages/twenty-front/src/modules/ui/field/input/components/TextInput.tsx +++ b/packages/twenty-front/src/modules/ui/field/input/components/TextInput.tsx @@ -1,6 +1,7 @@ import { ChangeEvent, useEffect, useRef, useState } from 'react'; import styled from '@emotion/styled'; +import { LightCopyIconButton } from '@/object-record/record-field/components/LightCopyIconButton'; import { useRegisterInputEvents } from '@/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents'; import { TEXT_INPUT_STYLE } from '@/ui/theme/constants/TextInputStyle'; @@ -21,6 +22,7 @@ type TextInputProps = { onClickOutside: (event: MouseEvent | TouchEvent, inputValue: string) => void; hotkeyScope: string; onChange?: (newText: string) => void; + copyButton?: boolean; }; export const TextInput = ({ @@ -34,10 +36,12 @@ export const TextInput = ({ onShiftTab, onClickOutside, onChange, + copyButton = true, }: TextInputProps) => { const [internalText, setInternalText] = useState(value); const wrapperRef = useRef(null); + const copyRef = useRef(null); const handleChange = (event: ChangeEvent) => { setInternalText(event.target.value); @@ -50,6 +54,7 @@ export const TextInput = ({ useRegisterInputEvents({ inputRef: wrapperRef, + copyRef: copyRef, inputValue: internalText, onEnter, onEscape, @@ -60,13 +65,20 @@ export const TextInput = ({ }); return ( - + <> + + {copyButton && ( +
+ +
+ )} + ); }; diff --git a/packages/twenty-front/src/modules/ui/input/button/components/LightIconButton.tsx b/packages/twenty-front/src/modules/ui/input/button/components/LightIconButton.tsx index 739930c7397a..97ab444e7ec9 100644 --- a/packages/twenty-front/src/modules/ui/input/button/components/LightIconButton.tsx +++ b/packages/twenty-front/src/modules/ui/input/button/components/LightIconButton.tsx @@ -106,7 +106,7 @@ export const LightIconButton = ({ active={active} title={title} > - {Icon && } + {Icon && } ); };