From be171e84d7198f6f0439152f5527aa3661a0e7a9 Mon Sep 17 00:00:00 2001
From: Marie <51697796+ijreilly@users.noreply.github.com>
Date: Tue, 8 Oct 2024 16:20:34 +0200
Subject: [PATCH 01/10] Fix create task (#7498)
Fixing issue introduced by [Add Skeleton loading for side
panel](https://github.com/twentyhq/twenty/pull/7394/files#top):
https://github.com/user-attachments/assets/6c8e299c-d663-4aa7-83ed-ca7041cd15e7
---
.../hooks/useOpenCreateActivityDrawer.ts | 2 +-
.../components/RightDrawerRecord.tsx | 18 +++++++++++++++++-
.../components/RecordShowContainer.tsx | 8 ++------
.../record-show/hooks/useRecordShowPage.ts | 6 +++---
4 files changed, 23 insertions(+), 11 deletions(-)
diff --git a/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts b/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts
index c35d27837121..1cc4af08a81e 100644
--- a/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts
+++ b/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts
@@ -67,8 +67,8 @@ export const useOpenCreateActivityDrawer = ({
targetableObjects: ActivityTargetableObject[];
customAssignee?: WorkspaceMember;
}) => {
- openRightDrawer(RightDrawerPages.ViewRecord);
setIsNewViewableRecordLoading(true);
+ openRightDrawer(RightDrawerPages.ViewRecord);
setViewableRecordId(null);
setViewableRecordNameSingular(activityObjectNameSingular);
diff --git a/packages/twenty-front/src/modules/object-record/record-right-drawer/components/RightDrawerRecord.tsx b/packages/twenty-front/src/modules/object-record/record-right-drawer/components/RightDrawerRecord.tsx
index 6a76206e6e75..3a747b0913a1 100644
--- a/packages/twenty-front/src/modules/object-record/record-right-drawer/components/RightDrawerRecord.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-right-drawer/components/RightDrawerRecord.tsx
@@ -1,5 +1,6 @@
import { useRecoilValue } from 'recoil';
+import { isNewViewableRecordLoadingState } from '@/object-record/record-right-drawer/states/isNewViewableRecordLoading';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
import { RecordShowContainer } from '@/object-record/record-show/components/RecordShowContainer';
@@ -18,7 +19,19 @@ export const RightDrawerRecord = () => {
const viewableRecordNameSingular = useRecoilValue(
viewableRecordNameSingularState,
);
+ const isNewViewableRecordLoading = useRecoilValue(
+ isNewViewableRecordLoadingState,
+ );
const viewableRecordId = useRecoilValue(viewableRecordIdState);
+
+ if (!viewableRecordNameSingular && !isNewViewableRecordLoading) {
+ throw new Error(`Object name is not defined`);
+ }
+
+ if (!viewableRecordId && !isNewViewableRecordLoading) {
+ throw new Error(`Record id is not defined`);
+ }
+
const { objectNameSingular, objectRecordId } = useRecordShowPage(
viewableRecordNameSingular ?? '',
viewableRecordId ?? '',
@@ -27,12 +40,15 @@ export const RightDrawerRecord = () => {
return (
-
+ {!isNewViewableRecordLoading && (
+
+ )}
diff --git a/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx b/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx
index f9176f499913..9b1e10601ab4 100644
--- a/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx
@@ -21,7 +21,6 @@ import { RecordInlineCell } from '@/object-record/record-inline-cell/components/
import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox';
import { PropertyBoxSkeletonLoader } from '@/object-record/record-inline-cell/property-box/components/PropertyBoxSkeletonLoader';
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
-import { isNewViewableRecordLoadingState } from '@/object-record/record-right-drawer/states/isNewViewableRecordLoading';
import { RecordDetailDuplicatesSection } from '@/object-record/record-show/record-detail-section/components/RecordDetailDuplicatesSection';
import { RecordDetailRelationSection } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationSection';
import { recordLoadingFamilyState } from '@/object-record/record-store/states/recordLoadingFamilyState';
@@ -49,6 +48,7 @@ type RecordShowContainerProps = {
objectRecordId: string;
loading: boolean;
isInRightDrawer?: boolean;
+ isNewRightDrawerItemLoading?: boolean;
};
export const RecordShowContainer = ({
@@ -56,6 +56,7 @@ export const RecordShowContainer = ({
objectRecordId,
loading,
isInRightDrawer = false,
+ isNewRightDrawerItemLoading = false,
}: RecordShowContainerProps) => {
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
@@ -82,9 +83,6 @@ export const RecordShowContainer = ({
recordId: objectRecordId,
}),
);
- const isNewViewableRecordLoading = useRecoilValue(
- isNewViewableRecordLoadingState,
- );
const [uploadImage] = useUploadImageMutation();
const { updateOneRecord } = useUpdateOneRecord({ objectNameSingular });
@@ -166,8 +164,6 @@ export const RecordShowContainer = ({
const isReadOnly = objectMetadataItem.isRemote;
const isMobile = useIsMobile() || isInRightDrawer;
const isPrefetchLoading = useIsPrefetchLoading();
- const isNewRightDrawerItemLoading =
- isInRightDrawer && isNewViewableRecordLoading;
const summaryCard =
!isNewRightDrawerItemLoading && isDefined(recordFromStore) ? (
diff --git a/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowPage.ts b/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowPage.ts
index 7bdaf0b4284b..fb73b8ce9a52 100644
--- a/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowPage.ts
+++ b/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowPage.ts
@@ -23,10 +23,10 @@ export const useRecordShowPage = (
objectRecordId: paramObjectRecordId,
} = useParams();
- const objectNameSingular = propsObjectNameSingular || paramObjectNameSingular;
- const objectRecordId = propsObjectRecordId || paramObjectRecordId;
+ const objectNameSingular = propsObjectNameSingular ?? paramObjectNameSingular;
+ const objectRecordId = propsObjectRecordId ?? paramObjectRecordId;
- if (!objectNameSingular || !objectRecordId) {
+ if (!isDefined(objectNameSingular) || !isDefined(objectRecordId)) {
throw new Error('Object name or Record id is not defined');
}
From 10e75174f5ae424eaf456777e4f9e18ec23a5d06 Mon Sep 17 00:00:00 2001
From: Vardhaman Bhandari <97441447+Vardhaman619@users.noreply.github.com>
Date: Tue, 8 Oct 2024 20:12:13 +0530
Subject: [PATCH 02/10] Fix: Adjust chevron alignment to the right edge (#7438)
This pull request addresses the alignment issue of the chevron icon,
ensuring that it is positioned correctly on the right edge.
Fixes [#7403](https://github.com/twentyhq/twenty/issues/7403)

---------
Co-authored-by: Charles Bochet
Co-authored-by: ehconitin
---
.../components/SettingsApiKeysFieldItemTableRow.tsx | 3 ++-
.../settings/developers/components/SettingsApiKeysTable.tsx | 2 +-
.../components/SettingsDevelopersWebhookTableRow.tsx | 4 ++--
3 files changed, 5 insertions(+), 4 deletions(-)
diff --git a/packages/twenty-front/src/modules/settings/developers/components/SettingsApiKeysFieldItemTableRow.tsx b/packages/twenty-front/src/modules/settings/developers/components/SettingsApiKeysFieldItemTableRow.tsx
index 4d4dc8d45e6f..c8051eab0511 100644
--- a/packages/twenty-front/src/modules/settings/developers/components/SettingsApiKeysFieldItemTableRow.tsx
+++ b/packages/twenty-front/src/modules/settings/developers/components/SettingsApiKeysFieldItemTableRow.tsx
@@ -7,7 +7,7 @@ import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableRow } from '@/ui/layout/table/components/TableRow';
export const StyledApisFieldTableRow = styled(TableRow)`
- grid-template-columns: 312px 132px 68px;
+ grid-template-columns: 312px auto 28px;
`;
const StyledNameTableCell = styled(TableCell)`
@@ -18,6 +18,7 @@ const StyledNameTableCell = styled(TableCell)`
const StyledIconTableCell = styled(TableCell)`
justify-content: center;
padding-right: ${({ theme }) => theme.spacing(1)};
+ padding-left: 0;
`;
const StyledIconChevronRight = styled(IconChevronRight)`
diff --git a/packages/twenty-front/src/modules/settings/developers/components/SettingsApiKeysTable.tsx b/packages/twenty-front/src/modules/settings/developers/components/SettingsApiKeysTable.tsx
index ede12c34bf6c..6d70ada4c862 100644
--- a/packages/twenty-front/src/modules/settings/developers/components/SettingsApiKeysTable.tsx
+++ b/packages/twenty-front/src/modules/settings/developers/components/SettingsApiKeysTable.tsx
@@ -18,7 +18,7 @@ const StyledTableBody = styled(TableBody)`
`;
const StyledTableRow = styled(TableRow)`
- grid-template-columns: 312px 132px 68px;
+ grid-template-columns: 312px auto 28px;
`;
export const SettingsApiKeysTable = () => {
diff --git a/packages/twenty-front/src/modules/settings/developers/components/SettingsDevelopersWebhookTableRow.tsx b/packages/twenty-front/src/modules/settings/developers/components/SettingsDevelopersWebhookTableRow.tsx
index 0c093d5ecbd2..d559f89a200a 100644
--- a/packages/twenty-front/src/modules/settings/developers/components/SettingsDevelopersWebhookTableRow.tsx
+++ b/packages/twenty-front/src/modules/settings/developers/components/SettingsDevelopersWebhookTableRow.tsx
@@ -1,4 +1,3 @@
-import React from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconChevronRight } from 'twenty-ui';
@@ -8,12 +7,13 @@ import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableRow } from '@/ui/layout/table/components/TableRow';
export const StyledApisFieldTableRow = styled(TableRow)`
- grid-template-columns: 444px 68px;
+ grid-template-columns: 1fr 28px;
`;
const StyledIconTableCell = styled(TableCell)`
justify-content: center;
padding-right: ${({ theme }) => theme.spacing(1)};
+ padding-left: 0;
`;
const StyledUrlTableCell = styled(TableCell)`
From 098551b7b882c158bd71e4d65cc4f6375085294e Mon Sep 17 00:00:00 2001
From: Harshit Singh <73997189+harshit078@users.noreply.github.com>
Date: Tue, 8 Oct 2024 20:18:15 +0530
Subject: [PATCH 03/10] fix: Invite by email table overflows in mobile viewport
(#7273)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
##Description
- This PR solves the issue #7253
- Made the invite table mobile friendly for all media width
## Before
https://github.com/user-attachments/assets/458bd47d-38fb-4ddc-a996-c1bb3908d014
## After
https://github.com/user-attachments/assets/7a4f6f9a-7fef-42f1-a226-59a1d73767f4
> [!Note]
> I've added 2 implementations and if either doesn't follow design rules
then it can be changed-
> - Made the trash icon `accent danger`
> - When emails are long, given scroll for ease of convience.
---------
Co-authored-by: Nitin Koche
---
.../settings/SettingsWorkspaceMembers.tsx | 114 ++++++++++++------
1 file changed, 77 insertions(+), 37 deletions(-)
diff --git a/packages/twenty-front/src/pages/settings/SettingsWorkspaceMembers.tsx b/packages/twenty-front/src/pages/settings/SettingsWorkspaceMembers.tsx
index 0079fb888501..63bbd6717d63 100644
--- a/packages/twenty-front/src/pages/settings/SettingsWorkspaceMembers.tsx
+++ b/packages/twenty-front/src/pages/settings/SettingsWorkspaceMembers.tsx
@@ -7,8 +7,8 @@ import {
IconUsers,
IconReload,
IconMail,
- StyledText,
Avatar,
+ MOBILE_VIEWPORT,
} from 'twenty-ui';
import { isNonEmptyArray } from '@sniptt/guards';
import { useTheme } from '@emotion/react';
@@ -53,6 +53,47 @@ const StyledTable = styled(Table)`
margin-top: ${({ theme }) => theme.spacing(0.5)};
`;
+const StyledTableRow = styled(TableRow)`
+ @media (max-width: ${MOBILE_VIEWPORT}px) {
+ display: grid;
+ grid-template-columns: 3fr;
+ }
+`;
+const StyledTableCell = styled(TableCell)`
+ padding: ${({ theme }) => theme.spacing(1)};
+ @media (max-width: ${MOBILE_VIEWPORT}px) {
+ &:first-child {
+ max-width: 100%;
+ padding-top: 2px;
+ white-space: nowrap;
+ overflow: scroll;
+ scroll-behavior: smooth;
+ }
+ }
+`;
+const StyledIconWrapper = styled.div`
+ left: 2px;
+ margin-right: ${({ theme }) => theme.spacing(2)};
+ position: relative;
+ top: 1px;
+`;
+
+const StyledScrollableTextContainer = styled.div`
+ max-width: 100%;
+ overflow-x: auto;
+ white-space: pre-line;
+`;
+
+const StyledTextContainer = styled.div`
+ color: ${({ theme }) => theme.font.color.secondary};
+ max-width: max-content;
+ overflow-x: auto;
+ position: absolute;
+ @media (min-width: 360px) and (max-width: 420px) {
+ max-width: 150px;
+ margin-top: ${({ theme }) => theme.spacing(1)};
+ }
+`;
const StyledTableHeaderRow = styled(Table)`
margin-bottom: ${({ theme }) => theme.spacing(1.5)};
`;
@@ -165,28 +206,25 @@ export const SettingsWorkspaceMembers = () => {
-
- }
- text={
- workspaceMember.name.firstName +
+
+
+
+
+ {workspaceMember.name.firstName +
' ' +
- workspaceMember.name.lastName
- }
- />
+ workspaceMember.name.lastName}
+
-
+
+ {workspaceMember.userEmail}
+
{currentWorkspaceMember?.id !== workspaceMember.id && (
@@ -225,25 +263,27 @@ export const SettingsWorkspaceMembers = () => {
{workspaceInvitations?.map((workspaceInvitation) => (
-
-
-
- }
- text={workspaceInvitation.email}
- />
-
-
+
+
+
+
+
+
+ {workspaceInvitation.email}
+
+
+
-
-
+
+
{
@@ -266,8 +306,8 @@ export const SettingsWorkspaceMembers = () => {
Icon={IconTrash}
/>
-
-
+
+
))}
From fcd60be110eb0a524f1f2eefe439c3249dd3086d Mon Sep 17 00:00:00 2001
From: Charles Bochet
Date: Tue, 8 Oct 2024 16:52:15 +0200
Subject: [PATCH 04/10] Fix filtered INDEX view not loading (#7501)
## Context
We have recently merged a refactoring of our view module. However, one
case was forgotten which is to test our dynamic filtering logic.
It is currently possible to pass unsaved filters through the URL and
these filters will be applied to the currentView through
`QueryParamsFiltersEffect`. This component was saving filters but also
listening to them through useGetCurrentView hook.
## How
1) I'm removing this infinite loop by directly loading currentViewId
through the right recoil atom.
Bonus: I'm also removing the unmounting logic which seems wrong to me as
unsaved filters are mounted on a specific view, there is no need to
remove them while switching views in my opinion.
---
.../components/RecordDetailRelationSection.tsx | 12 ++++++++++++
.../components/QueryParamsFiltersEffect.tsx | 16 +++++-----------
2 files changed, 17 insertions(+), 11 deletions(-)
diff --git a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx
index 23fc1f07096f..edd49b3f8406 100644
--- a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx
@@ -24,12 +24,15 @@ import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRela
import { RelationPickerScope } from '@/object-record/relation-picker/scopes/RelationPickerScope';
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
+import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
+import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { FilterQueryParams } from '@/views/hooks/internal/useViewFromQueryParams';
+import { View } from '@/views/types/View';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
@@ -119,12 +122,21 @@ export const RecordDetailRelationSection = ({
scopeId: dropdownId,
});
+ const { records: views } = usePrefetchedData(PrefetchKey.AllViews);
+
+ const indexView = views.find(
+ (view) =>
+ view.key === 'INDEX' &&
+ view.objectMetadataId === relationObjectMetadataItem.id,
+ );
+
const filterQueryParams: FilterQueryParams = {
filter: {
[relationFieldMetadataItem?.name || '']: {
[ViewFilterOperand.Is]: [recordId],
},
},
+ view: indexView?.id,
};
const filterLinkHref = `/objects/${
relationObjectMetadataItem.namePlural
diff --git a/packages/twenty-front/src/modules/views/components/QueryParamsFiltersEffect.tsx b/packages/twenty-front/src/modules/views/components/QueryParamsFiltersEffect.tsx
index 10ecdfa83b87..f811df0ddf6b 100644
--- a/packages/twenty-front/src/modules/views/components/QueryParamsFiltersEffect.tsx
+++ b/packages/twenty-front/src/modules/views/components/QueryParamsFiltersEffect.tsx
@@ -1,23 +1,24 @@
import { useEffect } from 'react';
+import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentFamilyStateV2';
import { useViewFromQueryParams } from '@/views/hooks/internal/useViewFromQueryParams';
-import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { useResetUnsavedViewStates } from '@/views/hooks/useResetUnsavedViewStates';
+import { currentViewIdComponentState } from '@/views/states/currentViewIdComponentState';
import { unsavedToUpsertViewFiltersComponentFamilyState } from '@/views/states/unsavedToUpsertViewFiltersComponentFamilyState';
-import { isDefined } from 'twenty-ui';
export const QueryParamsFiltersEffect = () => {
const { hasFiltersQueryParams, getFiltersFromQueryParams, viewIdQueryParam } =
useViewFromQueryParams();
+ const currentViewId = useRecoilComponentValueV2(currentViewIdComponentState);
+
const setUnsavedViewFilter = useSetRecoilComponentFamilyStateV2(
unsavedToUpsertViewFiltersComponentFamilyState,
- { viewId: viewIdQueryParam },
+ { viewId: viewIdQueryParam ?? currentViewId },
);
const { resetUnsavedViewStates } = useResetUnsavedViewStates();
- const { currentViewId } = useGetCurrentView();
useEffect(() => {
if (!hasFiltersQueryParams) {
@@ -29,18 +30,11 @@ export const QueryParamsFiltersEffect = () => {
setUnsavedViewFilter(filtersFromParams);
}
});
-
- return () => {
- if (isDefined(currentViewId)) {
- resetUnsavedViewStates(currentViewId);
- }
- };
}, [
getFiltersFromQueryParams,
hasFiltersQueryParams,
resetUnsavedViewStates,
setUnsavedViewFilter,
- currentViewId,
]);
return <>>;
From cbdd09b00e518986e98ded44a3d515eb2479b4e9 Mon Sep 17 00:00:00 2001
From: Thomas Trompette
Date: Tue, 8 Oct 2024 17:14:50 +0200
Subject: [PATCH 05/10] Fix advanced settings animation (#7497)
As title
---
.../SettingsNavigationDrawerItems.tsx | 24 ++++++++++---------
.../hooks/useExpandedHeightAnimation.tsx | 5 ----
.../components/NavigationDrawer.tsx | 8 ++-----
3 files changed, 15 insertions(+), 22 deletions(-)
diff --git a/packages/twenty-front/src/modules/settings/components/SettingsNavigationDrawerItems.tsx b/packages/twenty-front/src/modules/settings/components/SettingsNavigationDrawerItems.tsx
index f2ea6c95ad18..b41eb757e9ba 100644
--- a/packages/twenty-front/src/modules/settings/components/SettingsNavigationDrawerItems.tsx
+++ b/packages/twenty-front/src/modules/settings/components/SettingsNavigationDrawerItems.tsx
@@ -47,14 +47,16 @@ type SettingsNavigationItem = {
indentationLevel?: NavigationDrawerItemIndentationLevel;
};
+const StyledNavigationDrawerSection = styled(NavigationDrawerSection)`
+ margin-bottom: ${({ theme }) => theme.spacing(3)};
+`;
+
const StyledIconContainer = styled.div`
border-right: 1px solid ${MAIN_COLORS.yellow};
- display: flex;
- width: 16px;
position: absolute;
left: ${({ theme }) => theme.spacing(-5)};
margin-top: ${({ theme }) => theme.spacing(2)};
- height: 90%;
+ height: 75%;
`;
const StyledDeveloperSection = styled.div`
@@ -119,7 +121,7 @@ export const SettingsNavigationDrawerItems = () => {
return (
<>
-
+
{
/>
))}
-
-
+
+
{
Icon={IconCode}
/>
)}
-
+
{isAdvancedModeEnabled && (
{
-
+
{
Icon={IconFunction}
/>
)}
-
+
)}
-
+
{
onClick={signOut}
Icon={IconDoorEnter}
/>
-
+
>
);
};
diff --git a/packages/twenty-front/src/modules/settings/hooks/useExpandedHeightAnimation.tsx b/packages/twenty-front/src/modules/settings/hooks/useExpandedHeightAnimation.tsx
index 3a48c3bb20b9..e762990f0612 100644
--- a/packages/twenty-front/src/modules/settings/hooks/useExpandedHeightAnimation.tsx
+++ b/packages/twenty-front/src/modules/settings/hooks/useExpandedHeightAnimation.tsx
@@ -6,15 +6,11 @@ const transitionValues = {
opactity: { duration: 0.2 },
height: { duration: 0.4 },
},
- transitionEnd: {
- overflow: 'visible',
- },
};
const commonStyles = {
opacity: 0,
height: 0,
- overflow: 'hidden',
...transitionValues,
};
@@ -29,7 +25,6 @@ const advancedSectionAnimationConfig = (
opacity: 1,
height: isExpanded ? measuredHeight : 0,
...transitionValues,
- overflow: 'hidden',
},
exit: {
...commonStyles,
diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawer.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawer.tsx
index d25c3ab59f4e..ce0bb6f70e6a 100644
--- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawer.tsx
+++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawer.tsx
@@ -49,12 +49,10 @@ const StyledContainer = styled.div<{ isSubMenu?: boolean }>`
width: 100%;
}
`;
-const StyledItemsContainer = styled.div<{ isSubMenu?: boolean }>`
+const StyledItemsContainer = styled.div`
display: flex;
flex-direction: column;
margin-bottom: auto;
- gap: ${({ theme }) => theme.spacing(3)};
- ${({ isSubMenu }) => (!isSubMenu ? 'overflow-y: auto' : '')};
`;
export const NavigationDrawer = ({
@@ -111,9 +109,7 @@ export const NavigationDrawer = ({
showCollapseButton={isHovered}
/>
)}
-
- {children}
-
+ {children}
{footer}
From 78a7c733086d07a95d829f4449b234e6d8f08e06 Mon Sep 17 00:00:00 2001
From: Charles Bochet
Date: Tue, 8 Oct 2024 17:20:23 +0200
Subject: [PATCH 06/10] Fix relation direction on show page
---
.../components/RecordDetailRelationSection.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx
index edd49b3f8406..4e2d194036f6 100644
--- a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx
@@ -72,7 +72,7 @@ export const RecordDetailRelationSection = ({
// TODO: use new relation type
const isToOneObject = relationType === RelationDefinitionType.ManyToOne;
- const isToManyObjects = RelationDefinitionType.OneToMany;
+ const isToManyObjects = relationType === RelationDefinitionType.OneToMany;
const relationRecords: ObjectRecord[] =
fieldValue && isToOneObject
From a0484369298256a608f3ce9b69b7ae31ccd012bd Mon Sep 17 00:00:00 2001
From: Marie <51697796+ijreilly@users.noreply.github.com>
Date: Tue, 8 Oct 2024 17:22:16 +0200
Subject: [PATCH 07/10] Bump version to v0.31.0 (#7500)
Co-authored-by: Weiko
---
packages/twenty-emails/package.json | 2 +-
packages/twenty-front/package.json | 2 +-
packages/twenty-server/package.json | 2 +-
packages/twenty-ui/package.json | 2 +-
packages/twenty-website/package.json | 2 +-
.../developers/self-hosting/upgrade-guide.mdx | 19 +++++++++++++++++++
6 files changed, 24 insertions(+), 5 deletions(-)
diff --git a/packages/twenty-emails/package.json b/packages/twenty-emails/package.json
index 1f20fbe7eb64..e42c2b318a21 100644
--- a/packages/twenty-emails/package.json
+++ b/packages/twenty-emails/package.json
@@ -1,6 +1,6 @@
{
"name": "twenty-emails",
- "version": "0.31.0-canary",
+ "version": "0.31.0",
"description": "",
"author": "",
"private": true,
diff --git a/packages/twenty-front/package.json b/packages/twenty-front/package.json
index 9370f2460b92..90bd99f40175 100644
--- a/packages/twenty-front/package.json
+++ b/packages/twenty-front/package.json
@@ -1,6 +1,6 @@
{
"name": "twenty-front",
- "version": "0.31.0-canary",
+ "version": "0.31.0",
"private": true,
"type": "module",
"scripts": {
diff --git a/packages/twenty-server/package.json b/packages/twenty-server/package.json
index d5c8f91b9c69..1f94d52eff93 100644
--- a/packages/twenty-server/package.json
+++ b/packages/twenty-server/package.json
@@ -1,6 +1,6 @@
{
"name": "twenty-server",
- "version": "0.31.0-canary",
+ "version": "0.31.0",
"description": "",
"author": "",
"private": true,
diff --git a/packages/twenty-ui/package.json b/packages/twenty-ui/package.json
index 3e9aadc6ce64..ecf63eb4cbdf 100644
--- a/packages/twenty-ui/package.json
+++ b/packages/twenty-ui/package.json
@@ -1,6 +1,6 @@
{
"name": "twenty-ui",
- "version": "0.31.0-canary",
+ "version": "0.31.0",
"type": "module",
"main": "./src/index.ts",
"exports": {
diff --git a/packages/twenty-website/package.json b/packages/twenty-website/package.json
index f3ca9e2137db..9db997579234 100644
--- a/packages/twenty-website/package.json
+++ b/packages/twenty-website/package.json
@@ -1,6 +1,6 @@
{
"name": "twenty-website",
- "version": "0.31.0-canary",
+ "version": "0.31.0",
"private": true,
"scripts": {
"nx": "NX_DEFAULT_PROJECT=twenty-website node ../../node_modules/nx/bin/nx.js",
diff --git a/packages/twenty-website/src/content/developers/self-hosting/upgrade-guide.mdx b/packages/twenty-website/src/content/developers/self-hosting/upgrade-guide.mdx
index 8a71df02a433..c260b6a04178 100644
--- a/packages/twenty-website/src/content/developers/self-hosting/upgrade-guide.mdx
+++ b/packages/twenty-website/src/content/developers/self-hosting/upgrade-guide.mdx
@@ -22,6 +22,8 @@ Migrating a CRM is a bit trickier than migrating a traditional software, because
## v0.21.0 to v0.22.0
+Upgrade your Twenty instance to use v0.22.0 image
+
Run the following commands:
```
@@ -36,6 +38,8 @@ The `yarn command:prod upgrade-0.22` command will apply specific data transforma
## v0.22.0 to v0.23.0
+Upgrade your Twenty instance to use v0.23.0 image
+
Run the following commands:
```
@@ -48,6 +52,8 @@ The `yarn command:prod upgrade-0.23` takes care of the data migration, including
## v0.23.0 to v0.24.0
+Upgrade your Twenty instance to use v0.24.0 image
+
Run the following commands:
```
@@ -80,4 +86,17 @@ yarn command:prod upgrade-0.30
The `yarn database:migrate:prod` command will apply the migrations to the database structure (core and metadata schemas)
The `yarn command:prod upgrade-30` takes care of the data migration of all workspaces.
+# v0.30.0 to v0.31.0
+
+Upgrade your Twenty instance to use v0.31.0 image
+
+**Schema and data migration**:
+```
+yarn database:migrate:prod
+yarn command:prod upgrade-0.31
+```
+
+The `yarn database:migrate:prod` command will apply the migrations to the database structure (core and metadata schemas)
+The `yarn command:prod upgrade-31` takes care of the data migration of all workspaces.
+
From 711ff5d9572fd62ba57830e53bac2ef8740f70a0 Mon Sep 17 00:00:00 2001
From: Harshit Singh <73997189+harshit078@users.noreply.github.com>
Date: Tue, 8 Oct 2024 21:09:28 +0530
Subject: [PATCH 08/10] fix: Filter chips lacks width for longer values (#7025)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Description
Closes #7018
- When given longer values, filter chips break-spaces and lack
sufficient width
- As a result, a design overhaul is given to `StyledBar` and
`StyledChipcontainer` components.
## Before
- on Desktop
- On mobile viewport
## After
- On desktop
- On mobile viewport
https://github.com/user-attachments/assets/0b4ff758-3b6e-4bd5-8247-6b096fa7d1c0
---------
Co-authored-by: Lucas Bordeau
---
.../views/components/SortOrFilterChip.tsx | 2 ++
.../views/components/ViewBarDetails.tsx | 26 +++++++++----------
2 files changed, 14 insertions(+), 14 deletions(-)
diff --git a/packages/twenty-front/src/modules/views/components/SortOrFilterChip.tsx b/packages/twenty-front/src/modules/views/components/SortOrFilterChip.tsx
index 0fab6fffdece..8b66bfc32b5d 100644
--- a/packages/twenty-front/src/modules/views/components/SortOrFilterChip.tsx
+++ b/packages/twenty-front/src/modules/views/components/SortOrFilterChip.tsx
@@ -41,6 +41,7 @@ const StyledChip = styled.div<{ variant: SortOrFitlerChipVariant }>`
font-weight: ${({ theme }) => theme.font.weight.medium};
padding: ${({ theme }) => theme.spacing(1) + ' ' + theme.spacing(2)};
user-select: none;
+ white-space: nowrap;
`;
const StyledIcon = styled.div`
@@ -52,6 +53,7 @@ const StyledIcon = styled.div`
const StyledDelete = styled.div<{ variant: SortOrFitlerChipVariant }>`
align-items: center;
cursor: pointer;
+ padding: ${({ theme }) => theme.spacing(0.5)};
display: flex;
font-size: ${({ theme }) => theme.font.size.sm};
margin-left: ${({ theme }) => theme.spacing(2)};
diff --git a/packages/twenty-front/src/modules/views/components/ViewBarDetails.tsx b/packages/twenty-front/src/modules/views/components/ViewBarDetails.tsx
index e3d443ace9e1..87b16a65fd14 100644
--- a/packages/twenty-front/src/modules/views/components/ViewBarDetails.tsx
+++ b/packages/twenty-front/src/modules/views/components/ViewBarDetails.tsx
@@ -31,24 +31,27 @@ export type ViewBarDetailsProps = {
const StyledBar = styled.div`
align-items: center;
+ align-items: center;
+ border-top: 1px solid ${({ theme }) => theme.border.color.light};
border-top: 1px solid ${({ theme }) => theme.border.color.light};
display: flex;
flex-direction: row;
- min-height: 32px;
justify-content: space-between;
- z-index: 4;
+ min-height: 32px;
padding-top: ${({ theme }) => theme.spacing(1)};
padding-bottom: ${({ theme }) => theme.spacing(1)};
+ z-index: 4;
`;
const StyledChipcontainer = styled.div`
align-items: center;
display: flex;
flex-direction: row;
+ overflow: scroll;
gap: ${({ theme }) => theme.spacing(1)};
- min-height: 32px;
- margin-left: ${({ theme }) => theme.spacing(2)};
- flex-wrap: wrap;
+ padding-top: ${({ theme }) => theme.spacing(1)};
+ padding-bottom: ${({ theme }) => theme.spacing(0.5)};
+ z-index: 1;
`;
const StyledCancelButton = styled.button`
@@ -57,15 +60,8 @@ const StyledCancelButton = styled.button`
color: ${({ theme }) => theme.font.color.tertiary};
cursor: pointer;
font-weight: ${({ theme }) => theme.font.weight.medium};
- margin-left: auto;
- margin-right: ${({ theme }) => theme.spacing(2)};
- padding: ${(props) => {
- const horiz = props.theme.spacing(2);
- const vert = props.theme.spacing(1);
- return `${vert} ${horiz} ${vert} ${horiz}`;
- }};
user-select: none;
-
+ margin-right: ${({ theme }) => theme.spacing(2)};
&:hover {
background-color: ${({ theme }) => theme.background.tertiary};
border-radius: ${({ theme }) => theme.spacing(1)};
@@ -73,8 +69,10 @@ const StyledCancelButton = styled.button`
`;
const StyledFilterContainer = styled.div`
- align-items: center;
display: flex;
+ align-items: center;
+ flex: 1;
+ overflow-x: hidden;
`;
const StyledSeperatorContainer = styled.div`
From e662f6ccb3971be5776a78abe23fb60f9e9658e2 Mon Sep 17 00:00:00 2001
From: sid0-0 <43578323+sid0-0@users.noreply.github.com>
Date: Tue, 8 Oct 2024 21:09:41 +0530
Subject: [PATCH 09/10] fix: fixed shortcuts population (#7016)
This PR fixes #6776
Screenshots:
---------
Co-authored-by: sid0-0
Co-authored-by: Lucas Bordeau
---
packages/twenty-front/src/App.tsx | 171 ------------------
...{App.stories.tsx => AppRouter.stories.tsx} | 13 +-
.../effect-components/GotoHotkeysEffect.tsx | 11 --
packages/twenty-front/src/index.tsx | 35 +---
.../src/modules/app/components/App.tsx | 32 ++++
.../src/modules/app/components/AppRouter.tsx | 27 +++
.../app/components/AppRouterProviders.tsx | 66 +++++++
.../app/components}/SettingsRoutes.tsx | 0
.../effect-components/CommandMenuEffect.tsx | 2 +-
.../GoToHotkeyItemEffect.tsx | 12 ++
.../effect-components/GotoHotkeysEffect.tsx | 18 ++
.../effect-components/PageChangeEffect.tsx | 19 +-
.../src/modules/app/utils/createAppRouter.tsx | 78 ++++++++
.../__stories__/CommandMenu.stories.tsx | 21 ++-
.../constants/CommandMenuCommands.ts | 14 +-
.../hooks/__test__/useCommandMenu.test.tsx | 34 +++-
.../command-menu/hooks/useCommandMenu.ts | 34 +++-
.../useNonSystemActiveObjectMetadataItems.ts | 20 ++
.../src/utils/array/sortByProperty.ts | 18 ++
19 files changed, 380 insertions(+), 245 deletions(-)
delete mode 100644 packages/twenty-front/src/App.tsx
rename packages/twenty-front/src/__stories__/{App.stories.tsx => AppRouter.stories.tsx} (92%)
delete mode 100644 packages/twenty-front/src/effect-components/GotoHotkeysEffect.tsx
create mode 100644 packages/twenty-front/src/modules/app/components/App.tsx
create mode 100644 packages/twenty-front/src/modules/app/components/AppRouter.tsx
create mode 100644 packages/twenty-front/src/modules/app/components/AppRouterProviders.tsx
rename packages/twenty-front/src/{ => modules/app/components}/SettingsRoutes.tsx (100%)
rename packages/twenty-front/src/{ => modules/app}/effect-components/CommandMenuEffect.tsx (89%)
create mode 100644 packages/twenty-front/src/modules/app/effect-components/GoToHotkeyItemEffect.tsx
create mode 100644 packages/twenty-front/src/modules/app/effect-components/GotoHotkeysEffect.tsx
rename packages/twenty-front/src/{ => modules/app}/effect-components/PageChangeEffect.tsx (90%)
create mode 100644 packages/twenty-front/src/modules/app/utils/createAppRouter.tsx
create mode 100644 packages/twenty-front/src/modules/object-metadata/hooks/useNonSystemActiveObjectMetadataItems.ts
create mode 100644 packages/twenty-front/src/utils/array/sortByProperty.ts
diff --git a/packages/twenty-front/src/App.tsx b/packages/twenty-front/src/App.tsx
deleted file mode 100644
index e8757d1b9913..000000000000
--- a/packages/twenty-front/src/App.tsx
+++ /dev/null
@@ -1,171 +0,0 @@
-import { StrictMode } from 'react';
-import {
- createBrowserRouter,
- createRoutesFromElements,
- Outlet,
- Route,
- RouterProvider,
- useLocation,
-} from 'react-router-dom';
-import { useRecoilValue } from 'recoil';
-
-import { ApolloProvider } from '@/apollo/components/ApolloProvider';
-import { AuthProvider } from '@/auth/components/AuthProvider';
-import { VerifyEffect } from '@/auth/components/VerifyEffect';
-import { ChromeExtensionSidecarEffect } from '@/chrome-extension-sidecar/components/ChromeExtensionSidecarEffect';
-import { ChromeExtensionSidecarProvider } from '@/chrome-extension-sidecar/components/ChromeExtensionSidecarProvider';
-import { ClientConfigProvider } from '@/client-config/components/ClientConfigProvider';
-import { ClientConfigProviderEffect } from '@/client-config/components/ClientConfigProviderEffect';
-import { billingState } from '@/client-config/states/billingState';
-import { PromiseRejectionEffect } from '@/error-handler/components/PromiseRejectionEffect';
-import indexAppPath from '@/navigation/utils/indexAppPath';
-import { ApolloMetadataClientProvider } from '@/object-metadata/components/ApolloMetadataClientProvider';
-import { ObjectMetadataItemsProvider } from '@/object-metadata/components/ObjectMetadataItemsProvider';
-import { PrefetchDataProvider } from '@/prefetch/components/PrefetchDataProvider';
-import { AppPath } from '@/types/AppPath';
-import { DialogManager } from '@/ui/feedback/dialog-manager/components/DialogManager';
-import { DialogManagerScope } from '@/ui/feedback/dialog-manager/scopes/DialogManagerScope';
-import { SnackBarProvider } from '@/ui/feedback/snack-bar-manager/components/SnackBarProvider';
-import { BlankLayout } from '@/ui/layout/page/BlankLayout';
-import { DefaultLayout } from '@/ui/layout/page/DefaultLayout';
-import { AppThemeProvider } from '@/ui/theme/components/AppThemeProvider';
-import { PageTitle } from '@/ui/utilities/page-title/PageTitle';
-import { UserProvider } from '@/users/components/UserProvider';
-import { UserProviderEffect } from '@/users/components/UserProviderEffect';
-import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
-import { CommandMenuEffect } from '~/effect-components/CommandMenuEffect';
-import { GotoHotkeysEffect } from '~/effect-components/GotoHotkeysEffect';
-import { PageChangeEffect } from '~/effect-components/PageChangeEffect';
-import { Authorize } from '~/pages/auth/Authorize';
-import { Invite } from '~/pages/auth/Invite';
-import { PasswordReset } from '~/pages/auth/PasswordReset';
-import { SignInUp } from '~/pages/auth/SignInUp';
-import { ImpersonateEffect } from '~/pages/impersonate/ImpersonateEffect';
-import { NotFound } from '~/pages/not-found/NotFound';
-import { RecordIndexPage } from '~/pages/object-record/RecordIndexPage';
-import { RecordShowPage } from '~/pages/object-record/RecordShowPage';
-import { ChooseYourPlan } from '~/pages/onboarding/ChooseYourPlan';
-import { CreateProfile } from '~/pages/onboarding/CreateProfile';
-import { CreateWorkspace } from '~/pages/onboarding/CreateWorkspace';
-import { InviteTeam } from '~/pages/onboarding/InviteTeam';
-import { PaymentSuccess } from '~/pages/onboarding/PaymentSuccess';
-import { SyncEmails } from '~/pages/onboarding/SyncEmails';
-import { SettingsRoutes } from '~/SettingsRoutes';
-import { getPageTitleFromPath } from '~/utils/title-utils';
-
-const ProvidersThatNeedRouterContext = () => {
- const { pathname } = useLocation();
- const pageTitle = getPageTitleFromPath(pathname);
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-const createRouter = (
- isBillingEnabled?: boolean,
- isCRMMigrationEnabled?: boolean,
- isServerlessFunctionSettingsEnabled?: boolean,
-) =>
- createBrowserRouter(
- createRoutesFromElements(
- }
- // To switch state to `loading` temporarily to enable us
- // to set scroll position before the page is rendered
- loader={async () => Promise.resolve(null)}
- >
- }>
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- }
- />
- >} />
- } />
- } />
- } />
-
- }
- />
- } />
-
- }>
- } />
-
- ,
- ),
- );
-
-export const App = () => {
- const billing = useRecoilValue(billingState);
- const isFreeAccessEnabled = useIsFeatureEnabled('IS_FREE_ACCESS_ENABLED');
- const isCRMMigrationEnabled = useIsFeatureEnabled('IS_CRM_MIGRATION_ENABLED');
- const isServerlessFunctionSettingsEnabled = useIsFeatureEnabled(
- 'IS_FUNCTION_SETTINGS_ENABLED',
- );
-
- const isBillingPageEnabled =
- billing?.isBillingEnabled && !isFreeAccessEnabled;
-
- return (
-
- );
-};
diff --git a/packages/twenty-front/src/__stories__/App.stories.tsx b/packages/twenty-front/src/__stories__/AppRouter.stories.tsx
similarity index 92%
rename from packages/twenty-front/src/__stories__/App.stories.tsx
rename to packages/twenty-front/src/__stories__/AppRouter.stories.tsx
index c5314b5652a1..9d2fe91a6523 100644
--- a/packages/twenty-front/src/__stories__/App.stories.tsx
+++ b/packages/twenty-front/src/__stories__/AppRouter.stories.tsx
@@ -1,8 +1,8 @@
-import { HelmetProvider } from 'react-helmet-async';
import { getOperationName } from '@apollo/client/utilities';
import { jest } from '@storybook/jest';
import { Meta, StoryObj } from '@storybook/react';
import { graphql, HttpResponse } from 'msw';
+import { HelmetProvider } from 'react-helmet-async';
import { RecoilRoot } from 'recoil';
import { IconsProvider } from 'twenty-ui';
@@ -11,13 +11,14 @@ import indexAppPath from '@/navigation/utils/indexAppPath';
import { AppPath } from '@/types/AppPath';
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
-import { App } from '~/App';
+
+import { AppRouter } from '@/app/components/AppRouter';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockedUserData } from '~/testing/mock-data/users';
-const meta: Meta = {
- title: 'App/App',
- component: App,
+const meta: Meta = {
+ title: 'App/AppRouter',
+ component: AppRouter,
decorators: [
(Story) => {
return (
@@ -41,7 +42,7 @@ const meta: Meta = {
};
export default meta;
-export type Story = StoryObj;
+export type Story = StoryObj;
export const Default: Story = {
play: async () => {
diff --git a/packages/twenty-front/src/effect-components/GotoHotkeysEffect.tsx b/packages/twenty-front/src/effect-components/GotoHotkeysEffect.tsx
deleted file mode 100644
index 1109066af34a..000000000000
--- a/packages/twenty-front/src/effect-components/GotoHotkeysEffect.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import { useGoToHotkeys } from '@/ui/utilities/hotkey/hooks/useGoToHotkeys';
-
-export const GotoHotkeysEffect = () => {
- useGoToHotkeys('p', '/objects/people');
- useGoToHotkeys('c', '/objects/companies');
- useGoToHotkeys('o', '/objects/opportunities');
- useGoToHotkeys('s', '/settings/profile');
- useGoToHotkeys('t', '/objects/tasks');
-
- return <>>;
-};
diff --git a/packages/twenty-front/src/index.tsx b/packages/twenty-front/src/index.tsx
index 06527d80050b..2a9ce791fc41 100644
--- a/packages/twenty-front/src/index.tsx
+++ b/packages/twenty-front/src/index.tsx
@@ -1,42 +1,13 @@
import ReactDOM from 'react-dom/client';
-import { HelmetProvider } from 'react-helmet-async';
-import { RecoilRoot } from 'recoil';
-import { IconsProvider } from 'twenty-ui';
-
-import { CaptchaProvider } from '@/captcha/components/CaptchaProvider';
-import { ApolloDevLogEffect } from '@/debug/components/ApolloDevLogEffect';
-import { RecoilDebugObserverEffect } from '@/debug/components/RecoilDebugObserver';
-import { AppErrorBoundary } from '@/error-handler/components/AppErrorBoundary';
-import { ExceptionHandlerProvider } from '@/error-handler/components/ExceptionHandlerProvider';
-import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
import '@emotion/react';
-import { App } from './App';
-
-import './index.css';
+import { App } from '@/app/components/App';
import 'react-loading-skeleton/dist/skeleton.css';
+import './index.css';
const root = ReactDOM.createRoot(
document.getElementById('root') ?? document.body,
);
-root.render(
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ,
-);
+root.render();
diff --git a/packages/twenty-front/src/modules/app/components/App.tsx b/packages/twenty-front/src/modules/app/components/App.tsx
new file mode 100644
index 000000000000..f760ee9f6fb4
--- /dev/null
+++ b/packages/twenty-front/src/modules/app/components/App.tsx
@@ -0,0 +1,32 @@
+import { AppRouter } from '@/app/components/AppRouter';
+import { CaptchaProvider } from '@/captcha/components/CaptchaProvider';
+import { ApolloDevLogEffect } from '@/debug/components/ApolloDevLogEffect';
+import { RecoilDebugObserverEffect } from '@/debug/components/RecoilDebugObserver';
+import { AppErrorBoundary } from '@/error-handler/components/AppErrorBoundary';
+import { ExceptionHandlerProvider } from '@/error-handler/components/ExceptionHandlerProvider';
+import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
+import { HelmetProvider } from 'react-helmet-async';
+import { RecoilRoot } from 'recoil';
+import { IconsProvider } from 'twenty-ui';
+
+export const App = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/app/components/AppRouter.tsx b/packages/twenty-front/src/modules/app/components/AppRouter.tsx
new file mode 100644
index 000000000000..d8985e676332
--- /dev/null
+++ b/packages/twenty-front/src/modules/app/components/AppRouter.tsx
@@ -0,0 +1,27 @@
+import { createAppRouter } from '@/app/utils/createAppRouter';
+import { billingState } from '@/client-config/states/billingState';
+import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
+import { RouterProvider } from 'react-router-dom';
+import { useRecoilValue } from 'recoil';
+
+export const AppRouter = () => {
+ const billing = useRecoilValue(billingState);
+ const isFreeAccessEnabled = useIsFeatureEnabled('IS_FREE_ACCESS_ENABLED');
+ const isCRMMigrationEnabled = useIsFeatureEnabled('IS_CRM_MIGRATION_ENABLED');
+ const isServerlessFunctionSettingsEnabled = useIsFeatureEnabled(
+ 'IS_FUNCTION_SETTINGS_ENABLED',
+ );
+
+ const isBillingPageEnabled =
+ billing?.isBillingEnabled && !isFreeAccessEnabled;
+
+ return (
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/app/components/AppRouterProviders.tsx b/packages/twenty-front/src/modules/app/components/AppRouterProviders.tsx
new file mode 100644
index 000000000000..e5a24da4057a
--- /dev/null
+++ b/packages/twenty-front/src/modules/app/components/AppRouterProviders.tsx
@@ -0,0 +1,66 @@
+import { ApolloProvider } from '@/apollo/components/ApolloProvider';
+import { CommandMenuEffect } from '@/app/effect-components/CommandMenuEffect';
+import { GotoHotkeys } from '@/app/effect-components/GotoHotkeysEffect';
+import { PageChangeEffect } from '@/app/effect-components/PageChangeEffect';
+import { AuthProvider } from '@/auth/components/AuthProvider';
+import { ChromeExtensionSidecarEffect } from '@/chrome-extension-sidecar/components/ChromeExtensionSidecarEffect';
+import { ChromeExtensionSidecarProvider } from '@/chrome-extension-sidecar/components/ChromeExtensionSidecarProvider';
+import { ClientConfigProvider } from '@/client-config/components/ClientConfigProvider';
+import { ClientConfigProviderEffect } from '@/client-config/components/ClientConfigProviderEffect';
+import { PromiseRejectionEffect } from '@/error-handler/components/PromiseRejectionEffect';
+import { ApolloMetadataClientProvider } from '@/object-metadata/components/ApolloMetadataClientProvider';
+import { ObjectMetadataItemsProvider } from '@/object-metadata/components/ObjectMetadataItemsProvider';
+import { PrefetchDataProvider } from '@/prefetch/components/PrefetchDataProvider';
+import { DialogManager } from '@/ui/feedback/dialog-manager/components/DialogManager';
+import { DialogManagerScope } from '@/ui/feedback/dialog-manager/scopes/DialogManagerScope';
+import { SnackBarProvider } from '@/ui/feedback/snack-bar-manager/components/SnackBarProvider';
+import { AppThemeProvider } from '@/ui/theme/components/AppThemeProvider';
+import { PageTitle } from '@/ui/utilities/page-title/PageTitle';
+import { UserProvider } from '@/users/components/UserProvider';
+import { UserProviderEffect } from '@/users/components/UserProviderEffect';
+import { StrictMode } from 'react';
+import { Outlet, useLocation } from 'react-router-dom';
+import { getPageTitleFromPath } from '~/utils/title-utils';
+
+export const AppRouterProviders = () => {
+ const { pathname } = useLocation();
+ const pageTitle = getPageTitleFromPath(pathname);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/SettingsRoutes.tsx b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx
similarity index 100%
rename from packages/twenty-front/src/SettingsRoutes.tsx
rename to packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx
diff --git a/packages/twenty-front/src/effect-components/CommandMenuEffect.tsx b/packages/twenty-front/src/modules/app/effect-components/CommandMenuEffect.tsx
similarity index 89%
rename from packages/twenty-front/src/effect-components/CommandMenuEffect.tsx
rename to packages/twenty-front/src/modules/app/effect-components/CommandMenuEffect.tsx
index ece319312e91..b210ae724276 100644
--- a/packages/twenty-front/src/effect-components/CommandMenuEffect.tsx
+++ b/packages/twenty-front/src/modules/app/effect-components/CommandMenuEffect.tsx
@@ -7,7 +7,7 @@ import { commandMenuCommandsState } from '@/command-menu/states/commandMenuComma
export const CommandMenuEffect = () => {
const setCommands = useSetRecoilState(commandMenuCommandsState);
- const commands = COMMAND_MENU_COMMANDS;
+ const commands = Object.values(COMMAND_MENU_COMMANDS);
useEffect(() => {
setCommands(commands);
}, [commands, setCommands]);
diff --git a/packages/twenty-front/src/modules/app/effect-components/GoToHotkeyItemEffect.tsx b/packages/twenty-front/src/modules/app/effect-components/GoToHotkeyItemEffect.tsx
new file mode 100644
index 000000000000..a0b545302501
--- /dev/null
+++ b/packages/twenty-front/src/modules/app/effect-components/GoToHotkeyItemEffect.tsx
@@ -0,0 +1,12 @@
+import { useGoToHotkeys } from '@/ui/utilities/hotkey/hooks/useGoToHotkeys';
+
+export const GoToHotkeyItemEffect = (props: {
+ hotkey: string;
+ pathToNavigateTo: string;
+}) => {
+ const { hotkey, pathToNavigateTo } = props;
+
+ useGoToHotkeys(hotkey, pathToNavigateTo);
+
+ return <>>;
+};
diff --git a/packages/twenty-front/src/modules/app/effect-components/GotoHotkeysEffect.tsx b/packages/twenty-front/src/modules/app/effect-components/GotoHotkeysEffect.tsx
new file mode 100644
index 000000000000..15d371f9f44a
--- /dev/null
+++ b/packages/twenty-front/src/modules/app/effect-components/GotoHotkeysEffect.tsx
@@ -0,0 +1,18 @@
+import { GoToHotkeyItemEffect } from '@/app/effect-components/GoToHotkeyItemEffect';
+import { useNonSystemActiveObjectMetadataItems } from '@/object-metadata/hooks/useNonSystemActiveObjectMetadataItems';
+import { useGoToHotkeys } from '@/ui/utilities/hotkey/hooks/useGoToHotkeys';
+
+export const GotoHotkeys = () => {
+ const { nonSystemActiveObjectMetadataItems } =
+ useNonSystemActiveObjectMetadataItems();
+
+ // Hardcoded since settings is static
+ useGoToHotkeys('s', '/settings/profile');
+
+ return nonSystemActiveObjectMetadataItems.map((objectMetadataItem) => (
+
+ ));
+};
diff --git a/packages/twenty-front/src/effect-components/PageChangeEffect.tsx b/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx
similarity index 90%
rename from packages/twenty-front/src/effect-components/PageChangeEffect.tsx
rename to packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx
index 05c99cc89d56..a8b05f4c0904 100644
--- a/packages/twenty-front/src/effect-components/PageChangeEffect.tsx
+++ b/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx
@@ -12,6 +12,8 @@ import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCapt
import { isCaptchaScriptLoadedState } from '@/captcha/states/isCaptchaScriptLoadedState';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { CommandType } from '@/command-menu/types/Command';
+import { useNonSystemActiveObjectMetadataItems } from '@/object-metadata/hooks/useNonSystemActiveObjectMetadataItems';
+import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { AppBasePath } from '@/types/AppBasePath';
@@ -43,7 +45,9 @@ export const PageChangeEffect = () => {
const eventTracker = useEventTracker();
- const { addToCommandMenu, setToInitialCommandMenu } = useCommandMenu();
+ const { addToCommandMenu, setObjectsInCommandMenu } = useCommandMenu();
+
+ const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const openCreateActivity = useOpenCreateActivityDrawer({
activityObjectNameSingular: CoreObjectNameSingular.Task,
@@ -146,8 +150,11 @@ export const PageChangeEffect = () => {
}
}, [isMatchingLocation, setHotkeyScope]);
+ const { nonSystemActiveObjectMetadataItems } =
+ useNonSystemActiveObjectMetadataItems();
+
useEffect(() => {
- setToInitialCommandMenu();
+ setObjectsInCommandMenu(nonSystemActiveObjectMetadataItems);
addToCommandMenu([
{
@@ -162,7 +169,13 @@ export const PageChangeEffect = () => {
}),
},
]);
- }, [addToCommandMenu, setToInitialCommandMenu, openCreateActivity]);
+ }, [
+ nonSystemActiveObjectMetadataItems,
+ addToCommandMenu,
+ setObjectsInCommandMenu,
+ openCreateActivity,
+ objectMetadataItems,
+ ]);
useEffect(() => {
setTimeout(() => {
diff --git a/packages/twenty-front/src/modules/app/utils/createAppRouter.tsx b/packages/twenty-front/src/modules/app/utils/createAppRouter.tsx
new file mode 100644
index 000000000000..0ddb70ac1a34
--- /dev/null
+++ b/packages/twenty-front/src/modules/app/utils/createAppRouter.tsx
@@ -0,0 +1,78 @@
+import { AppRouterProviders } from '@/app/components/AppRouterProviders';
+import { SettingsRoutes } from '@/app/components/SettingsRoutes';
+import { VerifyEffect } from '@/auth/components/VerifyEffect';
+import indexAppPath from '@/navigation/utils/indexAppPath';
+import { AppPath } from '@/types/AppPath';
+import { BlankLayout } from '@/ui/layout/page/BlankLayout';
+import { DefaultLayout } from '@/ui/layout/page/DefaultLayout';
+import {
+ createBrowserRouter,
+ createRoutesFromElements,
+ Route,
+} from 'react-router-dom';
+import { Authorize } from '~/pages/auth/Authorize';
+import { Invite } from '~/pages/auth/Invite';
+import { PasswordReset } from '~/pages/auth/PasswordReset';
+import { SignInUp } from '~/pages/auth/SignInUp';
+import { ImpersonateEffect } from '~/pages/impersonate/ImpersonateEffect';
+import { NotFound } from '~/pages/not-found/NotFound';
+import { RecordIndexPage } from '~/pages/object-record/RecordIndexPage';
+import { RecordShowPage } from '~/pages/object-record/RecordShowPage';
+import { ChooseYourPlan } from '~/pages/onboarding/ChooseYourPlan';
+import { CreateProfile } from '~/pages/onboarding/CreateProfile';
+import { CreateWorkspace } from '~/pages/onboarding/CreateWorkspace';
+import { InviteTeam } from '~/pages/onboarding/InviteTeam';
+import { PaymentSuccess } from '~/pages/onboarding/PaymentSuccess';
+import { SyncEmails } from '~/pages/onboarding/SyncEmails';
+
+export const createAppRouter = (
+ isBillingEnabled?: boolean,
+ isCRMMigrationEnabled?: boolean,
+ isServerlessFunctionSettingsEnabled?: boolean,
+) =>
+ createBrowserRouter(
+ createRoutesFromElements(
+ }
+ // To switch state to `loading` temporarily to enable us
+ // to set scroll position before the page is rendered
+ loader={async () => Promise.resolve(null)}
+ >
+ }>
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ }
+ />
+ >} />
+ } />
+ } />
+ } />
+
+ }
+ />
+ } />
+
+ }>
+ } />
+
+ ,
+ ),
+ );
diff --git a/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx b/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx
index 16c8bf7f4104..a7e1dc95e33d 100644
--- a/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx
+++ b/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx
@@ -2,7 +2,7 @@ import { action } from '@storybook/addon-actions';
import { Meta, StoryObj } from '@storybook/react';
import { expect, userEvent, within } from '@storybook/test';
import { useEffect } from 'react';
-import { useSetRecoilState } from 'recoil';
+import { useRecoilValue, useSetRecoilState } from 'recoil';
import { IconCheckbox, IconNotes } from 'twenty-ui';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
@@ -20,6 +20,7 @@ import {
} from '~/testing/mock-data/users';
import { sleep } from '~/utils/sleep';
+import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { CommandMenu } from '../CommandMenu';
const companiesMock = getCompaniesMock();
@@ -35,14 +36,21 @@ const meta: Meta = {
const setCurrentWorkspaceMember = useSetRecoilState(
currentWorkspaceMemberState,
);
- const { addToCommandMenu, setToInitialCommandMenu, openCommandMenu } =
+ const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
+
+ const { addToCommandMenu, setObjectsInCommandMenu, openCommandMenu } =
useCommandMenu();
setCurrentWorkspace(mockDefaultWorkspace);
setCurrentWorkspaceMember(mockedWorkspaceMemberData);
useEffect(() => {
- setToInitialCommandMenu();
+ const nonSystemActiveObjects = objectMetadataItems.filter(
+ (object) => !object.isSystem && object.isActive,
+ );
+
+ setObjectsInCommandMenu(nonSystemActiveObjects);
+
addToCommandMenu([
{
id: 'create-task',
@@ -62,7 +70,12 @@ const meta: Meta = {
},
]);
openCommandMenu();
- }, [addToCommandMenu, setToInitialCommandMenu, openCommandMenu]);
+ }, [
+ addToCommandMenu,
+ setObjectsInCommandMenu,
+ openCommandMenu,
+ objectMetadataItems,
+ ]);
return ;
},
diff --git a/packages/twenty-front/src/modules/command-menu/constants/CommandMenuCommands.ts b/packages/twenty-front/src/modules/command-menu/constants/CommandMenuCommands.ts
index 3c7f03168d98..711fbff881e9 100644
--- a/packages/twenty-front/src/modules/command-menu/constants/CommandMenuCommands.ts
+++ b/packages/twenty-front/src/modules/command-menu/constants/CommandMenuCommands.ts
@@ -8,8 +8,8 @@ import {
import { Command, CommandType } from '../types/Command';
-export const COMMAND_MENU_COMMANDS: Command[] = [
- {
+export const COMMAND_MENU_COMMANDS: { [key: string]: Command } = {
+ people: {
id: 'go-to-people',
to: '/objects/people',
label: 'Go to People',
@@ -18,7 +18,7 @@ export const COMMAND_MENU_COMMANDS: Command[] = [
secondHotKey: 'P',
Icon: IconUser,
},
- {
+ companies: {
id: 'go-to-companies',
to: '/objects/companies',
label: 'Go to Companies',
@@ -27,7 +27,7 @@ export const COMMAND_MENU_COMMANDS: Command[] = [
secondHotKey: 'C',
Icon: IconBuildingSkyscraper,
},
- {
+ opportunities: {
id: 'go-to-activities',
to: '/objects/opportunities',
label: 'Go to Opportunities',
@@ -36,7 +36,7 @@ export const COMMAND_MENU_COMMANDS: Command[] = [
secondHotKey: 'O',
Icon: IconTargetArrow,
},
- {
+ settings: {
id: 'go-to-settings',
to: '/settings/profile',
label: 'Go to Settings',
@@ -45,7 +45,7 @@ export const COMMAND_MENU_COMMANDS: Command[] = [
secondHotKey: 'S',
Icon: IconSettings,
},
- {
+ tasks: {
id: 'go-to-tasks',
to: '/objects/tasks',
label: 'Go to Tasks',
@@ -54,4 +54,4 @@ export const COMMAND_MENU_COMMANDS: Command[] = [
secondHotKey: 'T',
Icon: IconCheckbox,
},
-];
+};
diff --git a/packages/twenty-front/src/modules/command-menu/hooks/__test__/useCommandMenu.test.tsx b/packages/twenty-front/src/modules/command-menu/hooks/__test__/useCommandMenu.test.tsx
index e1ee5501382f..b0502e58374a 100644
--- a/packages/twenty-front/src/modules/command-menu/hooks/__test__/useCommandMenu.test.tsx
+++ b/packages/twenty-front/src/modules/command-menu/hooks/__test__/useCommandMenu.test.tsx
@@ -1,6 +1,6 @@
+import { renderHook } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
import { MemoryRouter } from 'react-router-dom';
-import { renderHook } from '@testing-library/react';
import { RecoilRoot, useRecoilState, useRecoilValue } from 'recoil';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
@@ -107,13 +107,39 @@ describe('useCommandMenu', () => {
expect(onClickMock).toHaveBeenCalledTimes(1);
});
- it('should setToInitialCommandMenu command menu', () => {
+ it('should setObjectsInCommandMenu command menu', () => {
const { result } = renderHooks();
act(() => {
- result.current.commandMenu.setToInitialCommandMenu();
+ result.current.commandMenu.setObjectsInCommandMenu([]);
+ });
+
+ expect(result.current.commandMenuCommands.length).toBe(1);
+
+ act(() => {
+ result.current.commandMenu.setObjectsInCommandMenu([
+ {
+ id: 'b88745ce-9021-4316-a018-8884e02d05ca',
+ nameSingular: 'task',
+ namePlural: 'tasks',
+ labelSingular: 'Task',
+ labelPlural: 'Tasks',
+ description: 'A task',
+ icon: 'IconCheckbox',
+ isCustom: false,
+ isRemote: false,
+ isActive: true,
+ isSystem: false,
+ createdAt: '2024-09-12T20:23:46.041Z',
+ updatedAt: '2024-09-13T08:36:53.426Z',
+ labelIdentifierFieldMetadataId:
+ 'ab7901eb-43e1-4dc7-8f3b-cdee2857eb9a',
+ imageIdentifierFieldMetadataId: null,
+ fields: [],
+ },
+ ]);
});
- expect(result.current.commandMenuCommands.length).toBe(5);
+ expect(result.current.commandMenuCommands.length).toBe(2);
});
});
diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts
index 1a8e085064e2..d19c314a1c8b 100644
--- a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts
+++ b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts
@@ -1,6 +1,6 @@
+import { isNonEmptyString } from '@sniptt/guards';
import { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
-import { isNonEmptyString } from '@sniptt/guards';
import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
@@ -9,10 +9,13 @@ import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousH
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { isDefined } from '~/utils/isDefined';
-import { COMMAND_MENU_COMMANDS } from '../constants/CommandMenuCommands';
+import { COMMAND_MENU_COMMANDS } from '@/command-menu/constants/CommandMenuCommands';
+import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
+import { ALL_ICONS } from '@ui/display/icon/providers/internal/AllIcons';
+import { sortByProperty } from '~/utils/array/sortByProperty';
import { commandMenuCommandsState } from '../states/commandMenuCommandsState';
import { isCommandMenuOpenedState } from '../states/isCommandMenuOpenedState';
-import { Command } from '../types/Command';
+import { Command, CommandType } from '../types/Command';
export const useCommandMenu = () => {
const navigate = useNavigate();
@@ -70,8 +73,27 @@ export const useCommandMenu = () => {
[setCommands],
);
- const setToInitialCommandMenu = () => {
- setCommands(COMMAND_MENU_COMMANDS);
+ const setObjectsInCommandMenu = (menuItems: ObjectMetadataItem[]) => {
+ const formattedItems = [
+ ...[
+ ...menuItems.map(
+ (item) =>
+ ({
+ id: item.id,
+ to: `/objects/${item.namePlural}`,
+ label: `Go to ${item.labelPlural}`,
+ type: CommandType.Navigate,
+ firstHotKey: 'G',
+ secondHotKey: item.labelPlural[0],
+ Icon: ALL_ICONS[
+ (item?.icon as keyof typeof ALL_ICONS) ?? 'IconArrowUpRight'
+ ],
+ }) as Command,
+ ),
+ ].sort(sortByProperty('label', 'asc')),
+ COMMAND_MENU_COMMANDS.settings,
+ ];
+ setCommands(formattedItems);
};
const onItemClick = useCallback(
@@ -96,6 +118,6 @@ export const useCommandMenu = () => {
toggleCommandMenu,
addToCommandMenu,
onItemClick,
- setToInitialCommandMenu,
+ setObjectsInCommandMenu,
};
};
diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useNonSystemActiveObjectMetadataItems.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useNonSystemActiveObjectMetadataItems.ts
new file mode 100644
index 000000000000..a33b80e1d125
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-metadata/hooks/useNonSystemActiveObjectMetadataItems.ts
@@ -0,0 +1,20 @@
+import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
+import { useMemo } from 'react';
+import { useRecoilValue } from 'recoil';
+
+export const useNonSystemActiveObjectMetadataItems = () => {
+ const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
+
+ const nonSystemActiveObjectMetadataItems = useMemo(
+ () =>
+ objectMetadataItems.filter(
+ (objectMetadataItem) =>
+ !objectMetadataItem.isSystem && objectMetadataItem.isActive,
+ ),
+ [objectMetadataItems],
+ );
+
+ return {
+ nonSystemActiveObjectMetadataItems,
+ };
+};
diff --git a/packages/twenty-front/src/utils/array/sortByProperty.ts b/packages/twenty-front/src/utils/array/sortByProperty.ts
new file mode 100644
index 000000000000..7cdb9b3b488b
--- /dev/null
+++ b/packages/twenty-front/src/utils/array/sortByProperty.ts
@@ -0,0 +1,18 @@
+export const sortByProperty =
+ (propertyName: K, sortBy: 'asc' | 'desc' = 'asc') =>
+ (objectA: T, objectB: T) => {
+ const a = sortBy === 'asc' ? objectA : objectB;
+ const b = sortBy === 'asc' ? objectB : objectA;
+
+ if (typeof a[propertyName] === 'string') {
+ return (a[propertyName] as string).localeCompare(
+ b[propertyName] as string,
+ );
+ } else if (typeof a[propertyName] === 'number') {
+ return (a[propertyName] as number) - (b[propertyName] as number);
+ } else {
+ throw new Error(
+ 'Property type not supported in sortByProperty, only string and number are supported',
+ );
+ }
+ };
From d5bd320b8d55c05343c2f0f44e2c4c66dade3791 Mon Sep 17 00:00:00 2001
From: Weiko
Date: Tue, 8 Oct 2024 17:40:48 +0200
Subject: [PATCH 10/10] Add DestroyMany to graphql query runner (#7507)
## Context
destroyMany was not implemented, this PR adds it
---
.../graphql-query-resolver.factory.ts | 3 +
.../graphql-query-runner.module.ts | 12 +-
.../graphql-query-runner.service.ts | 20 ++++
...ct-records-to-graphql-connection.helper.ts | 86 ++++++++------
...phql-query-create-many-resolver.service.ts | 12 +-
...hql-query-destroy-many-resolver.service.ts | 108 ++++++++++++++++++
...phql-query-destroy-one-resolver.service.ts | 74 ++++++++++--
...-query-find-duplicates-resolver.service.ts | 36 +++---
...raphql-query-find-many-resolver.service.ts | 10 +-
...graphql-query-find-one-resolver.service.ts | 12 +-
.../graphql-query-search-resolver.service.ts | 32 +++---
...phql-query-update-many-resolver.service.ts | 24 ++--
...aphql-query-update-one-resolver.service.ts | 12 +-
.../destroy-many-resolver.factory.ts | 18 +++
14 files changed, 341 insertions(+), 118 deletions(-)
create mode 100644 packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service.ts
diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/factories/graphql-query-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/factories/graphql-query-resolver.factory.ts
index ca2506d18aa9..e3ada8ac7cea 100644
--- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/factories/graphql-query-resolver.factory.ts
+++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/factories/graphql-query-resolver.factory.ts
@@ -8,6 +8,7 @@ import {
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { GraphqlQueryCreateManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service';
+import { GraphqlQueryDestroyManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service';
import { GraphqlQueryDestroyOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service';
import { GraphqlQueryFindDuplicatesResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service';
import { GraphqlQueryFindManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service';
@@ -37,6 +38,8 @@ export class GraphqlQueryResolverFactory {
return this.moduleRef.get(GraphqlQueryCreateManyResolverService);
case 'destroyOne':
return this.moduleRef.get(GraphqlQueryDestroyOneResolverService);
+ case 'destroyMany':
+ return this.moduleRef.get(GraphqlQueryDestroyManyResolverService);
case 'updateOne':
case 'deleteOne':
return this.moduleRef.get(GraphqlQueryUpdateOneResolverService);
diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.module.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.module.ts
index 8c689ff8a979..642e11c81b1f 100644
--- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.module.ts
+++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.module.ts
@@ -3,6 +3,7 @@ import { Module } from '@nestjs/common';
import { GraphqlQueryResolverFactory } from 'src/engine/api/graphql/graphql-query-runner/factories/graphql-query-resolver.factory';
import { GraphqlQueryRunnerService } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service';
import { GraphqlQueryCreateManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service';
+import { GraphqlQueryDestroyManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service';
import { GraphqlQueryDestroyOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service';
import { GraphqlQueryFindDuplicatesResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service';
import { GraphqlQueryFindManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service';
@@ -16,14 +17,15 @@ import { WorkspaceQueryRunnerModule } from 'src/engine/api/graphql/workspace-que
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
const graphqlQueryResolvers = [
- GraphqlQueryFindOneResolverService,
- GraphqlQueryFindManyResolverService,
- GraphqlQueryFindDuplicatesResolverService,
GraphqlQueryCreateManyResolverService,
+ GraphqlQueryDestroyManyResolverService,
GraphqlQueryDestroyOneResolverService,
- GraphqlQueryUpdateOneResolverService,
- GraphqlQueryUpdateManyResolverService,
+ GraphqlQueryFindDuplicatesResolverService,
+ GraphqlQueryFindManyResolverService,
+ GraphqlQueryFindOneResolverService,
GraphqlQuerySearchResolverService,
+ GraphqlQueryUpdateManyResolverService,
+ GraphqlQueryUpdateOneResolverService,
];
@Module({
diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service.ts
index 4e3475927ecd..d3d47daed171 100644
--- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service.ts
+++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service.ts
@@ -13,6 +13,7 @@ import {
CreateOneResolverArgs,
DeleteManyResolverArgs,
DeleteOneResolverArgs,
+ DestroyManyResolverArgs,
DestroyOneResolverArgs,
FindDuplicatesResolverArgs,
FindManyResolverArgs,
@@ -285,6 +286,25 @@ export class GraphqlQueryRunnerService {
return result;
}
+ @LogExecutionTime()
+ async destroyMany(
+ args: DestroyManyResolverArgs,
+ options: WorkspaceQueryRunnerOptions,
+ ): Promise {
+ const result = await this.executeQuery<
+ DestroyManyResolverArgs,
+ ObjectRecord[]
+ >('destroyMany', args, options);
+
+ this.apiEventEmitterService.emitDestroyEvents(
+ result,
+ options.authContext,
+ options.objectMetadataItem,
+ );
+
+ return result;
+ }
+
@LogExecutionTime()
public async restoreMany(
args: RestoreManyResolverArgs,
diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper.ts
index 5ccff3af114d..54220315345a 100644
--- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper.ts
+++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper.ts
@@ -27,25 +27,34 @@ export class ObjectRecordsToGraphqlConnectionHelper {
this.objectMetadataMap = objectMetadataMap;
}
- public createConnection(
- objectRecords: ObjectRecord[],
- objectName: string,
- take: number,
- totalCount: number,
- order: RecordOrderBy | undefined,
- hasNextPage: boolean,
- hasPreviousPage: boolean,
+ public createConnection({
+ objectRecords,
+ objectName,
+ take,
+ totalCount,
+ order,
+ hasNextPage,
+ hasPreviousPage,
depth = 0,
- ): IConnection {
+ }: {
+ objectRecords: ObjectRecord[];
+ objectName: string;
+ take: number;
+ totalCount: number;
+ order?: RecordOrderBy;
+ hasNextPage: boolean;
+ hasPreviousPage: boolean;
+ depth?: number;
+ }): IConnection {
const edges = (objectRecords ?? []).map((objectRecord) => ({
- node: this.processRecord(
+ node: this.processRecord({
objectRecord,
objectName,
take,
totalCount,
order,
depth,
- ),
+ }),
cursor: encodeCursor(objectRecord, order),
}));
@@ -61,14 +70,21 @@ export class ObjectRecordsToGraphqlConnectionHelper {
};
}
- public processRecord>(
- objectRecord: T,
- objectName: string,
- take: number,
- totalCount: number,
- order?: RecordOrderBy,
+ public processRecord>({
+ objectRecord,
+ objectName,
+ take,
+ totalCount,
+ order,
depth = 0,
- ): T {
+ }: {
+ objectRecord: T;
+ objectName: string;
+ take: number;
+ totalCount: number;
+ order?: RecordOrderBy;
+ depth?: number;
+ }): T {
if (depth >= CONNECTION_MAX_DEPTH) {
throw new GraphqlQueryRunnerException(
`Maximum depth of ${CONNECTION_MAX_DEPTH} reached`,
@@ -97,27 +113,31 @@ export class ObjectRecordsToGraphqlConnectionHelper {
if (isRelationFieldMetadataType(fieldMetadata.type)) {
if (Array.isArray(value)) {
- processedObjectRecord[key] = this.createConnection(
- value,
- getRelationObjectMetadata(fieldMetadata, this.objectMetadataMap)
- .nameSingular,
+ processedObjectRecord[key] = this.createConnection({
+ objectRecords: value,
+ objectName: getRelationObjectMetadata(
+ fieldMetadata,
+ this.objectMetadataMap,
+ ).nameSingular,
take,
- value.length,
+ totalCount: value.length,
order,
- false,
- false,
- depth + 1,
- );
+ hasNextPage: false,
+ hasPreviousPage: false,
+ depth: depth + 1,
+ });
} else if (isPlainObject(value)) {
- processedObjectRecord[key] = this.processRecord(
- value,
- getRelationObjectMetadata(fieldMetadata, this.objectMetadataMap)
- .nameSingular,
+ processedObjectRecord[key] = this.processRecord({
+ objectRecord: value,
+ objectName: getRelationObjectMetadata(
+ fieldMetadata,
+ this.objectMetadataMap,
+ ).nameSingular,
take,
totalCount,
order,
- depth + 1,
- );
+ depth: depth + 1,
+ });
}
} else if (isCompositeFieldMetadataType(fieldMetadata.type)) {
processedObjectRecord[key] = this.processCompositeField(
diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts
index aa8a81ce85a6..d5ee25e9be6d 100644
--- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts
+++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts
@@ -93,12 +93,12 @@ export class GraphqlQueryCreateManyResolverService
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap);
return upsertedRecords.map((record: ObjectRecord) =>
- typeORMObjectRecordsParser.processRecord(
- record,
- objectMetadataMapItem.nameSingular,
- 1,
- 1,
- ),
+ typeORMObjectRecordsParser.processRecord({
+ objectRecord: record,
+ objectName: objectMetadataMapItem.nameSingular,
+ take: 1,
+ totalCount: 1,
+ }),
);
}
diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service.ts
new file mode 100644
index 000000000000..04ceddf9ac9d
--- /dev/null
+++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service.ts
@@ -0,0 +1,108 @@
+import { Injectable } from '@nestjs/common';
+
+import graphqlFields from 'graphql-fields';
+
+import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface';
+import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
+import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
+import { DestroyManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
+
+import { QUERY_MAX_RECORDS } from 'src/engine/api/graphql/graphql-query-runner/constants/query-max-records.constant';
+import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
+import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
+import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper';
+import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
+import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
+
+@Injectable()
+export class GraphqlQueryDestroyManyResolverService
+ implements ResolverService
+{
+ constructor(
+ private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
+ ) {}
+
+ async resolve(
+ args: DestroyManyResolverArgs,
+ options: WorkspaceQueryRunnerOptions,
+ ): Promise {
+ const { authContext, objectMetadataMapItem, objectMetadataMap, info } =
+ options;
+ const dataSource =
+ await this.twentyORMGlobalManager.getDataSourceForWorkspace(
+ authContext.workspace.id,
+ );
+
+ const repository = dataSource.getRepository(
+ objectMetadataMapItem.nameSingular,
+ );
+
+ const graphqlQueryParser = new GraphqlQueryParser(
+ objectMetadataMapItem.fields,
+ objectMetadataMap,
+ );
+
+ const selectedFields = graphqlFields(info);
+
+ const { relations } = graphqlQueryParser.parseSelectedFields(
+ objectMetadataMapItem,
+ selectedFields,
+ );
+
+ const queryBuilder = repository.createQueryBuilder(
+ objectMetadataMapItem.nameSingular,
+ );
+
+ const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder(
+ queryBuilder,
+ objectMetadataMapItem.nameSingular,
+ args.filter,
+ );
+
+ const nonFormattedDeletedObjectRecords = await withFilterQueryBuilder
+ .delete()
+ .returning('*')
+ .execute();
+
+ const deletedRecords = formatResult(
+ nonFormattedDeletedObjectRecords.raw,
+ objectMetadataMapItem,
+ objectMetadataMap,
+ );
+
+ const processNestedRelationsHelper = new ProcessNestedRelationsHelper();
+
+ if (relations) {
+ await processNestedRelationsHelper.processNestedRelations(
+ objectMetadataMap,
+ objectMetadataMapItem,
+ deletedRecords,
+ relations,
+ QUERY_MAX_RECORDS,
+ authContext,
+ dataSource,
+ );
+ }
+
+ const typeORMObjectRecordsParser =
+ new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap);
+
+ return deletedRecords.map((record: ObjectRecord) =>
+ typeORMObjectRecordsParser.processRecord({
+ objectRecord: record,
+ objectName: objectMetadataMapItem.nameSingular,
+ take: 1,
+ totalCount: 1,
+ }),
+ );
+ }
+
+ async validate(
+ args: DestroyManyResolverArgs,
+ _options: WorkspaceQueryRunnerOptions,
+ ): Promise {
+ if (!args.filter) {
+ throw new Error('Filter is required');
+ }
+ }
+}
diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service.ts
index 3540dcbf9559..483222a49050 100644
--- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service.ts
+++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service.ts
@@ -1,14 +1,20 @@
import { Injectable } from '@nestjs/common';
+import graphqlFields from 'graphql-fields';
+
import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface';
import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { DestroyOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
+import { QUERY_MAX_RECORDS } from 'src/engine/api/graphql/graphql-query-runner/constants/query-max-records.constant';
import {
GraphqlQueryRunnerException,
GraphqlQueryRunnerExceptionCode,
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
+import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
+import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
+import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
@@ -24,19 +30,43 @@ export class GraphqlQueryDestroyOneResolverService
args: DestroyOneResolverArgs,
options: WorkspaceQueryRunnerOptions,
): Promise {
- const { authContext, objectMetadataMapItem, objectMetadataMap } = options;
- const repository =
- await this.twentyORMGlobalManager.getRepositoryForWorkspace(
+ const { authContext, objectMetadataMapItem, objectMetadataMap, info } =
+ options;
+ const dataSource =
+ await this.twentyORMGlobalManager.getDataSourceForWorkspace(
authContext.workspace.id,
- objectMetadataMapItem.nameSingular,
);
- const nonFormattedRecordBeforeDeletion = await repository.findOne({
- where: { id: args.id },
- withDeleted: true,
- });
+ const repository = dataSource.getRepository(
+ objectMetadataMapItem.nameSingular,
+ );
+
+ const graphqlQueryParser = new GraphqlQueryParser(
+ objectMetadataMapItem.fields,
+ objectMetadataMap,
+ );
+
+ const selectedFields = graphqlFields(info);
+
+ const { relations } = graphqlQueryParser.parseSelectedFields(
+ objectMetadataMapItem,
+ selectedFields,
+ );
- if (!nonFormattedRecordBeforeDeletion) {
+ const queryBuilder = repository.createQueryBuilder(
+ objectMetadataMapItem.nameSingular,
+ );
+
+ const nonFormattedDeletedObjectRecords = await queryBuilder
+ .where({
+ id: args.id,
+ })
+ .take(1)
+ .delete()
+ .returning('*')
+ .execute();
+
+ if (!nonFormattedDeletedObjectRecords.affected) {
throw new GraphqlQueryRunnerException(
'Record not found',
GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND,
@@ -44,14 +74,34 @@ export class GraphqlQueryDestroyOneResolverService
}
const recordBeforeDeletion = formatResult(
- [nonFormattedRecordBeforeDeletion],
+ nonFormattedDeletedObjectRecords.raw,
objectMetadataMapItem,
objectMetadataMap,
)[0];
- await repository.delete(args.id);
+ const processNestedRelationsHelper = new ProcessNestedRelationsHelper();
- return recordBeforeDeletion as ObjectRecord;
+ if (relations) {
+ await processNestedRelationsHelper.processNestedRelations(
+ objectMetadataMap,
+ objectMetadataMapItem,
+ [recordBeforeDeletion],
+ relations,
+ QUERY_MAX_RECORDS,
+ authContext,
+ dataSource,
+ );
+ }
+
+ const typeORMObjectRecordsParser =
+ new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap);
+
+ return typeORMObjectRecordsParser.processRecord({
+ objectRecord: recordBeforeDeletion,
+ objectName: objectMetadataMapItem.nameSingular,
+ take: 1,
+ totalCount: 1,
+ });
}
async validate(
diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service.ts
index 00561933043d..d3bc72fa8220 100644
--- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service.ts
+++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service.ts
@@ -88,15 +88,15 @@ export class GraphqlQueryFindDuplicatesResolverService
);
if (isEmpty(duplicateConditions)) {
- return typeORMObjectRecordsParser.createConnection(
- [],
- objectMetadataMapItem.nameSingular,
- 0,
- 0,
- [{ id: OrderByDirection.AscNullsFirst }],
- false,
- false,
- );
+ return typeORMObjectRecordsParser.createConnection({
+ objectRecords: [],
+ objectName: objectMetadataMapItem.nameSingular,
+ take: 0,
+ totalCount: 0,
+ order: [{ id: OrderByDirection.AscNullsFirst }],
+ hasNextPage: false,
+ hasPreviousPage: false,
+ });
}
const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder(
@@ -114,15 +114,15 @@ export class GraphqlQueryFindDuplicatesResolverService
objectMetadataMap,
);
- return typeORMObjectRecordsParser.createConnection(
- duplicates,
- objectMetadataMapItem.nameSingular,
- duplicates.length,
- duplicates.length,
- [{ id: OrderByDirection.AscNullsFirst }],
- false,
- false,
- );
+ return typeORMObjectRecordsParser.createConnection({
+ objectRecords: duplicates,
+ objectName: objectMetadataMapItem.nameSingular,
+ take: duplicates.length,
+ totalCount: duplicates.length,
+ order: [{ id: OrderByDirection.AscNullsFirst }],
+ hasNextPage: false,
+ hasPreviousPage: false,
+ });
}),
);
diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service.ts
index 85fdd3948274..9411c5502103 100644
--- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service.ts
+++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service.ts
@@ -176,15 +176,15 @@ export class GraphqlQueryFindManyResolverService
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap);
- const result = typeORMObjectRecordsParser.createConnection(
+ const result = typeORMObjectRecordsParser.createConnection({
objectRecords,
- objectMetadataMapItem.nameSingular,
- limit,
+ objectName: objectMetadataMapItem.nameSingular,
+ take: limit,
totalCount,
- orderByWithIdCondition,
+ order: orderByWithIdCondition,
hasNextPage,
hasPreviousPage,
- );
+ });
return result;
}
diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service.ts
index 164f30b68664..42c8daae8079 100644
--- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service.ts
+++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service.ts
@@ -113,12 +113,12 @@ export class GraphqlQueryFindOneResolverService
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap);
- return typeORMObjectRecordsParser.processRecord(
- objectRecords[0],
- objectMetadataMapItem.nameSingular,
- 1,
- 1,
- ) as ObjectRecord;
+ return typeORMObjectRecordsParser.processRecord({
+ objectRecord: objectRecords[0],
+ objectName: objectMetadataMapItem.nameSingular,
+ take: 1,
+ totalCount: 1,
+ }) as ObjectRecord;
}
async validate(
diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-search-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-search-resolver.service.ts
index fe9d86f6cd84..e6f3273b9c09 100644
--- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-search-resolver.service.ts
+++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-search-resolver.service.ts
@@ -44,15 +44,15 @@ export class GraphqlQuerySearchResolverService
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap);
if (!args.searchInput) {
- return typeORMObjectRecordsParser.createConnection(
- [],
- objectMetadataItem.nameSingular,
- 0,
- 0,
- [{ id: OrderByDirection.AscNullsFirst }],
- false,
- false,
- );
+ return typeORMObjectRecordsParser.createConnection({
+ objectRecords: [],
+ objectName: objectMetadataItem.nameSingular,
+ take: 0,
+ totalCount: 0,
+ order: [{ id: OrderByDirection.AscNullsFirst }],
+ hasNextPage: false,
+ hasPreviousPage: false,
+ });
}
const searchTerms = this.formatSearchTerms(args.searchInput);
@@ -76,15 +76,15 @@ export class GraphqlQuerySearchResolverService
const totalCount = await repository.count();
const order = undefined;
- return typeORMObjectRecordsParser.createConnection(
- objectRecords ?? [],
- objectMetadataItem.nameSingular,
- limit,
+ return typeORMObjectRecordsParser.createConnection({
+ objectRecords: objectRecords ?? [],
+ objectName: objectMetadataItem.nameSingular,
+ take: limit,
totalCount,
order,
- false,
- false,
- );
+ hasNextPage: false,
+ hasPreviousPage: false,
+ });
}
private formatSearchTerms(searchTerm: string) {
diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service.ts
index d8854223794b..270e6fd8196f 100644
--- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service.ts
+++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service.ts
@@ -65,15 +65,13 @@ export class GraphqlQueryUpdateManyResolverService
const data = formatData(args.data, objectMetadataMapItem);
- const result = await withFilterQueryBuilder
+ const nonFormattedUpdatedObjectRecords = await withFilterQueryBuilder
.update(data)
.returning('*')
.execute();
- const nonFormattedUpdatedObjectRecords = result.raw;
-
const updatedRecords = formatResult(
- nonFormattedUpdatedObjectRecords,
+ nonFormattedUpdatedObjectRecords.raw,
objectMetadataMapItem,
objectMetadataMap,
);
@@ -96,12 +94,12 @@ export class GraphqlQueryUpdateManyResolverService
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap);
return updatedRecords.map((record: ObjectRecord) =>
- typeORMObjectRecordsParser.processRecord(
- record,
- objectMetadataMapItem.nameSingular,
- 1,
- 1,
- ),
+ typeORMObjectRecordsParser.processRecord({
+ objectRecord: record,
+ objectName: objectMetadataMapItem.nameSingular,
+ take: 1,
+ totalCount: 1,
+ }),
);
}
@@ -110,6 +108,10 @@ export class GraphqlQueryUpdateManyResolverService
options: WorkspaceQueryRunnerOptions,
): Promise {
assertMutationNotOnRemoteObject(options.objectMetadataMapItem);
- args.filter?.id?.in?.forEach((id: string) => assertIsValidUuid(id));
+ if (!args.filter) {
+ throw new Error('Filter is required');
+ }
+
+ args.filter.id?.in?.forEach((id: string) => assertIsValidUuid(id));
}
}
diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service.ts
index 6fc4e1a72c58..8fe4396d2413 100644
--- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service.ts
+++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service.ts
@@ -103,12 +103,12 @@ export class GraphqlQueryUpdateOneResolverService
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap);
- return typeORMObjectRecordsParser.processRecord(
- updatedRecord,
- objectMetadataMapItem.nameSingular,
- 1,
- 1,
- );
+ return typeORMObjectRecordsParser.processRecord({
+ objectRecord: updatedRecord,
+ objectName: objectMetadataMapItem.nameSingular,
+ take: 1,
+ totalCount: 1,
+ });
}
async validate(
diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory.ts
index 2e6cf835effa..e90b93309c6f 100644
--- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory.ts
+++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory.ts
@@ -8,8 +8,11 @@ import {
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
+import { GraphqlQueryRunnerService } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service';
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service';
+import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
+import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
@Injectable()
export class DestroyManyResolverFactory
@@ -19,6 +22,8 @@ export class DestroyManyResolverFactory
constructor(
private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService,
+ private readonly featureFlagService: FeatureFlagService,
+ private readonly graphqlQueryRunnerService: GraphqlQueryRunnerService,
) {}
create(
@@ -38,6 +43,19 @@ export class DestroyManyResolverFactory
objectMetadataMapItem: internalContext.objectMetadataMapItem,
};
+ const isQueryRunnerTwentyORMEnabled =
+ await this.featureFlagService.isFeatureEnabled(
+ FeatureFlagKey.IsQueryRunnerTwentyORMEnabled,
+ internalContext.authContext.workspace.id,
+ );
+
+ if (isQueryRunnerTwentyORMEnabled) {
+ return await this.graphqlQueryRunnerService.destroyMany(
+ args,
+ options,
+ );
+ }
+
return await this.workspaceQueryRunnerService.destroyMany(
args,
options,