From f847e1270907c44a76427d39e83d494d829900ea Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Thu, 4 Jul 2024 19:05:33 +0200 Subject: [PATCH 1/3] Implement Settings Tabs (#6136) In this PR: - Renaming SettingsAccountsEmailBlocklist to SettingsAccountsEmailBlocklist as the blocklist is not tied to emails/messaging but is user level - Changing the UI settings UI by removing /emails/{id} page and adding tabs on /emails page image --- packages/twenty-front/src/App.tsx | 5 - ...tsx => SettingsAccountsBlocklistInput.tsx} | 6 +- ...x => SettingsAccountsBlocklistSection.tsx} | 10 +- ...tsx => SettingsAccountsBlocklistTable.tsx} | 10 +- ... => SettingsAccountsBlocklistTableRow.tsx} | 6 +- .../SettingsAccountsMessageChannelDetails.tsx | 116 ++++++++++++++++ ...ttingsAccountsMessageChannelsContainer.tsx | 71 ++++++++++ ...ettingsAccountsMessageChannelsListCard.tsx | 86 ------------ ...ettingsAccountsBlocklistInput.stories.tsx} | 11 +- ...ttingsAccountsBlocklistSection.stories.tsx | 16 +++ ...ettingsAccountsBlocklistTable.stories.tsx} | 11 +- ...ingsAccountsBlocklistTableRow.stories.tsx} | 10 +- ...AccountsEmailsBlocklistSection.stories.tsx | 17 --- ...sAccountsMessageChannelDetails.stories.tsx | 30 ++++ ...ccountMessageChannelsTabListComponentId.ts | 2 + .../settings/accounts/SettingsAccounts.tsx | 4 +- .../accounts/SettingsAccountsEmails.tsx | 10 +- .../SettingsAccountsEmailsInboxSettings.tsx | 128 ------------------ .../SettingsAccountsEmails.stories.tsx | 119 +++++++++++++++- ...ngsAccountsEmailsInboxSettings.stories.tsx | 79 ----------- 20 files changed, 389 insertions(+), 358 deletions(-) rename packages/twenty-front/src/modules/settings/accounts/components/{SettingsAccountsEmailsBlocklistInput.tsx => SettingsAccountsBlocklistInput.tsx} (94%) rename packages/twenty-front/src/modules/settings/accounts/components/{SettingsAccountsEmailsBlocklistSection.tsx => SettingsAccountsBlocklistSection.tsx} (81%) rename packages/twenty-front/src/modules/settings/accounts/components/{SettingsAccountsEmailsBlocklistTable.tsx => SettingsAccountsBlocklistTable.tsx} (79%) rename packages/twenty-front/src/modules/settings/accounts/components/{SettingsAccountsEmailsBlocklistTableRow.tsx => SettingsAccountsBlocklistTableRow.tsx} (85%) create mode 100644 packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsMessageChannelDetails.tsx create mode 100644 packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsMessageChannelsContainer.tsx delete mode 100644 packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsMessageChannelsListCard.tsx rename packages/twenty-front/src/modules/settings/accounts/components/__stories__/{SettingsAccountsEmailsBlocklistInput.stories.tsx => SettingsAccountsBlocklistInput.stories.tsx} (77%) create mode 100644 packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsBlocklistSection.stories.tsx rename packages/twenty-front/src/modules/settings/accounts/components/__stories__/{SettingsAccountsEmailsBlocklistTable.stories.tsx => SettingsAccountsBlocklistTable.stories.tsx} (81%) rename packages/twenty-front/src/modules/settings/accounts/components/__stories__/{SettingsAccountsEmailsBlocklistTableRow.stories.tsx => SettingsAccountsBlocklistTableRow.stories.tsx} (80%) delete mode 100644 packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsEmailsBlocklistSection.stories.tsx create mode 100644 packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsMessageChannelDetails.stories.tsx create mode 100644 packages/twenty-front/src/modules/settings/accounts/constants/SettingsAccountMessageChannelsTabListComponentId.ts delete mode 100644 packages/twenty-front/src/pages/settings/accounts/SettingsAccountsEmailsInboxSettings.tsx delete mode 100644 packages/twenty-front/src/pages/settings/accounts/__stories__/SettingsAccountsEmailsInboxSettings.stories.tsx diff --git a/packages/twenty-front/src/App.tsx b/packages/twenty-front/src/App.tsx index 956a6c59e3..fff38bef20 100644 --- a/packages/twenty-front/src/App.tsx +++ b/packages/twenty-front/src/App.tsx @@ -55,7 +55,6 @@ import { SettingsAccounts } from '~/pages/settings/accounts/SettingsAccounts'; import { SettingsAccountsCalendars } from '~/pages/settings/accounts/SettingsAccountsCalendars'; import { SettingsAccountsCalendarsSettings } from '~/pages/settings/accounts/SettingsAccountsCalendarsSettings'; import { SettingsAccountsEmails } from '~/pages/settings/accounts/SettingsAccountsEmails'; -import { SettingsAccountsEmailsInboxSettings } from '~/pages/settings/accounts/SettingsAccountsEmailsInboxSettings'; import { SettingsNewAccount } from '~/pages/settings/accounts/SettingsNewAccount'; import { SettingsNewObject } from '~/pages/settings/data-model/SettingsNewObject'; import { SettingsObjectDetail } from '~/pages/settings/data-model/SettingsObjectDetail'; @@ -188,10 +187,6 @@ const createRouter = (isBillingEnabled?: boolean) => path={SettingsPath.AccountsEmails} element={} /> - } - /> } diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsEmailsBlocklistInput.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistInput.tsx similarity index 94% rename from packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsEmailsBlocklistInput.tsx rename to packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistInput.tsx index 1794eb1d42..9bb4fdd98c 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsEmailsBlocklistInput.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistInput.tsx @@ -19,7 +19,7 @@ const StyledLinkContainer = styled.div` margin-right: ${({ theme }) => theme.spacing(2)}; `; -type SettingsAccountsEmailsBlocklistInputProps = { +type SettingsAccountsBlocklistInputProps = { updateBlockedEmailList: (email: string) => void; blockedEmailOrDomainList: string[]; }; @@ -50,10 +50,10 @@ type FormInput = { emailOrDomain: string; }; -export const SettingsAccountsEmailsBlocklistInput = ({ +export const SettingsAccountsBlocklistInput = ({ updateBlockedEmailList, blockedEmailOrDomainList, -}: SettingsAccountsEmailsBlocklistInputProps) => { +}: SettingsAccountsBlocklistInputProps) => { const { reset, handleSubmit, control, formState } = useForm({ mode: 'onSubmit', resolver: zodResolver(validationSchema(blockedEmailOrDomainList)), diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsEmailsBlocklistSection.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistSection.tsx similarity index 81% rename from packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsEmailsBlocklistSection.tsx rename to packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistSection.tsx index 39b789fae5..51ff642b1b 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsEmailsBlocklistSection.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistSection.tsx @@ -7,11 +7,11 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; -import { SettingsAccountsEmailsBlocklistInput } from '@/settings/accounts/components/SettingsAccountsEmailsBlocklistInput'; -import { SettingsAccountsEmailsBlocklistTable } from '@/settings/accounts/components/SettingsAccountsEmailsBlocklistTable'; +import { SettingsAccountsBlocklistInput } from '@/settings/accounts/components/SettingsAccountsBlocklistInput'; +import { SettingsAccountsBlocklistTable } from '@/settings/accounts/components/SettingsAccountsBlocklistTable'; import { Section } from '@/ui/layout/section/components/Section'; -export const SettingsAccountsEmailsBlocklistSection = () => { +export const SettingsAccountsBlocklistSection = () => { const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); const { records: blocklist } = useFindManyRecords({ @@ -44,11 +44,11 @@ export const SettingsAccountsEmailsBlocklistSection = () => { title="Blocklist" description="Exclude the following people and domains from my email sync" /> - item.handle)} updateBlockedEmailList={updateBlockedEmailList} /> - diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsEmailsBlocklistTable.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistTable.tsx similarity index 79% rename from packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsEmailsBlocklistTable.tsx rename to packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistTable.tsx index a238ef6e5e..a4c9f5306f 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsEmailsBlocklistTable.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistTable.tsx @@ -1,13 +1,13 @@ import styled from '@emotion/styled'; import { BlocklistItem } from '@/accounts/types/BlocklistItem'; -import { SettingsAccountsEmailsBlocklistTableRow } from '@/settings/accounts/components/SettingsAccountsEmailsBlocklistTableRow'; +import { SettingsAccountsBlocklistTableRow } from '@/settings/accounts/components/SettingsAccountsBlocklistTableRow'; import { Table } from '@/ui/layout/table/components/Table'; import { TableBody } from '@/ui/layout/table/components/TableBody'; import { TableHeader } from '@/ui/layout/table/components/TableHeader'; import { TableRow } from '@/ui/layout/table/components/TableRow'; -type SettingsAccountsEmailsBlocklistTableProps = { +type SettingsAccountsBlocklistTableProps = { blocklist: BlocklistItem[]; handleBlockedEmailRemove: (id: string) => void; }; @@ -20,10 +20,10 @@ const StyledTableBody = styled(TableBody)` border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; `; -export const SettingsAccountsEmailsBlocklistTable = ({ +export const SettingsAccountsBlocklistTable = ({ blocklist, handleBlockedEmailRemove, -}: SettingsAccountsEmailsBlocklistTableProps) => { +}: SettingsAccountsBlocklistTableProps) => { return ( <> {blocklist.length > 0 && ( @@ -35,7 +35,7 @@ export const SettingsAccountsEmailsBlocklistTable = ({ {blocklist.map((blocklistItem) => ( - void; }; -export const SettingsAccountsEmailsBlocklistTableRow = ({ +export const SettingsAccountsBlocklistTableRow = ({ blocklistItem, onRemove, -}: SettingsAccountsEmailsBlocklistTableRowProps) => { +}: SettingsAccountsBlocklistTableRowProps) => { return ( {blocklistItem.handle} diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsMessageChannelDetails.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsMessageChannelDetails.tsx new file mode 100644 index 0000000000..ad3b912db1 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsMessageChannelDetails.tsx @@ -0,0 +1,116 @@ +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import { H2Title, IconRefresh, IconUser } from 'twenty-ui'; + +import { MessageChannel } from '@/accounts/types/MessageChannel'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; +import { SettingsAccountsCardMedia } from '@/settings/accounts/components/SettingsAccountsCardMedia'; +import { SettingsAccountsInboxVisibilitySettingsCard } from '@/settings/accounts/components/SettingsAccountsInboxVisibilitySettingsCard'; +import { SettingsAccountsToggleSettingCard } from '@/settings/accounts/components/SettingsAccountsToggleSettingCard'; +import { Section } from '@/ui/layout/section/components/Section'; +import { MessageChannelVisibility } from '~/generated-metadata/graphql'; + +type SettingsAccountsMessageChannelDetailsProps = { + messageChannel: Pick< + MessageChannel, + 'id' | 'visibility' | 'isContactAutoCreationEnabled' | 'isSyncEnabled' + >; +}; + +const StyledDetailsContainer = styled.div` + display: flex; + flex-direction: column; + gap: ${({ theme }) => theme.spacing(6)}; + padding-top: ${({ theme }) => theme.spacing(6)}; +`; + +export const SettingsAccountsMessageChannelDetails = ({ + messageChannel, +}: SettingsAccountsMessageChannelDetailsProps) => { + const theme = useTheme(); + + const { updateOneRecord } = useUpdateOneRecord({ + objectNameSingular: CoreObjectNameSingular.MessageChannel, + }); + + const handleVisibilityChange = (value: MessageChannelVisibility) => { + updateOneRecord({ + idToUpdate: messageChannel.id, + updateOneRecordInput: { + visibility: value, + }, + }); + }; + + const handleContactAutoCreationToggle = (value: boolean) => { + updateOneRecord({ + idToUpdate: messageChannel.id, + updateOneRecordInput: { + isContactAutoCreationEnabled: value, + }, + }); + }; + + const handleIsSyncEnabledToggle = (value: boolean) => { + updateOneRecord({ + idToUpdate: messageChannel.id, + updateOneRecordInput: { + isSyncEnabled: value, + }, + }); + }; + + return ( + +
+ + +
+
+ + + + + } + title="Auto-creation" + value={!!messageChannel.isContactAutoCreationEnabled} + onToggle={handleContactAutoCreationToggle} + /> +
+
+ + + + + } + title="Sync emails" + value={!!messageChannel.isSyncEnabled} + onToggle={handleIsSyncEnabledToggle} + /> +
+
+ ); +}; diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsMessageChannelsContainer.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsMessageChannelsContainer.tsx new file mode 100644 index 0000000000..95f462e067 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsMessageChannelsContainer.tsx @@ -0,0 +1,71 @@ +import { useRecoilValue } from 'recoil'; + +import { ConnectedAccount } from '@/accounts/types/ConnectedAccount'; +import { MessageChannel } from '@/accounts/types/MessageChannel'; +import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; +import { SettingsAccountsListEmptyStateCard } from '@/settings/accounts/components/SettingsAccountsListEmptyStateCard'; +import { SettingsAccountsMessageChannelDetails } from '@/settings/accounts/components/SettingsAccountsMessageChannelDetails'; +import { SETTINGS_ACCOUNT_MESSAGE_CHANNELS_TAB_LIST_COMPONENT_ID } from '@/settings/accounts/constants/SettingsAccountMessageChannelsTabListComponentId'; +import { TabList } from '@/ui/layout/tab/components/TabList'; +import { useTabList } from '@/ui/layout/tab/hooks/useTabList'; + +export const SettingsAccountsMessageChannelsContainer = () => { + const { activeTabIdState } = useTabList( + SETTINGS_ACCOUNT_MESSAGE_CHANNELS_TAB_LIST_COMPONENT_ID, + ); + const activeTabId = useRecoilValue(activeTabIdState); + const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); + + const { records: accounts } = useFindManyRecords({ + objectNameSingular: CoreObjectNameSingular.ConnectedAccount, + filter: { + accountOwnerId: { + eq: currentWorkspaceMember?.id, + }, + }, + }); + + const { records: messageChannels } = useFindManyRecords< + MessageChannel & { + connectedAccount: ConnectedAccount; + } + >({ + objectNameSingular: CoreObjectNameSingular.MessageChannel, + filter: { + connectedAccountId: { + in: accounts.map((account) => account.id), + }, + }, + }); + + const tabs = [ + ...messageChannels.map((messageChannel) => ({ + id: messageChannel.id, + title: messageChannel.handle, + })), + ]; + + if (!messageChannels.length) { + return ; + } + + return ( + <> + + {messageChannels.map((messageChannel) => ( + <> + {messageChannel.id === activeTabId && ( + + )} + + ))} + + ); +}; diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsMessageChannelsListCard.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsMessageChannelsListCard.tsx deleted file mode 100644 index addc376b4d..0000000000 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsMessageChannelsListCard.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { useNavigate } from 'react-router-dom'; -import styled from '@emotion/styled'; -import { useRecoilValue } from 'recoil'; -import { IconChevronRight, IconGmail } from 'twenty-ui'; - -import { ConnectedAccount } from '@/accounts/types/ConnectedAccount'; -import { MessageChannel } from '@/accounts/types/MessageChannel'; -import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; -import { SettingsAccountsListEmptyStateCard } from '@/settings/accounts/components/SettingsAccountsListEmptyStateCard'; -import { - SettingsAccountsSynchronizationStatus, - SettingsAccountsSynchronizationStatusProps, -} from '@/settings/accounts/components/SettingsAccountsSynchronizationStatus'; -import { SettingsListCard } from '@/settings/components/SettingsListCard'; -import { LightIconButton } from '@/ui/input/button/components/LightIconButton'; - -const StyledRowRightContainer = styled.div` - align-items: center; - display: flex; - gap: ${({ theme }) => theme.spacing(1)}; -`; - -export const SettingsAccountsMessageChannelsListCard = () => { - const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); - const navigate = useNavigate(); - - const { records: accounts, loading: accountsLoading } = - useFindManyRecords({ - objectNameSingular: CoreObjectNameSingular.ConnectedAccount, - filter: { - accountOwnerId: { - eq: currentWorkspaceMember?.id, - }, - }, - }); - - const { records: messageChannels, loading: messageChannelsLoading } = - useFindManyRecords< - MessageChannel & { - connectedAccount: ConnectedAccount; - } - >({ - objectNameSingular: CoreObjectNameSingular.MessageChannel, - filter: { - connectedAccountId: { - in: accounts.map((account) => account.id), - }, - }, - }); - - const messageChannelsWithSyncedEmails: (MessageChannel & { - connectedAccount: ConnectedAccount; - } & SettingsAccountsSynchronizationStatusProps)[] = messageChannels.map( - (messageChannel) => ({ - ...messageChannel, - syncStatus: messageChannel.syncStatus, - }), - ); - - if (!messageChannelsWithSyncedEmails.length) { - return ; - } - - return ( - messageChannel.handle} - isLoading={accountsLoading || messageChannelsLoading} - onRowClick={(messageChannel) => - navigate(`/settings/accounts/emails/${messageChannel.id}`) - } - RowIcon={IconGmail} - RowRightComponent={({ item: messageChannel }) => ( - - - - - )} - /> - ); -}; diff --git a/packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsEmailsBlocklistInput.stories.tsx b/packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsBlocklistInput.stories.tsx similarity index 77% rename from packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsEmailsBlocklistInput.stories.tsx rename to packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsBlocklistInput.stories.tsx index 3ceef7405d..48a52896b5 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsEmailsBlocklistInput.stories.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsBlocklistInput.stories.tsx @@ -2,7 +2,7 @@ import { Decorator, Meta, StoryObj } from '@storybook/react'; import { expect, fn, userEvent, within } from '@storybook/test'; import { ComponentDecorator } from 'twenty-ui'; -import { SettingsAccountsEmailsBlocklistInput } from '@/settings/accounts/components/SettingsAccountsEmailsBlocklistInput'; +import { SettingsAccountsBlocklistInput } from '@/settings/accounts/components/SettingsAccountsBlocklistInput'; const updateBlockedEmailListJestFn = fn(); @@ -13,10 +13,9 @@ const ClearMocksDecorator: Decorator = (Story, context) => { return ; }; -const meta: Meta = { - title: - 'Modules/Settings/Accounts/Blocklist/SettingsAccountsEmailsBlocklistInput', - component: SettingsAccountsEmailsBlocklistInput, +const meta: Meta = { + title: 'Modules/Settings/Accounts/Blocklist/SettingsAccountsBlocklistInput', + component: SettingsAccountsBlocklistInput, decorators: [ComponentDecorator, ClearMocksDecorator], args: { updateBlockedEmailList: updateBlockedEmailListJestFn, @@ -31,7 +30,7 @@ const meta: Meta = { }; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = {}; diff --git a/packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsBlocklistSection.stories.tsx b/packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsBlocklistSection.stories.tsx new file mode 100644 index 0000000000..bc3f9cb721 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsBlocklistSection.stories.tsx @@ -0,0 +1,16 @@ +import { Meta, StoryObj } from '@storybook/react'; +import { ComponentDecorator } from 'twenty-ui'; + +import { SettingsAccountsBlocklistInput } from '@/settings/accounts/components/SettingsAccountsBlocklistInput'; +import { SettingsAccountsBlocklistSection } from '@/settings/accounts/components/SettingsAccountsBlocklistSection'; + +const meta: Meta = { + title: 'Modules/Settings/Accounts/Blocklist/SettingsAccountsBlocklistSection', + component: SettingsAccountsBlocklistInput, + decorators: [ComponentDecorator], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsEmailsBlocklistTable.stories.tsx b/packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsBlocklistTable.stories.tsx similarity index 81% rename from packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsEmailsBlocklistTable.stories.tsx rename to packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsBlocklistTable.stories.tsx index 7ab7f1c9b0..71126ba4e2 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsEmailsBlocklistTable.stories.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsBlocklistTable.stories.tsx @@ -3,7 +3,7 @@ import { expect, fn, userEvent, within } from '@storybook/test'; import { ComponentDecorator } from 'twenty-ui'; import { mockedBlocklist } from '@/settings/accounts/components/__stories__/mockedBlocklist'; -import { SettingsAccountsEmailsBlocklistTable } from '@/settings/accounts/components/SettingsAccountsEmailsBlocklistTable'; +import { SettingsAccountsBlocklistTable } from '@/settings/accounts/components/SettingsAccountsBlocklistTable'; import { formatToHumanReadableDate } from '~/utils/date-utils'; const handleBlockedEmailRemoveJestFn = fn(); @@ -15,10 +15,9 @@ const ClearMocksDecorator: Decorator = (Story, context) => { return ; }; -const meta: Meta = { - title: - 'Modules/Settings/Accounts/Blocklist/SettingsAccountsEmailsBlocklistTable', - component: SettingsAccountsEmailsBlocklistTable, +const meta: Meta = { + title: 'Modules/Settings/Accounts/Blocklist/SettingsAccountsBlocklistTable', + component: SettingsAccountsBlocklistTable, decorators: [ComponentDecorator, ClearMocksDecorator], args: { blocklist: mockedBlocklist, @@ -34,7 +33,7 @@ const meta: Meta = { }; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = { play: async ({ canvasElement }) => { diff --git a/packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsEmailsBlocklistTableRow.stories.tsx b/packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsBlocklistTableRow.stories.tsx similarity index 80% rename from packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsEmailsBlocklistTableRow.stories.tsx rename to packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsBlocklistTableRow.stories.tsx index f24b0033f5..b4a1991e65 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsEmailsBlocklistTableRow.stories.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsBlocklistTableRow.stories.tsx @@ -3,7 +3,7 @@ import { expect, fn, userEvent, within } from '@storybook/test'; import { ComponentDecorator } from 'twenty-ui'; import { mockedBlocklist } from '@/settings/accounts/components/__stories__/mockedBlocklist'; -import { SettingsAccountsEmailsBlocklistTableRow } from '@/settings/accounts/components/SettingsAccountsEmailsBlocklistTableRow'; +import { SettingsAccountsBlocklistTableRow } from '@/settings/accounts/components/SettingsAccountsBlocklistTableRow'; import { formatToHumanReadableDate } from '~/utils/date-utils'; const onRemoveJestFn = fn(); @@ -15,10 +15,10 @@ const ClearMocksDecorator: Decorator = (Story, context) => { return ; }; -const meta: Meta = { +const meta: Meta = { title: - 'Modules/Settings/Accounts/Blocklist/SettingsAccountsEmailsBlocklistTableRow', - component: SettingsAccountsEmailsBlocklistTableRow, + 'Modules/Settings/Accounts/Blocklist/SettingsAccountsBlocklistTableRow', + component: SettingsAccountsBlocklistTableRow, decorators: [ComponentDecorator, ClearMocksDecorator], args: { blocklistItem: mockedBlocklist[0], @@ -34,7 +34,7 @@ const meta: Meta = { }; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = { play: async ({ canvasElement }) => { diff --git a/packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsEmailsBlocklistSection.stories.tsx b/packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsEmailsBlocklistSection.stories.tsx deleted file mode 100644 index 7fa2183aae..0000000000 --- a/packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsEmailsBlocklistSection.stories.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Meta, StoryObj } from '@storybook/react'; -import { ComponentDecorator } from 'twenty-ui'; - -import { SettingsAccountsEmailsBlocklistInput } from '@/settings/accounts/components/SettingsAccountsEmailsBlocklistInput'; -import { SettingsAccountsEmailsBlocklistSection } from '@/settings/accounts/components/SettingsAccountsEmailsBlocklistSection'; - -const meta: Meta = { - title: - 'Modules/Settings/Accounts/Blocklist/SettingsAccountsEmailsBlocklistSection', - component: SettingsAccountsEmailsBlocklistInput, - decorators: [ComponentDecorator], -}; - -export default meta; -type Story = StoryObj; - -export const Default: Story = {}; diff --git a/packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsMessageChannelDetails.stories.tsx b/packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsMessageChannelDetails.stories.tsx new file mode 100644 index 0000000000..e1472836e9 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsMessageChannelDetails.stories.tsx @@ -0,0 +1,30 @@ +import { Meta, StoryObj } from '@storybook/react'; +import { ComponentDecorator } from 'twenty-ui'; + +import { SettingsAccountsMessageChannelDetails } from '@/settings/accounts/components/SettingsAccountsMessageChannelDetails'; +import { MessageChannelVisibility } from '~/generated/graphql'; + +const meta: Meta = { + title: + 'Modules/Settings/Accounts/MessageChannels/SettingsAccountsMessageChannelDetails', + component: SettingsAccountsMessageChannelDetails, + decorators: [ComponentDecorator], + args: { + messageChannel: { + id: '20202020-ef5a-4822-9e08-ce6e6a4dcb6a', + isContactAutoCreationEnabled: true, + isSyncEnabled: true, + visibility: MessageChannelVisibility.ShareEverything, + }, + }, + argTypes: { + messageChannel: { control: false }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + play: async () => {}, +}; diff --git a/packages/twenty-front/src/modules/settings/accounts/constants/SettingsAccountMessageChannelsTabListComponentId.ts b/packages/twenty-front/src/modules/settings/accounts/constants/SettingsAccountMessageChannelsTabListComponentId.ts new file mode 100644 index 0000000000..31f4638d76 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/accounts/constants/SettingsAccountMessageChannelsTabListComponentId.ts @@ -0,0 +1,2 @@ +export const SETTINGS_ACCOUNT_MESSAGE_CHANNELS_TAB_LIST_COMPONENT_ID = + 'settings-account-message-channels-tab-list'; diff --git a/packages/twenty-front/src/pages/settings/accounts/SettingsAccounts.tsx b/packages/twenty-front/src/pages/settings/accounts/SettingsAccounts.tsx index 484a0a6817..0a46dfbe95 100644 --- a/packages/twenty-front/src/pages/settings/accounts/SettingsAccounts.tsx +++ b/packages/twenty-front/src/pages/settings/accounts/SettingsAccounts.tsx @@ -8,8 +8,8 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { SettingsAccountLoader } from '@/settings/accounts/components/SettingsAccountLoader'; +import { SettingsAccountsBlocklistSection } from '@/settings/accounts/components/SettingsAccountsBlocklistSection'; import { SettingsAccountsConnectedAccountsListCard } from '@/settings/accounts/components/SettingsAccountsConnectedAccountsListCard'; -import { SettingsAccountsEmailsBlocklistSection } from '@/settings/accounts/components/SettingsAccountsEmailsBlocklistSection'; import { SettingsAccountsSettingsSection } from '@/settings/accounts/components/SettingsAccountsSettingsSection'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; @@ -55,7 +55,7 @@ export const SettingsAccounts = () => { loading={loading} /> - {isBlocklistEnabled && } + {isBlocklistEnabled && } )} diff --git a/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsEmails.tsx b/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsEmails.tsx index d2827187f7..b972ad215e 100644 --- a/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsEmails.tsx +++ b/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsEmails.tsx @@ -1,6 +1,6 @@ -import { H2Title, IconSettings } from 'twenty-ui'; +import { IconSettings } from 'twenty-ui'; -import { SettingsAccountsMessageChannelsListCard } from '@/settings/accounts/components/SettingsAccountsMessageChannelsListCard'; +import { SettingsAccountsMessageChannelsContainer } from '@/settings/accounts/components/SettingsAccountsMessageChannelsContainer'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; import { Section } from '@/ui/layout/section/components/Section'; @@ -16,11 +16,7 @@ export const SettingsAccountsEmails = () => ( ]} />
- - +
diff --git a/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsEmailsInboxSettings.tsx b/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsEmailsInboxSettings.tsx deleted file mode 100644 index afef0c498d..0000000000 --- a/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsEmailsInboxSettings.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import { useEffect } from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; -import { useTheme } from '@emotion/react'; -import { H2Title, IconRefresh, IconSettings, IconUser } from 'twenty-ui'; - -import { MessageChannel } from '@/accounts/types/MessageChannel'; -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; -import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; -import { SettingsAccountsCardMedia } from '@/settings/accounts/components/SettingsAccountsCardMedia'; -import { SettingsAccountsInboxVisibilitySettingsCard } from '@/settings/accounts/components/SettingsAccountsInboxVisibilitySettingsCard'; -import { SettingsAccountsToggleSettingCard } from '@/settings/accounts/components/SettingsAccountsToggleSettingCard'; -import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; -import { AppPath } from '@/types/AppPath'; -import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; -import { Section } from '@/ui/layout/section/components/Section'; -import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb'; -import { MessageChannelVisibility } from '~/generated/graphql'; - -export const SettingsAccountsEmailsInboxSettings = () => { - const theme = useTheme(); - const navigate = useNavigate(); - const { accountUuid: messageChannelId = '' } = useParams(); - - const { record: messageChannel, loading } = useFindOneRecord({ - objectNameSingular: CoreObjectNameSingular.MessageChannel, - objectRecordId: messageChannelId, - }); - - const { updateOneRecord } = useUpdateOneRecord({ - objectNameSingular: CoreObjectNameSingular.MessageChannel, - }); - - const handleVisibilityChange = (value: MessageChannelVisibility) => { - updateOneRecord({ - idToUpdate: messageChannelId, - updateOneRecordInput: { - visibility: value, - }, - }); - }; - - const handleContactAutoCreationToggle = (value: boolean) => { - updateOneRecord({ - idToUpdate: messageChannelId, - updateOneRecordInput: { - isContactAutoCreationEnabled: value, - }, - }); - }; - - const handleIsSyncEnabledToggle = (value: boolean) => { - updateOneRecord({ - idToUpdate: messageChannelId, - updateOneRecordInput: { - isSyncEnabled: value, - }, - }); - }; - - useEffect(() => { - if (!loading && !messageChannel) navigate(AppPath.NotFound); - }, [loading, messageChannel, navigate]); - - if (!messageChannel) return null; - - return ( - - - -
- - -
-
- - - - - } - title="Auto-creation" - value={!!messageChannel.isContactAutoCreationEnabled} - onToggle={handleContactAutoCreationToggle} - /> -
-
- - - - - } - title="Sync emails" - value={!!messageChannel.isSyncEnabled} - onToggle={handleIsSyncEnabledToggle} - /> -
-
-
- ); -}; diff --git a/packages/twenty-front/src/pages/settings/accounts/__stories__/SettingsAccountsEmails.stories.tsx b/packages/twenty-front/src/pages/settings/accounts/__stories__/SettingsAccountsEmails.stories.tsx index 21bede0eb5..bde4ce31dc 100644 --- a/packages/twenty-front/src/pages/settings/accounts/__stories__/SettingsAccountsEmails.stories.tsx +++ b/packages/twenty-front/src/pages/settings/accounts/__stories__/SettingsAccountsEmails.stories.tsx @@ -1,4 +1,5 @@ import { Meta, StoryObj } from '@storybook/react'; +import { graphql, HttpResponse } from 'msw'; import { PageDecorator, @@ -25,4 +26,120 @@ export default meta; export type Story = StoryObj; -export const Default: Story = {}; +export const NoConnectedAccount: Story = {}; + +export const TwoConnectedAccounts: Story = { + parameters: { + msw: { + handlers: [ + ...graphqlMocks.handlers, + graphql.query('FindManyConnectedAccounts', () => { + return HttpResponse.json({ + data: { + connectedAccounts: { + __typename: 'ConnectedAccountConnection', + totalCount: 1, + pageInfo: { + __typename: 'PageInfo', + hasNextPage: false, + startCursor: '', + endCursor: '', + }, + edges: [ + { + __typename: 'ConnectedAccountEdge', + cursor: '', + node: { + __typename: 'ConnectedAccount', + accessToken: '', + refreshToken: '', + updatedAt: '2024-07-03T20:03:35.064Z', + createdAt: '2024-07-03T20:03:35.064Z', + id: '20202020-954c-4d76-9a87-e5f072d4b7ef', + provider: 'google', + accountOwnerId: '20202020-03f2-4d83-b0d5-2ec2bcee72d4', + lastSyncHistoryId: '', + emailAliases: '', + handle: 'test.test@gmail.com', + authFailedAt: null, + }, + }, + ], + }, + }, + }); + }), + graphql.query('FindManyMessageChannels', () => { + return HttpResponse.json({ + data: { + messageChannels: { + __typename: 'MessageChannelConnection', + totalCount: 2, + pageInfo: { + __typename: 'PageInfo', + hasNextPage: false, + startCursor: '', + endCursor: '', + }, + edges: [ + { + __typename: 'MessageChannelEdge', + cursor: '', + node: { + __typename: 'MessageChannel', + handle: 'test.test@gmail.com', + excludeNonProfessionalEmails: true, + syncStageStartedAt: null, + id: '20202020-ef5a-4822-9e08-ce6e6a4dcb6f', + updatedAt: '2024-07-03T20:03:11.903Z', + createdAt: '2024-07-03T20:03:11.903Z', + connectedAccountId: + '20202020-954c-4d76-9a87-e5f072d4b7ef', + contactAutoCreationPolicy: 'SENT', + syncStage: 'PARTIAL_MESSAGE_LIST_FETCH_PENDING', + type: 'email', + isContactAutoCreationEnabled: true, + syncCursor: '1562764', + excludeGroupEmails: true, + throttleFailureCount: 0, + isSyncEnabled: true, + visibility: 'SHARE_EVERYTHING', + syncStatus: 'COMPLETED', + syncedAt: '2024-07-04T16:25:04.960Z', + }, + }, + { + __typename: 'MessageChannelEdge', + cursor: '', + node: { + __typename: 'MessageChannel', + handle: 'test.test2@gmail.com', + excludeNonProfessionalEmails: true, + syncStageStartedAt: null, + id: '20202020-ef5a-4822-9e08-ce6e6a4dcb6a', + updatedAt: '2024-07-03T20:03:11.903Z', + createdAt: '2024-07-03T20:03:11.903Z', + connectedAccountId: + '20202020-954c-4d76-9a87-e5f072d4b7ef', + contactAutoCreationPolicy: 'SENT', + syncStage: 'PARTIAL_MESSAGE_LIST_FETCH_PENDING', + type: 'email', + isContactAutoCreationEnabled: true, + syncCursor: '1562764', + excludeGroupEmails: true, + throttleFailureCount: 0, + isSyncEnabled: true, + visibility: 'SHARE_EVERYTHING', + syncStatus: 'COMPLETED', + syncedAt: '2024-07-04T16:25:04.960Z', + }, + }, + ], + }, + }, + }); + }), + ], + }, + }, +}; diff --git a/packages/twenty-front/src/pages/settings/accounts/__stories__/SettingsAccountsEmailsInboxSettings.stories.tsx b/packages/twenty-front/src/pages/settings/accounts/__stories__/SettingsAccountsEmailsInboxSettings.stories.tsx deleted file mode 100644 index 6c23dd71c0..0000000000 --- a/packages/twenty-front/src/pages/settings/accounts/__stories__/SettingsAccountsEmailsInboxSettings.stories.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { Meta, StoryObj } from '@storybook/react'; -import { within } from '@storybook/test'; -import { graphql, HttpResponse } from 'msw'; - -import { MessageChannelVisibility } from '~/generated/graphql'; -import { SettingsAccountsEmailsInboxSettings } from '~/pages/settings/accounts/SettingsAccountsEmailsInboxSettings'; -import { - PageDecorator, - PageDecoratorArgs, -} from '~/testing/decorators/PageDecorator'; -import { graphqlMocks } from '~/testing/graphqlMocks'; - -const meta: Meta = { - title: 'Pages/Settings/Accounts/SettingsAccountsEmailsInboxSettings', - component: SettingsAccountsEmailsInboxSettings, - decorators: [PageDecorator], - args: { - routePath: '/settings/accounts/emails/:accountUuid', - routeParams: { ':accountUuid': '123' }, - }, - parameters: { - layout: 'fullscreen', - msw: { - handlers: [ - graphql.query('FindOneMessageChannel', () => { - return HttpResponse.json({ - data: { - messageChannel: { - id: '1', - visibility: MessageChannelVisibility.ShareEverything, - messageThreads: { edges: [] }, - createdAt: '2021-08-27T12:00:00Z', - type: 'email', - updatedAt: '2021-08-27T12:00:00Z', - targetUrl: 'https://example.com/webhook', - connectedAccountId: '1', - handle: 'handle', - connectedAccount: { - id: '1', - handle: 'handle', - updatedAt: '2021-08-27T12:00:00Z', - accessToken: 'accessToken', - messageChannels: { edges: [] }, - refreshToken: 'refreshToken', - __typename: 'ConnectedAccount', - accountOwner: { id: '1', __typename: 'WorkspaceMember' }, - provider: 'provider', - createdAt: '2021-08-27T12:00:00Z', - accountOwnerId: '1', - }, - __typename: 'MessageChannel', - }, - }, - }); - }), - graphqlMocks.handlers, - ], - }, - }, -}; - -export default meta; - -export type Story = StoryObj; - -export const Default: Story = { - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - await canvas.findByText('Email visibility'); - await canvas.findByText( - 'Define what will be visible to other users in your workspace', - ); - await canvas.findByText('Contact auto-creation'); - await canvas.findByText( - 'Automatically create contacts for people you’ve sent emails to', - ); - }, -}; From b33b468679b964d02c25960455602e5986d442ed Mon Sep 17 00:00:00 2001 From: Marie <51697796+ijreilly@users.noreply.github.com> Date: Fri, 5 Jul 2024 12:27:12 +0200 Subject: [PATCH 2/3] Add command to update boolean fields null values (#6113) We have recently decided that boolean fields should only accept truthy or falsy value, with users deciding of a default value at creation. This command helps cleaning the existing data, by 1. updating all boolean fields default values from null to false 2. updating all boolean fields values for records from null to false --------- Co-authored-by: Weiko --- ...-default-values-and-null-values.command.ts | 159 ++++++++++++++++++ .../commands/database-command.module.ts | 2 + .../calendar-event.workspace-entity.ts | 2 + 3 files changed, 163 insertions(+) create mode 100644 packages/twenty-server/src/database/commands/0-22-update-boolean-fields-null-default-values-and-null-values.command.ts diff --git a/packages/twenty-server/src/database/commands/0-22-update-boolean-fields-null-default-values-and-null-values.command.ts b/packages/twenty-server/src/database/commands/0-22-update-boolean-fields-null-default-values-and-null-values.command.ts new file mode 100644 index 0000000000..89b18dde97 --- /dev/null +++ b/packages/twenty-server/src/database/commands/0-22-update-boolean-fields-null-default-values-and-null-values.command.ts @@ -0,0 +1,159 @@ +import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; +import { Logger } from '@nestjs/common'; + +import { Command, CommandRunner, Option } from 'nest-commander'; +import { Repository, IsNull, DataSource } from 'typeorm'; +import chalk from 'chalk'; + +import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { TypeORMService } from 'src/database/typeorm/typeorm.service'; +import { + FieldMetadataEntity, + FieldMetadataType, +} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { WorkspaceCacheVersionService } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.service'; +import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; + +interface UpdateBooleanFieldsNullDefaultValuesAndNullValuesCommandOptions { + workspaceId?: string; +} + +@Command({ + name: 'migrate-0.22:update-boolean-field-null-default-values-and-null-values', + description: + 'Update boolean fields null default values and null values to false', +}) +export class UpdateBooleanFieldsNullDefaultValuesAndNullValuesCommand extends CommandRunner { + private readonly logger = new Logger( + UpdateBooleanFieldsNullDefaultValuesAndNullValuesCommand.name, + ); + constructor( + @InjectRepository(Workspace, 'core') + private readonly workspaceRepository: Repository, + private readonly typeORMService: TypeORMService, + private readonly dataSourceService: DataSourceService, + private readonly workspaceCacheVersionService: WorkspaceCacheVersionService, + @InjectDataSource('metadata') + private readonly metadataDataSource: DataSource, + ) { + super(); + } + + @Option({ + flags: '-w, --workspace-id [workspace_id]', + description: 'workspace id. Command runs on all workspaces if not provided', + required: false, + }) + parseWorkspaceId(value: string): string { + return value; + } + + async run( + _passedParam: string[], + options: UpdateBooleanFieldsNullDefaultValuesAndNullValuesCommandOptions, + ): Promise { + const workspaceIds = options.workspaceId + ? [options.workspaceId] + : (await this.workspaceRepository.find()).map( + (workspace) => workspace.id, + ); + + if (!workspaceIds.length) { + this.logger.log(chalk.yellow('No workspace found')); + + return; + } + + this.logger.log( + chalk.green(`Running command on ${workspaceIds.length} workspaces`), + ); + + for (const workspaceId of workspaceIds) { + const dataSourceMetadata = + await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceId( + workspaceId, + ); + + if (!dataSourceMetadata) { + throw new Error( + `Could not find dataSourceMetadata for workspace ${workspaceId}`, + ); + } + + const workspaceDataSource = + await this.typeORMService.connectToDataSource(dataSourceMetadata); + + if (!workspaceDataSource) { + throw new Error( + `Could not connect to dataSource for workspace ${workspaceId}`, + ); + } + + const workspaceQueryRunner = workspaceDataSource.createQueryRunner(); + const metadataQueryRunner = this.metadataDataSource.createQueryRunner(); + + await workspaceQueryRunner.connect(); + await metadataQueryRunner.connect(); + + await workspaceQueryRunner.startTransaction(); + await metadataQueryRunner.startTransaction(); + + try { + const fieldMetadataRepository = + metadataQueryRunner.manager.getRepository(FieldMetadataEntity); + + const booleanFieldsWithoutDefaultValue = + await fieldMetadataRepository.find({ + where: { + workspaceId, + type: FieldMetadataType.BOOLEAN, + defaultValue: IsNull(), + }, + relations: ['object'], + }); + + for (const booleanField of booleanFieldsWithoutDefaultValue) { + if (!booleanField.object) { + throw new Error( + `Could not find objectMetadataItem for field ${booleanField.id}`, + ); + } + + // Could be done via a batch update but it's safer in this context to run it sequentially with the ALTER TABLE + await fieldMetadataRepository.update(booleanField.id, { + defaultValue: false, + }); + + const fieldName = booleanField.name; + const tableName = computeObjectTargetTable(booleanField.object); + + await workspaceQueryRunner.query( + `ALTER TABLE "${dataSourceMetadata.schema}"."${tableName}" ALTER COLUMN "${fieldName}" SET DEFAULT false;`, + ); + } + + await workspaceQueryRunner.commitTransaction(); + await metadataQueryRunner.commitTransaction(); + } catch (error) { + await workspaceQueryRunner.rollbackTransaction(); + await metadataQueryRunner.rollbackTransaction(); + this.logger.log( + chalk.red(`Running command on workspace ${workspaceId} failed`), + ); + throw error; + } finally { + await workspaceQueryRunner.release(); + await metadataQueryRunner.release(); + } + + await this.workspaceCacheVersionService.incrementVersion(workspaceId); + + this.logger.log( + chalk.green(`Running command on workspace ${workspaceId} done`), + ); + } + + this.logger.log(chalk.green(`Command completed!`)); + } +} diff --git a/packages/twenty-server/src/database/commands/database-command.module.ts b/packages/twenty-server/src/database/commands/database-command.module.ts index d829bcf752..f673ca3937 100644 --- a/packages/twenty-server/src/database/commands/database-command.module.ts +++ b/packages/twenty-server/src/database/commands/database-command.module.ts @@ -21,6 +21,7 @@ import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/ import { WorkspaceCacheVersionModule } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.module'; import { UpdateMessageChannelSyncStatusEnumCommand } from 'src/database/commands/0-20-update-message-channel-sync-status-enum.command'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { UpdateBooleanFieldsNullDefaultValuesAndNullValuesCommand } from 'src/database/commands/0-22-update-boolean-fields-null-default-values-and-null-values.command'; @Module({ imports: [ @@ -48,6 +49,7 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat StopDataSeedDemoWorkspaceCronCommand, UpdateMessageChannelVisibilityEnumCommand, UpdateMessageChannelSyncStatusEnumCommand, + UpdateBooleanFieldsNullDefaultValuesAndNullValuesCommand, ], }) export class DatabaseCommandModule {} diff --git a/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-event.workspace-entity.ts b/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-event.workspace-entity.ts index a4609608a2..563c853916 100644 --- a/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-event.workspace-entity.ts +++ b/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-event.workspace-entity.ts @@ -44,6 +44,7 @@ export class CalendarEventWorkspaceEntity extends BaseWorkspaceEntity { label: 'Is canceled', description: 'Is canceled', icon: 'IconCalendarCancel', + defaultValue: false, }) isCanceled: boolean; @@ -53,6 +54,7 @@ export class CalendarEventWorkspaceEntity extends BaseWorkspaceEntity { label: 'Is Full Day', description: 'Is Full Day', icon: 'Icon24Hours', + defaultValue: false, }) isFullDay: boolean; From cc6ce142cec30be084d036f9709a9ebcdbabba62 Mon Sep 17 00:00:00 2001 From: Weiko Date: Fri, 5 Jul 2024 17:01:47 +0200 Subject: [PATCH 3/3] Fix sort with Email and FullName field types and add sort/filter to labelIdentifier column (#6132) Fixes https://github.com/twentyhq/twenty/issues/5958 Screenshot 2024-07-04 at 16 23 25 Screenshot 2024-07-04 at 16 15 39 --- ...rmatFieldMetadataItemsAsSortDefinitions.ts | 2 ++ .../utils/turnSortsIntoOrderBy.ts | 25 ++++++++++++++++--- .../RecordTableColumnDropdownMenu.tsx | 21 ++++++++++------ .../components/RecordTableHeaderCell.tsx | 7 +----- 4 files changed, 38 insertions(+), 17 deletions(-) diff --git a/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsSortDefinitions.ts b/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsSortDefinitions.ts index 56b84aec08..b62f7fb35b 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsSortDefinitions.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsSortDefinitions.ts @@ -18,6 +18,8 @@ export const formatFieldMetadataItemsAsSortDefinitions = ({ FieldMetadataType.Boolean, FieldMetadataType.Select, FieldMetadataType.Phone, + FieldMetadataType.Email, + FieldMetadataType.FullName, ].includes(field.type) ) { return acc; diff --git a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy.ts b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy.ts index b33c7ea1e8..04e8771094 100644 --- a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy.ts +++ b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy.ts @@ -2,7 +2,7 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { OrderBy } from '@/object-metadata/types/OrderBy'; import { hasPositionField } from '@/object-metadata/utils/hasPositionField'; import { RecordGqlOperationOrderBy } from '@/object-record/graphql/types/RecordGqlOperationOrderBy'; -import { Field } from '~/generated/graphql'; +import { Field, FieldMetadataType } from '~/generated/graphql'; import { mapArrayToObject } from '~/utils/array/mapArrayToObject'; import { isDefined } from '~/utils/isDefined'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; @@ -13,7 +13,8 @@ export const turnSortsIntoOrderBy = ( objectMetadataItem: ObjectMetadataItem, sorts: Sort[], ): RecordGqlOperationOrderBy => { - const fields: Pick[] = objectMetadataItem?.fields ?? []; + const fields: Pick[] = + objectMetadataItem?.fields ?? []; const fieldsById = mapArrayToObject(fields, ({ id }) => id); const sortsOrderBy = sorts .map((sort) => { @@ -26,7 +27,7 @@ export const turnSortsIntoOrderBy = ( const direction: OrderBy = sort.direction === 'asc' ? 'AscNullsFirst' : 'DescNullsLast'; - return { [correspondingField.name]: direction }; + return getOrderByForFieldMetadataType(correspondingField, direction); }) .filter(isDefined); @@ -36,3 +37,21 @@ export const turnSortsIntoOrderBy = ( return sortsOrderBy; }; + +const getOrderByForFieldMetadataType = ( + field: Pick, + direction: OrderBy, +) => { + switch (field.type) { + case FieldMetadataType.FullName: + return { + [field.name]: { + firstName: direction, + lastName: direction, + }, + }; + + default: + return { [field.name]: direction }; + } +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableColumnDropdownMenu.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableColumnDropdownMenu.tsx index 96a257041f..bbd2f131c5 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableColumnDropdownMenu.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableColumnDropdownMenu.tsx @@ -33,12 +33,13 @@ export const RecordTableColumnDropdownMenu = ({ const visibleTableColumns = useRecoilValue(visibleTableColumnsSelector()); const secondVisibleColumn = visibleTableColumns[1]; + const canMove = column.isLabelIdentifier !== true; const canMoveLeft = - column.fieldMetadataId !== secondVisibleColumn?.fieldMetadataId; + column.fieldMetadataId !== secondVisibleColumn?.fieldMetadataId && canMove; const lastVisibleColumn = visibleTableColumns[visibleTableColumns.length - 1]; const canMoveRight = - column.fieldMetadataId !== lastVisibleColumn?.fieldMetadataId; + column.fieldMetadataId !== lastVisibleColumn?.fieldMetadataId && canMove; const { handleColumnVisibilityChange, handleMoveTableColumn } = useTableColumns(); @@ -83,7 +84,9 @@ export const RecordTableColumnDropdownMenu = ({ const isSortable = column.isSortable === true; const isFilterable = column.isFilterable === true; - const showSeparator = isFilterable || isSortable; + const showSeparator = + (isFilterable || isSortable) && column.isLabelIdentifier !== true; + const canHide = column.isLabelIdentifier !== true; return ( @@ -116,11 +119,13 @@ export const RecordTableColumnDropdownMenu = ({ text="Move right" /> )} - + {canHide && ( + + )} ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableHeaderCell.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableHeaderCell.tsx index 9330ff2c54..702b644ffc 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableHeaderCell.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableHeaderCell.tsx @@ -4,7 +4,6 @@ import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil'; import { IconPlus } from 'twenty-ui'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; -import { ColumnHead } from '@/object-record/record-table/components/ColumnHead'; import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; import { useTableColumns } from '@/object-record/record-table/hooks/useTableColumns'; import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; @@ -186,11 +185,7 @@ export const RecordTableHeaderCell = ({ onMouseLeave={() => setIconVisibility(false)} > - {column.isLabelIdentifier ? ( - - ) : ( - - )} + {(useIsMobile() || iconVisibility) && !!column.isLabelIdentifier && (