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/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 && ( 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', - ); - }, -}; 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;