diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/ArrayFieldMenuItem.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/ArrayFieldMenuItem.tsx index dce319d0f..6501a5328 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/ArrayFieldMenuItem.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/ArrayFieldMenuItem.tsx @@ -21,7 +21,8 @@ export const ArrayFieldMenuItem = ({ onEdit={onEdit} onDelete={onDelete} DisplayComponent={() => } - hasPrimaryButton={false} + showPrimaryIcon={false} + showSetAsPrimaryButton={false} /> ); }; 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 99371d125..aa3ef4376 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 @@ -77,7 +77,8 @@ export const EmailsFieldInput = ({ void; onSetAsPrimary?: () => void; onDelete?: () => void; email: string; + showPrimaryIcon: boolean; + showSetAsPrimaryButton: boolean; }; export const EmailsFieldMenuItem = ({ dropdownId, - isPrimary, onEdit, onSetAsPrimary, onDelete, email, + showPrimaryIcon, + showSetAsPrimaryButton, }: EmailsFieldMenuItemProps) => { return ( ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/LinksFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/LinksFieldInput.tsx index e38906ed5..24faf715a 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/LinksFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/LinksFieldInput.tsx @@ -38,7 +38,8 @@ export const LinksFieldInput = ({ }); }; - const isPrimaryLink = (index: number) => index === 0 && links?.length > 1; + const getShowPrimaryIcon = (index: number) => index === 0 && links.length > 1; + const getShowSetAsPrimaryButton = (index: number) => index > 0; const setIsFieldInError = useSetRecoilComponentStateV2( recordFieldInputIsFieldInErrorComponentState, @@ -75,7 +76,8 @@ export const LinksFieldInput = ({ void; onSetAsPrimary?: () => void; onDelete?: () => void; + showPrimaryIcon: boolean; + showSetAsPrimaryButton: boolean; }; export const LinksFieldMenuItem = ({ dropdownId, - isPrimary, label, onEdit, onSetAsPrimary, onDelete, url, + showPrimaryIcon, + showSetAsPrimaryButton, }: LinksFieldMenuItemProps) => { return ( = { dropdownId: string; - isPrimary?: boolean; value: T; onEdit?: () => void; onSetAsPrimary?: () => void; onDelete?: () => void; DisplayComponent: React.ComponentType<{ value: T }>; - hasPrimaryButton?: boolean; + showPrimaryIcon: boolean; + showSetAsPrimaryButton: boolean; }; export const MultiItemFieldMenuItem = ({ dropdownId, - isPrimary, value, onEdit, onSetAsPrimary, onDelete, DisplayComponent, - hasPrimaryButton = true, + showPrimaryIcon, + showSetAsPrimaryButton, }: MultiItemFieldMenuItemProps) => { const [isHovered, setIsHovered] = useState(false); const { isDropdownOpen, closeDropdown } = useDropdown(dropdownId); @@ -69,12 +69,12 @@ export const MultiItemFieldMenuItem = ({ onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} text={} - isIconDisplayedOnHoverOnly={!isPrimary && !isDropdownOpen} - RightIcon={!isHovered && isPrimary ? IconBookmark : null} + isIconDisplayedOnHoverOnly={!showPrimaryIcon && !isDropdownOpen} + RightIcon={!isHovered && showPrimaryIcon ? IconBookmark : null} dropdownId={dropdownId} dropdownContent={ - {hasPrimaryButton && !isPrimary && ( + {showSetAsPrimaryButton && ( void; onSetAsPrimary?: () => void; onDelete?: () => void; phone: { number: string; callingCode: string }; + showPrimaryIcon: boolean; + showSetAsPrimaryButton: boolean; }; export const PhonesFieldMenuItem = ({ dropdownId, - isPrimary, onEdit, onSetAsPrimary, onDelete, phone, + showPrimaryIcon, + showSetAsPrimaryButton, }: PhonesFieldMenuItemProps) => { return ( ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/ArrayFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/ArrayFieldInput.stories.tsx new file mode 100644 index 000000000..9ae911027 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/ArrayFieldInput.stories.tsx @@ -0,0 +1,127 @@ +import { expect } from '@storybook/jest'; +import { Meta, StoryObj } from '@storybook/react'; +import { fn, userEvent, within } from '@storybook/test'; +import { useEffect } from 'react'; + +import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; +import { useArrayField } from '@/object-record/record-field/meta-types/hooks/useArrayField'; +import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; +import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2'; +import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId'; +import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; +import { ArrayFieldInput } from '../ArrayFieldInput'; + +const updateRecord = fn(); + +const ArrayValueSetterEffect = ({ value }: { value: string[] }) => { + const { setFieldValue } = useArrayField(); + + useEffect(() => { + setFieldValue(value); + }, [setFieldValue, value]); + + return null; +}; + +type ArrayFieldValueGaterProps = Pick< + ArrayInputWithContextProps, + 'onCancel' | 'onClickOutside' +>; + +const ArrayFieldValueGater = ({ + onCancel, + onClickOutside, +}: ArrayFieldValueGaterProps) => { + const { fieldValue } = useArrayField(); + + return ( + fieldValue && ( + + ) + ); +}; + +type ArrayInputWithContextProps = { + value: string[]; + recordId?: string; + onCancel?: () => void; + onClickOutside?: (event: MouseEvent | TouchEvent) => void; +}; + +const ArrayInputWithContext = ({ + value, + recordId = 'record-id', + onCancel, + onClickOutside, +}: ArrayInputWithContextProps) => { + const setHotkeyScope = useSetHotkeyScope(); + + useEffect(() => { + setHotkeyScope(DEFAULT_CELL_SCOPE.scope); + }, [setHotkeyScope]); + + return ( + + [updateRecord, { loading: false }], + }} + > + + + + + ); +}; + +const meta: Meta = { + title: 'UI/Input/ArrayFieldInput', + component: ArrayInputWithContext, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + value: ['tag1', 'tag2'], + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const addButton = await canvas.findByText('Add Item'); + await userEvent.click(addButton); + + const input = await canvas.findByPlaceholderText('Enter value'); + await userEvent.type(input, 'tag3{enter}'); + + const tag3Element = await canvas.findByText('tag3'); + expect(tag3Element).toBeVisible(); + }, +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/EmailsFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/EmailsFieldInput.stories.tsx new file mode 100644 index 000000000..1ca4a38a7 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/EmailsFieldInput.stories.tsx @@ -0,0 +1,179 @@ +import { expect } from '@storybook/jest'; +import { Meta, StoryObj } from '@storybook/react'; +import { fn, userEvent, waitFor, within } from '@storybook/test'; +import { useEffect } from 'react'; +import { getCanvasElementForDropdownTesting } from 'twenty-ui/testing'; + +import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; +import { useEmailsField } from '@/object-record/record-field/meta-types/hooks/useEmailsField'; +import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; +import { FieldEmailsValue } from '@/object-record/record-field/types/FieldMetadata'; +import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2'; +import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId'; +import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; +import { EmailsFieldInput } from '../EmailsFieldInput'; + +const updateRecord = fn(); + +const EmailValueSetterEffect = ({ value }: { value: FieldEmailsValue }) => { + const { setFieldValue } = useEmailsField(); + + useEffect(() => { + setFieldValue(value); + }, [setFieldValue, value]); + + return null; +}; + +type EmailFieldValueGaterProps = Pick< + EmailInputWithContextProps, + 'onCancel' | 'onClickOutside' +>; + +const EmailFieldValueGater = ({ + onCancel, + onClickOutside, +}: EmailFieldValueGaterProps) => { + const { fieldValue } = useEmailsField(); + + return ( + fieldValue && ( + + ) + ); +}; + +type EmailInputWithContextProps = { + value: FieldEmailsValue; + recordId?: string; + onCancel?: () => void; + onClickOutside?: (event: MouseEvent | TouchEvent) => void; +}; + +const EmailInputWithContext = ({ + value, + recordId = 'record-id', + onCancel, + onClickOutside, +}: EmailInputWithContextProps) => { + const setHotkeyScope = useSetHotkeyScope(); + + useEffect(() => { + setHotkeyScope(DEFAULT_CELL_SCOPE.scope); + }, [setHotkeyScope]); + + return ( + + [updateRecord, { loading: false }], + }} + > + + + + + ); +}; + +const meta: Meta = { + title: 'UI/Input/EmailsFieldInput', + component: EmailInputWithContext, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + value: { + primaryEmail: 'john@example.com', + additionalEmails: ['john.doe@example.com'], + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const addButton = await canvas.findByText('Add Email'); + await userEvent.click(addButton); + + const input = await canvas.findByPlaceholderText('Email'); + await userEvent.type(input, 'new.email@example.com{enter}'); + + const newEmailElement = await canvas.findByText('new.email@example.com'); + expect(newEmailElement).toBeVisible(); + }, +}; + +// FIXME: We will have to fix that behavior, we should only be able to set a secondary email as the primary email +export const CanSetPrimaryLinkAsPrimaryLink: Story = { + args: { + value: { + primaryEmail: 'primary@example.com', + additionalEmails: [], + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const primaryEmail = await canvas.findByText('primary@example.com'); + expect(primaryEmail).toBeVisible(); + + await userEvent.hover(primaryEmail); + + const openDropdownButtons = await canvas.findAllByRole('button', { + expanded: false, + }); + await userEvent.click(openDropdownButtons[0]); + + const setPrimaryOption = await within( + getCanvasElementForDropdownTesting(), + ).findByText('Set as Primary'); + + expect(setPrimaryOption).toBeVisible(); + + await userEvent.click(setPrimaryOption); + + // Verify the update was called with swapped emails + await waitFor(() => { + expect(updateRecord).toHaveBeenCalledWith({ + variables: { + where: { id: 'record-id' }, + updateOneRecordInput: { + emails: { + primaryEmail: 'primary@example.com', + additionalEmails: [], + }, + }, + }, + }); + }); + expect(updateRecord).toHaveBeenCalledTimes(1); + }, +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/LinksFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/LinksFieldInput.stories.tsx index ff7b3e0f8..3a2a46bf7 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/LinksFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/LinksFieldInput.stories.tsx @@ -115,6 +115,10 @@ const LinksInputWithContext = ({ ); }; +const getPrimaryLinkBookmarkIcon = (canvasElement: HTMLElement) => + // It would be better to use an aria-label on the icon, but we'll do this for now + canvasElement.querySelector('svg[class*="tabler-icon-bookmark"]'); + const cancelJestFn = fn(); const clickOutsideJestFn = fn(); @@ -172,10 +176,12 @@ export const PrimaryLinkOnly: Story = { const canvas = within(canvasElement); const primaryLink = await canvas.findByText('Twenty Website'); - await expect(primaryLink).toBeVisible(); + expect(primaryLink).toBeVisible(); const addButton = await canvas.findByText('Add URL'); - await expect(addButton).toBeVisible(); + expect(addButton).toBeVisible(); + + expect(getPrimaryLinkBookmarkIcon(canvasElement)).not.toBeInTheDocument(); }, }; @@ -200,16 +206,20 @@ export const WithSecondaryLinks: Story = { const canvas = within(canvasElement); const primaryLink = await canvas.findByText('Twenty Website'); - await expect(primaryLink).toBeVisible(); + expect(primaryLink).toBeVisible(); + + await waitFor(() => { + expect(getPrimaryLinkBookmarkIcon(canvasElement)).toBeVisible(); + }); const documentationLink = await canvas.findByText('Documentation'); - await expect(documentationLink).toBeVisible(); + expect(documentationLink).toBeVisible(); const githubLink = await canvas.findByText('GitHub'); - await expect(githubLink).toBeVisible(); + expect(githubLink).toBeVisible(); const addButton = await canvas.findByText('Add URL'); - await expect(addButton).toBeVisible(); + expect(addButton).toBeVisible(); }, }; @@ -221,7 +231,7 @@ export const CreatePrimaryLink: Story = { await userEvent.type(input, 'https://www.twenty.com{enter}'); const linkDisplay = await canvas.findByText('twenty.com'); - await expect(linkDisplay).toBeVisible(); + expect(linkDisplay).toBeVisible(); await waitFor(() => { expect(updateRecord).toHaveBeenCalledWith({ @@ -238,6 +248,8 @@ export const CreatePrimaryLink: Story = { }); }); expect(updateRecord).toHaveBeenCalledTimes(1); + + expect(getPrimaryLinkBookmarkIcon(canvasElement)).not.toBeInTheDocument(); }, }; @@ -252,16 +264,19 @@ export const AddSecondaryLink: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); + const primaryLink = await canvas.findByText('Twenty Website'); + expect(primaryLink).toBeVisible(); + + expect(getPrimaryLinkBookmarkIcon(canvasElement)).not.toBeInTheDocument(); + const addButton = await canvas.findByText('Add URL'); await userEvent.click(addButton); const input = await canvas.findByPlaceholderText('URL'); await userEvent.type(input, 'https://docs.twenty.com{enter}'); - const primaryLink = await canvas.findByText('Twenty Website'); const secondaryLink = await canvas.findByText('docs.twenty.com'); - await expect(primaryLink).toBeVisible(); - await expect(secondaryLink).toBeVisible(); + expect(secondaryLink).toBeVisible(); await waitFor(() => { expect(updateRecord).toHaveBeenCalledWith({ @@ -298,7 +313,9 @@ export const DeletePrimaryLink: Story = { const canvas = within(canvasElement); const listItemToDelete = await canvas.findByText('Twenty Website'); - await userEvent.hover(listItemToDelete); + expect(listItemToDelete).toBeVisible(); + + expect(getPrimaryLinkBookmarkIcon(canvasElement)).not.toBeInTheDocument(); const openDropdownButton = await canvas.findByRole('button', { expanded: false, @@ -311,8 +328,8 @@ export const DeletePrimaryLink: Story = { await userEvent.click(deleteOption); const input = await canvas.findByPlaceholderText('URL'); - await expect(input).toBeVisible(); - await expect(input).toHaveValue(''); + expect(input).toBeVisible(); + expect(input).toHaveValue(''); await waitFor(() => { expect(updateRecord).toHaveBeenCalledWith({ @@ -332,6 +349,64 @@ export const DeletePrimaryLink: Story = { }, }; +export const DeletePrimaryLinkAndUseSecondaryLinkAsTheNewPrimaryLink: Story = { + args: { + value: { + primaryLinkUrl: 'https://www.twenty.com', + primaryLinkLabel: 'Twenty Website', + secondaryLinks: [ + { + url: 'https://docs.twenty.com', + label: 'Documentation', + }, + ], + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const listItemToDelete = await canvas.findByText('Twenty Website'); + expect(listItemToDelete).toBeVisible(); + + await waitFor(() => { + expect(getPrimaryLinkBookmarkIcon(canvasElement)).toBeVisible(); + }); + + const openDropdownButtons = await canvas.findAllByRole('button', { + expanded: false, + }); + await userEvent.click(openDropdownButtons[0]); + + const deleteOption = await within( + getCanvasElementForDropdownTesting(), + ).findByText('Delete'); + await userEvent.click(deleteOption); + + const newPrimaryLink = await canvas.findByText('Documentation'); + expect(newPrimaryLink).toBeVisible(); + const oldPrimaryLink = canvas.queryByText('Twenty Website'); + expect(oldPrimaryLink).not.toBeInTheDocument(); + + expect(getPrimaryLinkBookmarkIcon(canvasElement)).not.toBeInTheDocument(); + + await waitFor(() => { + expect(updateRecord).toHaveBeenCalledWith({ + variables: { + where: { id: '123' }, + updateOneRecordInput: { + links: { + primaryLinkUrl: 'https://docs.twenty.com', + primaryLinkLabel: 'Documentation', + secondaryLinks: [], + }, + }, + }, + }); + }); + expect(updateRecord).toHaveBeenCalledTimes(1); + }, +}; + export const DeleteSecondaryLink: Story = { args: { value: { @@ -348,6 +423,10 @@ export const DeleteSecondaryLink: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); + await waitFor(() => { + expect(getPrimaryLinkBookmarkIcon(canvasElement)).toBeVisible(); + }); + const listItemToDelete = await canvas.findByText('Documentation'); await userEvent.hover(listItemToDelete); @@ -362,9 +441,11 @@ export const DeleteSecondaryLink: Story = { await userEvent.click(deleteOption); const primaryLink = await canvas.findByText('Twenty Website'); - await expect(primaryLink).toBeVisible(); + expect(primaryLink).toBeVisible(); const secondaryLink = canvas.queryByText('Documentation'); - await expect(secondaryLink).not.toBeInTheDocument(); + expect(secondaryLink).not.toBeInTheDocument(); + + expect(getPrimaryLinkBookmarkIcon(canvasElement)).not.toBeInTheDocument(); await waitFor(() => { expect(updateRecord).toHaveBeenCalledWith({ @@ -444,3 +525,94 @@ export const InvalidUrls: Story = { expect(canvas.queryByText('Invalid Characters')).not.toBeInTheDocument(); }, }; + +export const MakeSecondaryLinkPrimary: Story = { + args: { + value: { + primaryLinkUrl: 'https://www.twenty.com', + primaryLinkLabel: 'Twenty Website', + secondaryLinks: [ + { + url: 'https://docs.twenty.com', + label: 'Documentation', + }, + ], + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const primaryLink = await canvas.findByText('Twenty Website'); + expect(primaryLink).toBeVisible(); + + const secondaryLink = await canvas.findByText('Documentation'); + expect(secondaryLink).toBeVisible(); + + await waitFor(() => { + expect(getPrimaryLinkBookmarkIcon(canvasElement)).toBeVisible(); + }); + + await userEvent.hover(secondaryLink); + + const openDropdownButtons = await canvas.findAllByRole('button', { + expanded: false, + }); + await userEvent.click(openDropdownButtons[1]); // Click the secondary link's dropdown + + const setPrimaryOption = await within( + getCanvasElementForDropdownTesting(), + ).findByText('Set as Primary'); + await userEvent.click(setPrimaryOption); + + // Documentation should now be the primary link + await waitFor(() => { + expect(updateRecord).toHaveBeenCalledWith({ + variables: { + where: { id: '123' }, + updateOneRecordInput: { + links: { + primaryLinkUrl: 'https://docs.twenty.com', + primaryLinkLabel: 'Documentation', + secondaryLinks: [ + { + url: 'https://www.twenty.com', + label: 'Twenty Website', + }, + ], + }, + }, + }, + }); + }); + expect(updateRecord).toHaveBeenCalledTimes(1); + }, +}; + +export const CanNotSetPrimaryLinkAsPrimaryLink: Story = { + args: { + value: { + primaryLinkUrl: 'https://www.twenty.com', + primaryLinkLabel: 'Twenty Website', + secondaryLinks: [], + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const primaryLink = await canvas.findByText('Twenty Website'); + expect(primaryLink).toBeVisible(); + + expect(getPrimaryLinkBookmarkIcon(canvasElement)).not.toBeInTheDocument(); + + const openDropdownButton = await canvas.findByRole('button', { + expanded: false, + }); + await userEvent.click(openDropdownButton); + + // Should not see "Set as Primary" option for primary link + const setPrimaryOption = within( + getCanvasElementForDropdownTesting(), + ).queryByText('Set as Primary'); + expect(setPrimaryOption).not.toBeInTheDocument(); + }, +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/PhonesFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/PhonesFieldInput.stories.tsx new file mode 100644 index 000000000..60786a05a --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/PhonesFieldInput.stories.tsx @@ -0,0 +1,192 @@ +import { expect } from '@storybook/jest'; +import { Meta, StoryObj } from '@storybook/react'; +import { fn, userEvent, waitFor, within } from '@storybook/test'; +import { useEffect } from 'react'; +import { getCanvasElementForDropdownTesting } from 'twenty-ui/testing'; + +import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; +import { usePhonesField } from '@/object-record/record-field/meta-types/hooks/usePhonesField'; +import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; +import { FieldInputClickOutsideEvent } from '@/object-record/record-field/types/FieldInputEvent'; +import { FieldPhonesValue } from '@/object-record/record-field/types/FieldMetadata'; +import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2'; +import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId'; +import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; +import { PhonesFieldInput } from '../PhonesFieldInput'; + +const updateRecord = fn(); + +const PhoneValueSetterEffect = ({ value }: { value: FieldPhonesValue }) => { + const { setFieldValue } = usePhonesField(); + + useEffect(() => { + setFieldValue(value); + }, [setFieldValue, value]); + + return null; +}; + +type PhoneFieldValueGaterProps = Pick< + PhoneInputWithContextProps, + 'onCancel' | 'onClickOutside' +>; + +const PhoneFieldValueGater = ({ + onCancel, + onClickOutside, +}: PhoneFieldValueGaterProps) => { + const { fieldValue } = usePhonesField(); + + return ( + fieldValue && ( + + ) + ); +}; + +type PhoneInputWithContextProps = { + value: FieldPhonesValue; + recordId?: string; + onCancel?: () => void; + onClickOutside?: FieldInputClickOutsideEvent; +}; + +const PhoneInputWithContext = ({ + value, + recordId = 'record-id', + onCancel, + onClickOutside, +}: PhoneInputWithContextProps) => { + const setHotkeyScope = useSetHotkeyScope(); + + useEffect(() => { + setHotkeyScope(DEFAULT_CELL_SCOPE.scope); + }, [setHotkeyScope]); + + return ( + + [updateRecord, { loading: false }], + }} + > + + + + + ); +}; + +const meta: Meta = { + title: 'UI/Input/PhonesFieldInput', + component: PhoneInputWithContext, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + value: { + primaryPhoneCountryCode: 'FR', + primaryPhoneNumber: '642646272', + primaryPhoneCallingCode: '+33', + additionalPhones: [ + { + countryCode: 'FR', + number: '642646273', + callingCode: '+33', + }, + ], + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const addButton = await canvas.findByText('Add Phone'); + await userEvent.click(addButton); + + const input = await canvas.findByPlaceholderText('Phone'); + await userEvent.type(input, '+33642646274{enter}'); + + const newPhoneElement = await canvas.findByText('+33 6 42 64 62 74'); + expect(newPhoneElement).toBeVisible(); + }, +}; + +// FIXME: We will have to fix that behavior, we should only be able to set an additional phone as the primary phone +export const CanSetPrimaryLinkAsPrimaryLink: Story = { + args: { + value: { + primaryPhoneCountryCode: 'FR', + primaryPhoneNumber: '642646272', + primaryPhoneCallingCode: '+33', + additionalPhones: [], + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const primaryPhone = await canvas.findByText('+33 6 42 64 62 72'); + expect(primaryPhone).toBeVisible(); + + await userEvent.hover(primaryPhone); + + const openDropdownButtons = await canvas.findAllByRole('button', { + expanded: false, + }); + await userEvent.click(openDropdownButtons[0]); + + const setPrimaryOption = await within( + getCanvasElementForDropdownTesting(), + ).findByText('Set as Primary'); + + expect(setPrimaryOption).toBeVisible(); + + await userEvent.click(setPrimaryOption); + + // Verify the update was called with swapped phones + await waitFor(() => { + expect(updateRecord).toHaveBeenCalledWith({ + variables: { + where: { id: 'record-id' }, + updateOneRecordInput: { + phones: { + primaryPhoneCallingCode: '+33', + primaryPhoneCountryCode: 'FR', + primaryPhoneNumber: '642646272', + additionalPhones: [], + }, + }, + }, + }); + }); + expect(updateRecord).toHaveBeenCalledTimes(1); + }, +};