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;