From e8d96cfd10fc4d6fd1bb9bf14dd6761e2f372120 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20M?= Date: Thu, 24 Oct 2024 15:38:52 +0200 Subject: [PATCH 01/75] feat: view groups (#7176) Fix #4244 and #4356 This pull request introduces the new "view groups" capability, enabling the reordering, hiding, and showing of columns in Kanban mode. The core enhancement includes the addition of a new entity named `ViewGroup`, which manages column behaviors and interactions. #### Key Changes: 1. **ViewGroup Entity**: The newly added `ViewGroup` entity is responsible for handling the organization and state of columns. This includes: - The ability to reorder columns. - The option to hide or show specific columns based on user preferences. #### Conclusion: This PR adds a significant new feature that enhances the flexibility of Kanban views through the `ViewGroup` entity. We'll later add the view group logic to table view too. --------- Co-authored-by: Lucas Bordeau --- packages/twenty-front/.storybook/main.ts | 1 + packages/twenty-front/.storybook/preview.tsx | 1 + packages/twenty-front/jest.config.ts | 2 +- .../types/CoreObjectNameSingular.ts | 1 + .../record-board/components/RecordBoard.tsx | 11 + .../components/RecordBoardHeader.tsx | 4 + .../hooks/internal/useRecordBoardStates.ts | 10 - .../internal/useSetRecordBoardColumns.ts | 13 +- .../components/RecordBoardColumn.tsx | 25 +-- .../RecordBoardColumnDropdownMenu.tsx | 10 +- .../components/RecordBoardColumnHeader.tsx | 31 ++- .../RecordBoardColumnHeaderWrapper.tsx | 14 +- .../contexts/RecordBoardColumnContext.ts | 6 +- .../record-board/scopes/RecordBoardScope.tsx | 4 +- .../RecordBoardScopeInternalContext.ts | 4 +- ...stRecordBoardColumnComponentFamilyState.ts | 7 - ...stRecordBoardColumnComponentFamilyState.ts | 7 - .../recordBoardColumnsComponentFamilyState.ts | 4 +- ...cordBoardColumnsComponentFamilySelector.ts | 83 +------- .../types/RecordBoardColumnDefinition.ts | 31 --- .../hooks/useRecordGroupActions.ts | 96 +++++++++ .../hooks/useRecordGroupReorder.ts | 59 ++++++ .../hooks/useRecordGroupVisibility.ts | 45 ++++ .../record-group/hooks/useRecordGroups.ts | 58 ++++++ .../recordGroupDefinitionsComponentState.ts | 11 + .../types/RecordGroupActions.ts} | 2 +- .../types/RecordGroupDefinition.ts | 17 ++ .../RecordIndexBoardColumnLoaderEffect.tsx | 4 - .../components/RecordIndexBoardDataLoader.tsx | 10 +- .../RecordIndexBoardDataLoaderEffect.tsx | 48 ++--- .../components/RecordIndexContainer.tsx | 187 ++++++++++------- .../RecordIndexPageKanbanAddButton.tsx | 4 +- .../RecordIndexPageKanbanAddMenuItem.tsx | 8 +- .../hooks/useLoadRecordIndexBoard.ts | 10 + .../hooks/useLoadRecordIndexBoardColumn.ts | 9 +- .../RecordIndexOptionsDropdownContent.tsx | 114 ++++++++++- ...olumnDefinitionsFromObjectMetadata.test.ts | 27 --- ...oardColumnDefinitionsFromObjectMetadata.ts | 71 ------- .../findAllViewsOperationSignatureFactory.ts | 1 + .../components/MenuItemDraggable.tsx | 5 +- .../ViewGroupsVisibilityDropdownSection.tsx | 192 ++++++++++++++++++ .../internal/usePersistViewGroupRecords.ts | 118 +++++++++++ .../hooks/useCreateViewFiltersAndSorts.ts | 34 ---- .../hooks/useCreateViewFromCurrentView.ts | 67 +++++- .../views/hooks/useSaveCurrentViewGroups.ts | 96 +++++++++ .../src/modules/views/types/GraphQLView.ts | 2 + .../src/modules/views/types/View.ts | 2 + .../src/modules/views/types/ViewGroup.ts | 8 + .../mapRecordGroupDefinitionsToViewGroups.ts | 17 ++ .../mapViewGroupsToRecordGroupDefinitions.ts | 79 +++++++ .../pages/object-record/RecordIndexPage.tsx | 37 ++-- .../__stories__/RecordIndexPage.stories.tsx | 2 +- .../__stories__/SettingsNewObject.stories.tsx | 2 +- .../standard-objects-prefill-data/view.ts | 27 +++ .../views/opportunity-by-stage.view.ts | 47 +++++ .../views/tasks-by-status.view.ts | 29 +++ .../constants/standard-field-ids.ts | 9 + .../constants/standard-object-ids.ts | 1 + .../standard-objects/index.ts | 2 + .../view-group.workspace-entity.ts | 77 +++++++ .../standard-objects/view.workspace-entity.ts | 13 ++ 61 files changed, 1408 insertions(+), 508 deletions(-) delete mode 100644 packages/twenty-front/src/modules/object-record/record-board/states/isFirstRecordBoardColumnComponentFamilyState.ts delete mode 100644 packages/twenty-front/src/modules/object-record/record-board/states/isLastRecordBoardColumnComponentFamilyState.ts delete mode 100644 packages/twenty-front/src/modules/object-record/record-board/types/RecordBoardColumnDefinition.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupActions.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupReorder.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupVisibility.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroups.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-group/states/recordGroupDefinitionsComponentState.ts rename packages/twenty-front/src/modules/object-record/{record-board/types/RecordBoardColumnAction.ts => record-group/types/RecordGroupActions.ts} (78%) create mode 100644 packages/twenty-front/src/modules/object-record/record-group/types/RecordGroupDefinition.ts delete mode 100644 packages/twenty-front/src/modules/object-record/utils/__tests__/computeRecordBoardColumnDefinitionsFromObjectMetadata.test.ts delete mode 100644 packages/twenty-front/src/modules/object-record/utils/computeRecordBoardColumnDefinitionsFromObjectMetadata.ts create mode 100644 packages/twenty-front/src/modules/views/components/ViewGroupsVisibilityDropdownSection.tsx create mode 100644 packages/twenty-front/src/modules/views/hooks/internal/usePersistViewGroupRecords.ts delete mode 100644 packages/twenty-front/src/modules/views/hooks/useCreateViewFiltersAndSorts.ts create mode 100644 packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewGroups.ts create mode 100644 packages/twenty-front/src/modules/views/types/ViewGroup.ts create mode 100644 packages/twenty-front/src/modules/views/utils/mapRecordGroupDefinitionsToViewGroups.ts create mode 100644 packages/twenty-front/src/modules/views/utils/mapViewGroupsToRecordGroupDefinitions.ts create mode 100644 packages/twenty-server/src/modules/view/standard-objects/view-group.workspace-entity.ts diff --git a/packages/twenty-front/.storybook/main.ts b/packages/twenty-front/.storybook/main.ts index 04dd231aaa..6d34593abb 100644 --- a/packages/twenty-front/.storybook/main.ts +++ b/packages/twenty-front/.storybook/main.ts @@ -57,5 +57,6 @@ const config: StorybookConfig = { }, }); }, + logLevel: 'error', }; export default config; diff --git a/packages/twenty-front/.storybook/preview.tsx b/packages/twenty-front/.storybook/preview.tsx index 1d67634e2a..d35b87e856 100644 --- a/packages/twenty-front/.storybook/preview.tsx +++ b/packages/twenty-front/.storybook/preview.tsx @@ -29,6 +29,7 @@ initialize({ with payload ${JSON.stringify(requestBody)}\n This request should be mocked with MSW`); }, + quiet: true, }); const preview: Preview = { diff --git a/packages/twenty-front/jest.config.ts b/packages/twenty-front/jest.config.ts index c3e8ab4148..5cb6b789c8 100644 --- a/packages/twenty-front/jest.config.ts +++ b/packages/twenty-front/jest.config.ts @@ -27,7 +27,7 @@ const jestConfig: JestConfigWithTsJest = { global: { statements: 59, lines: 55, - functions: 49, + functions: 48, }, }, collectCoverageFrom: ['/src/**/*.ts'], diff --git a/packages/twenty-front/src/modules/object-metadata/types/CoreObjectNameSingular.ts b/packages/twenty-front/src/modules/object-metadata/types/CoreObjectNameSingular.ts index 8ac533f76a..03164e28ac 100644 --- a/packages/twenty-front/src/modules/object-metadata/types/CoreObjectNameSingular.ts +++ b/packages/twenty-front/src/modules/object-metadata/types/CoreObjectNameSingular.ts @@ -25,6 +25,7 @@ export enum CoreObjectNameSingular { ViewField = 'viewField', ViewFilter = 'viewFilter', ViewSort = 'viewSort', + ViewGroup = 'viewGroup', Webhook = 'webhook', WorkspaceMember = 'workspaceMember', MessageThreadSubscriber = 'messageThreadSubscriber', diff --git a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx index 592f2d7b4a..b08fe9e751 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx @@ -31,16 +31,21 @@ const StyledContainer = styled.div` const StyledColumnContainer = styled.div` display: flex; + & > *:not(:first-child) { + border-left: 1px solid ${({ theme }) => theme.border.color.light}; + } `; const StyledContainerContainer = styled.div` display: flex; flex-direction: column; + height: 100%; `; const StyledBoardContentContainer = styled.div` display: flex; flex-direction: column; + height: calc(100% - 48px); `; const RecordBoardScrollRestoreEffect = () => { @@ -137,6 +142,12 @@ export const RecordBoard = () => { ], ); + // FixMe: Check if we really need this as it depends on the times it takes to update the view groups + // if (isPersistingViewGroups) { + // // TODO: Add skeleton state + // return null; + // } + return ( *:not(:first-child) { + border-left: 1px solid ${({ theme }) => theme.border.color.light}; + } `; export const RecordBoardHeader = () => { diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useRecordBoardStates.ts b/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useRecordBoardStates.ts index aece94d483..65428a0197 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useRecordBoardStates.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useRecordBoardStates.ts @@ -1,6 +1,4 @@ import { RecordBoardScopeInternalContext } from '@/object-record/record-board/scopes/scope-internal-context/RecordBoardScopeInternalContext'; -import { isFirstRecordBoardColumnComponentFamilyState } from '@/object-record/record-board/states/isFirstRecordBoardColumnComponentFamilyState'; -import { isLastRecordBoardColumnComponentFamilyState } from '@/object-record/record-board/states/isLastRecordBoardColumnComponentFamilyState'; import { isRecordBoardCardSelectedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardSelectedComponentFamilyState'; import { isRecordBoardCompactModeActiveComponentState } from '@/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState'; import { isRecordBoardFetchingRecordsByColumnFamilyState } from '@/object-record/record-board/states/isRecordBoardFetchingRecordsByColumnFamilyState'; @@ -51,14 +49,6 @@ export const useRecordBoardStates = (recordBoardId?: string) => { recordBoardColumnIdsComponentState, scopeId, ), - isFirstColumnFamilyState: extractComponentFamilyState( - isFirstRecordBoardColumnComponentFamilyState, - scopeId, - ), - isLastColumnFamilyState: extractComponentFamilyState( - isLastRecordBoardColumnComponentFamilyState, - scopeId, - ), columnsFamilySelector: extractComponentFamilyState( recordBoardColumnsComponentFamilySelector, scopeId, diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useSetRecordBoardColumns.ts b/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useSetRecordBoardColumns.ts index d330a476d6..712fc2571c 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useSetRecordBoardColumns.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useSetRecordBoardColumns.ts @@ -1,8 +1,8 @@ import { useRecoilCallback } from 'recoil'; import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; -import { RecordBoardColumnDefinition } from '@/object-record/record-board/types/RecordBoardColumnDefinition'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; +import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; export const useSetRecordBoardColumns = (recordBoardId?: string) => { const { scopeId, columnIdsState, columnsFamilySelector } = @@ -10,21 +10,20 @@ export const useSetRecordBoardColumns = (recordBoardId?: string) => { const setColumns = useRecoilCallback( ({ set, snapshot }) => - (columns: RecordBoardColumnDefinition[]) => { + (columns: RecordGroupDefinition[]) => { const currentColumnsIds = snapshot .getLoadable(columnIdsState) .getValue(); - const columnIds = columns.map(({ id }) => id); + const columnIds = columns + .filter(({ isVisible }) => isVisible) + .map(({ id }) => id); if (isDeeplyEqual(currentColumnsIds, columnIds)) { return; } - set( - columnIdsState, - columns.map((column) => column.id), - ); + set(columnIdsState, columnIds); columns.forEach((column) => { const currentColumn = snapshot diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumn.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumn.tsx index 8fe9782442..489798e16c 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumn.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumn.tsx @@ -6,11 +6,8 @@ import { useRecordBoardStates } from '@/object-record/record-board/hooks/interna import { RecordBoardColumnCardsContainer } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnCardsContainer'; import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext'; -const StyledColumn = styled.div<{ isFirstColumn: boolean }>` +const StyledColumn = styled.div` background-color: ${({ theme }) => theme.background.primary}; - border-left: 1px solid - ${({ theme, isFirstColumn }) => - isFirstColumn ? 'none' : theme.border.color.light}; display: flex; flex-direction: column; max-width: 200px; @@ -32,24 +29,12 @@ type RecordBoardColumnProps = { export const RecordBoardColumn = ({ recordBoardColumnId, }: RecordBoardColumnProps) => { - const { - isFirstColumnFamilyState, - isLastColumnFamilyState, - columnsFamilySelector, - recordIdsByColumnIdFamilyState, - } = useRecordBoardStates(); + const { columnsFamilySelector, recordIdsByColumnIdFamilyState } = + useRecordBoardStates(); const columnDefinition = useRecoilValue( columnsFamilySelector(recordBoardColumnId), ); - const isFirstColumn = useRecoilValue( - isFirstColumnFamilyState(recordBoardColumnId), - ); - - const isLastColumn = useRecoilValue( - isLastColumnFamilyState(recordBoardColumnId), - ); - const recordIds = useRecoilValue( recordIdsByColumnIdFamilyState(recordBoardColumnId), ); @@ -62,8 +47,6 @@ export const RecordBoardColumn = ({ {(droppableProvided) => ( - + { const boardColumnMenuRef = useRef(null); + const recordGroupActions = useRecordGroupActions(); + const closeMenu = useCallback(() => { onClose(); }, [onClose]); @@ -34,13 +36,11 @@ export const RecordBoardColumnDropdownMenu = ({ callback: closeMenu, }); - const { columnDefinition } = useContext(RecordBoardColumnContext); - return ( - {columnDefinition.actions.map((action) => ( + {recordGroupActions.map((action) => ( { diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeader.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeader.tsx index 1f25864dd6..1d91fc5406 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeader.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeader.tsx @@ -10,7 +10,7 @@ import { RecordBoardColumnContext } from '@/object-record/record-board/record-bo import { useColumnNewCardActions } from '@/object-record/record-board/record-board-column/hooks/useColumnNewCardActions'; import { useIsOpportunitiesCompanyFieldDisabled } from '@/object-record/record-board/record-board-column/hooks/useIsOpportunitiesCompanyFieldDisabled'; import { RecordBoardColumnHotkeyScope } from '@/object-record/record-board/types/BoardColumnHotkeyScope'; -import { RecordBoardColumnDefinitionType } from '@/object-record/record-board/types/RecordBoardColumnDefinition'; +import { RecordGroupDefinitionType } from '@/object-record/record-group/types/RecordGroupDefinition'; import { SingleEntitySelect } from '@/object-record/relation-picker/components/SingleEntitySelect'; import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; @@ -59,11 +59,8 @@ const StyledRightContainer = styled.div` display: flex; `; -const StyledColumn = styled.div<{ isFirstColumn: boolean }>` +const StyledColumn = styled.div` background-color: ${({ theme }) => theme.background.primary}; - border-left: 1px solid - ${({ theme, isFirstColumn }) => - isFirstColumn ? 'none' : theme.border.color.light}; display: flex; flex-direction: column; max-width: 200px; @@ -75,7 +72,7 @@ const StyledColumn = styled.div<{ isFirstColumn: boolean }>` `; export const RecordBoardColumnHeader = () => { - const { columnDefinition, isFirstColumn, recordCount } = useContext( + const { columnDefinition, recordCount } = useContext( RecordBoardColumnContext, ); const [isBoardColumnMenuOpen, setIsBoardColumnMenuOpen] = useState(false); @@ -120,7 +117,7 @@ export const RecordBoardColumnHeader = () => { !isOpportunitiesCompanyFieldDisabled; return ( - + setIsHeaderHovered(true)} onMouseLeave={() => setIsHeaderHovered(false)} @@ -130,18 +127,18 @@ export const RecordBoardColumnHeader = () => { { {isHeaderHovered && ( - {columnDefinition.actions.length > 0 && ( - - )} + { - {isBoardColumnMenuOpen && columnDefinition.actions.length > 0 && ( + {isBoardColumnMenuOpen && ( { - const { - isFirstColumnFamilyState, - isLastColumnFamilyState, - columnsFamilySelector, - recordIdsByColumnIdFamilyState, - } = useRecordBoardStates(); + const { columnsFamilySelector, recordIdsByColumnIdFamilyState } = + useRecordBoardStates(); const columnDefinition = useRecoilValue(columnsFamilySelector(columnId)); - const isFirstColumn = useRecoilValue(isFirstColumnFamilyState(columnId)); - - const isLastColumn = useRecoilValue(isLastColumnFamilyState(columnId)); - const recordIds = useRecoilValue(recordIdsByColumnIdFamilyState(columnId)); if (!isDefined(columnDefinition)) { @@ -36,8 +28,6 @@ export const RecordBoardColumnHeaderWrapper = ({ value={{ columnId, columnDefinition: columnDefinition, - isFirstColumn: isFirstColumn, - isLastColumn: isLastColumn, recordCount: recordIds.length, recordIds, }} diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext.ts b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext.ts index f37c5c5cb3..1d000084f3 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext.ts @@ -1,11 +1,9 @@ import { createContext } from 'react'; -import { RecordBoardColumnDefinition } from '@/object-record/record-board/types/RecordBoardColumnDefinition'; +import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; type RecordBoardColumnContextProps = { - columnDefinition: RecordBoardColumnDefinition; - isFirstColumn: boolean; - isLastColumn: boolean; + columnDefinition: RecordGroupDefinition; recordCount: number; columnId: string; recordIds: string[]; diff --git a/packages/twenty-front/src/modules/object-record/record-board/scopes/RecordBoardScope.tsx b/packages/twenty-front/src/modules/object-record/record-board/scopes/RecordBoardScope.tsx index 37c6051855..2f61769ed1 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/scopes/RecordBoardScope.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/scopes/RecordBoardScope.tsx @@ -1,15 +1,15 @@ import { ReactNode } from 'react'; import { RecordBoardScopeInternalContext } from '@/object-record/record-board/scopes/scope-internal-context/RecordBoardScopeInternalContext'; -import { RecordBoardColumnDefinition } from '@/object-record/record-board/types/RecordBoardColumnDefinition'; import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; +import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; type RecordBoardScopeProps = { children: ReactNode; recordBoardScopeId: string; onFieldsChange: (fields: FieldDefinition[]) => void; - onColumnsChange: (column: RecordBoardColumnDefinition[]) => void; + onColumnsChange: (column: RecordGroupDefinition[]) => void; }; /** @deprecated */ diff --git a/packages/twenty-front/src/modules/object-record/record-board/scopes/scope-internal-context/RecordBoardScopeInternalContext.ts b/packages/twenty-front/src/modules/object-record/record-board/scopes/scope-internal-context/RecordBoardScopeInternalContext.ts index 44ac1e08e1..330ff18aba 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/scopes/scope-internal-context/RecordBoardScopeInternalContext.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/scopes/scope-internal-context/RecordBoardScopeInternalContext.ts @@ -1,12 +1,12 @@ -import { RecordBoardColumnDefinition } from '@/object-record/record-board/types/RecordBoardColumnDefinition'; import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; +import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; import { createScopeInternalContext } from '@/ui/utilities/recoil-scope/scopes-internal/utils/createScopeInternalContext'; import { RecoilComponentStateKey } from '@/ui/utilities/state/component-state/types/RecoilComponentStateKey'; type RecordBoardScopeInternalContextProps = RecoilComponentStateKey & { onFieldsChange: (fields: FieldDefinition[]) => void; - onColumnsChange: (column: RecordBoardColumnDefinition[]) => void; + onColumnsChange: (column: RecordGroupDefinition[]) => void; }; export const RecordBoardScopeInternalContext = diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/isFirstRecordBoardColumnComponentFamilyState.ts b/packages/twenty-front/src/modules/object-record/record-board/states/isFirstRecordBoardColumnComponentFamilyState.ts deleted file mode 100644 index bef4219700..0000000000 --- a/packages/twenty-front/src/modules/object-record/record-board/states/isFirstRecordBoardColumnComponentFamilyState.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { createComponentFamilyState } from '@/ui/utilities/state/component-state/utils/createComponentFamilyState'; - -export const isFirstRecordBoardColumnComponentFamilyState = - createComponentFamilyState({ - key: 'isFirstRecordBoardColumnComponentFamilyState', - defaultValue: false, - }); diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/isLastRecordBoardColumnComponentFamilyState.ts b/packages/twenty-front/src/modules/object-record/record-board/states/isLastRecordBoardColumnComponentFamilyState.ts deleted file mode 100644 index 9174fba1ca..0000000000 --- a/packages/twenty-front/src/modules/object-record/record-board/states/isLastRecordBoardColumnComponentFamilyState.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { createComponentFamilyState } from '@/ui/utilities/state/component-state/utils/createComponentFamilyState'; - -export const isLastRecordBoardColumnComponentFamilyState = - createComponentFamilyState({ - key: 'isLastRecordBoardColumnComponentFamilyState', - defaultValue: false, - }); diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardColumnsComponentFamilyState.ts b/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardColumnsComponentFamilyState.ts index c2b6cc1cfe..1530820d80 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardColumnsComponentFamilyState.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardColumnsComponentFamilyState.ts @@ -1,8 +1,8 @@ -import { RecordBoardColumnDefinition } from '@/object-record/record-board/types/RecordBoardColumnDefinition'; +import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; import { createComponentFamilyState } from '@/ui/utilities/state/component-state/utils/createComponentFamilyState'; export const recordBoardColumnsComponentFamilyState = - createComponentFamilyState({ + createComponentFamilyState({ key: 'recordBoardColumnsComponentFamilyState', defaultValue: undefined, }); diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/selectors/recordBoardColumnsComponentFamilySelector.ts b/packages/twenty-front/src/modules/object-record/record-board/states/selectors/recordBoardColumnsComponentFamilySelector.ts index 22dd7aa8df..fefff84510 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/states/selectors/recordBoardColumnsComponentFamilySelector.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/states/selectors/recordBoardColumnsComponentFamilySelector.ts @@ -1,19 +1,9 @@ -import { isUndefined } from '@sniptt/guards'; - -import { isFirstRecordBoardColumnComponentFamilyState } from '@/object-record/record-board/states/isFirstRecordBoardColumnComponentFamilyState'; -import { isLastRecordBoardColumnComponentFamilyState } from '@/object-record/record-board/states/isLastRecordBoardColumnComponentFamilyState'; -import { recordBoardColumnIdsComponentState } from '@/object-record/record-board/states/recordBoardColumnIdsComponentState'; import { recordBoardColumnsComponentFamilyState } from '@/object-record/record-board/states/recordBoardColumnsComponentFamilyState'; -import { RecordBoardColumnDefinition } from '@/object-record/record-board/types/RecordBoardColumnDefinition'; -import { guardRecoilDefaultValue } from '@/ui/utilities/recoil-scope/utils/guardRecoilDefaultValue'; import { createComponentFamilySelector } from '@/ui/utilities/state/component-state/utils/createComponentFamilySelector'; -import { isDefined } from '~/utils/isDefined'; +import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; export const recordBoardColumnsComponentFamilySelector = - createComponentFamilySelector< - RecordBoardColumnDefinition | undefined, - string - >({ + createComponentFamilySelector({ key: 'recordBoardColumnsComponentFamilySelector', get: ({ @@ -39,7 +29,7 @@ export const recordBoardColumnsComponentFamilySelector = scopeId: string; familyKey: string; }) => - ({ set, get }, newColumn) => { + ({ set }, newColumn) => { set( recordBoardColumnsComponentFamilyState({ scopeId, @@ -47,72 +37,5 @@ export const recordBoardColumnsComponentFamilySelector = }), newColumn, ); - - if (guardRecoilDefaultValue(newColumn)) return; - - const columnIds = get(recordBoardColumnIdsComponentState({ scopeId })); - - const columns = columnIds - .map((columnId) => { - return get( - recordBoardColumnsComponentFamilyState({ - scopeId, - familyKey: columnId, - }), - ); - }) - .filter(isDefined); - - const lastColumn = [...columns].sort( - (a, b) => b.position - a.position, - )[0]; - - const firstColumn = [...columns].sort( - (a, b) => a.position - b.position, - )[0]; - - if (!newColumn) { - return; - } - - if (!lastColumn || newColumn.position > lastColumn.position) { - set( - isLastRecordBoardColumnComponentFamilyState({ - scopeId, - familyKey: columnId, - }), - true, - ); - - if (!isUndefined(lastColumn)) { - set( - isLastRecordBoardColumnComponentFamilyState({ - scopeId, - familyKey: lastColumn.id, - }), - false, - ); - } - } - - if (!firstColumn || newColumn.position < firstColumn.position) { - set( - isFirstRecordBoardColumnComponentFamilyState({ - scopeId, - familyKey: columnId, - }), - true, - ); - - if (!isUndefined(firstColumn)) { - set( - isFirstRecordBoardColumnComponentFamilyState({ - scopeId, - familyKey: firstColumn.id, - }), - false, - ); - } - } }, }); diff --git a/packages/twenty-front/src/modules/object-record/record-board/types/RecordBoardColumnDefinition.ts b/packages/twenty-front/src/modules/object-record/record-board/types/RecordBoardColumnDefinition.ts deleted file mode 100644 index b5e443b0fd..0000000000 --- a/packages/twenty-front/src/modules/object-record/record-board/types/RecordBoardColumnDefinition.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { ThemeColor } from 'twenty-ui'; - -import { RecordBoardColumnAction } from '@/object-record/record-board/types/RecordBoardColumnAction'; - -export const enum RecordBoardColumnDefinitionType { - Value = 'value', - NoValue = 'no-value', -} - -export type RecordBoardColumnDefinitionNoValue = { - id: 'no-value'; - type: RecordBoardColumnDefinitionType.NoValue; - title: 'No Value'; - position: number; - value: null; - actions: RecordBoardColumnAction[]; -}; - -export type RecordBoardColumnDefinitionValue = { - id: string; - type: RecordBoardColumnDefinitionType.Value; - title: string; - value: string; - color: ThemeColor; - position: number; - actions: RecordBoardColumnAction[]; -}; - -export type RecordBoardColumnDefinition = - | RecordBoardColumnDefinitionValue - | RecordBoardColumnDefinitionNoValue; diff --git a/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupActions.ts b/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupActions.ts new file mode 100644 index 0000000000..2fa75b470f --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupActions.ts @@ -0,0 +1,96 @@ +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { getFieldSlug } from '@/object-metadata/utils/getFieldSlug'; +import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug'; +import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext'; +import { useRecordGroups } from '@/object-record/record-group/hooks/useRecordGroups'; +import { useRecordGroupVisibility } from '@/object-record/record-group/hooks/useRecordGroupVisibility'; +import { RecordGroupAction } from '@/object-record/record-group/types/RecordGroupActions'; +import { RecordGroupDefinitionType } from '@/object-record/record-group/types/RecordGroupDefinition'; +import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; +import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; +import { useCallback, useContext, useMemo } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { useSetRecoilState } from 'recoil'; +import { IconEyeOff, IconSettings, isDefined } from 'twenty-ui'; + +export const useRecordGroupActions = () => { + const navigate = useNavigate(); + const location = useLocation(); + + const { objectNameSingular, recordIndexId } = useContext( + RecordIndexRootPropsContext, + ); + + const { columnDefinition: recordGroupDefinition } = useContext( + RecordBoardColumnContext, + ); + + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular, + }); + + const { viewGroupFieldMetadataItem } = useRecordGroups({ + objectNameSingular, + }); + + const { handleVisibilityChange: handleRecordGroupVisibilityChange } = + useRecordGroupVisibility({ + viewBarId: recordIndexId, + }); + + const setNavigationMemorizedUrl = useSetRecoilState( + navigationMemorizedUrlState, + ); + + const navigateToSelectSettings = useCallback(() => { + setNavigationMemorizedUrl(location.pathname + location.search); + + if (!isDefined(viewGroupFieldMetadataItem)) { + throw new Error('viewGroupFieldMetadataItem is not a non-empty string'); + } + + const settingsPath = `/settings/objects/${getObjectSlug(objectMetadataItem)}/${getFieldSlug(viewGroupFieldMetadataItem)}`; + + navigate(settingsPath); + }, [ + setNavigationMemorizedUrl, + location.pathname, + location.search, + navigate, + objectMetadataItem, + viewGroupFieldMetadataItem, + ]); + + const recordGroupActions: RecordGroupAction[] = useMemo( + () => + [ + { + id: 'edit', + label: 'Edit', + icon: IconSettings, + position: 0, + callback: () => { + navigateToSelectSettings(); + }, + }, + recordGroupDefinition.type !== RecordGroupDefinitionType.NoValue + ? { + id: 'hide', + label: 'Hide', + icon: IconEyeOff, + position: 1, + callback: () => { + handleRecordGroupVisibilityChange(recordGroupDefinition); + }, + } + : undefined, + ].filter(isDefined), + [ + handleRecordGroupVisibilityChange, + navigateToSelectSettings, + recordGroupDefinition, + ], + ); + + return recordGroupActions; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupReorder.ts b/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupReorder.ts new file mode 100644 index 0000000000..97151a5838 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupReorder.ts @@ -0,0 +1,59 @@ +import { OnDragEndResponder } from '@hello-pangea/dnd'; +import { useCallback } from 'react'; + +import { useRecordGroups } from '@/object-record/record-group/hooks/useRecordGroups'; +import { recordGroupDefinitionsComponentState } from '@/object-record/record-group/states/recordGroupDefinitionsComponentState'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; +import { useSaveCurrentViewGroups } from '@/views/hooks/useSaveCurrentViewGroups'; +import { mapRecordGroupDefinitionsToViewGroups } from '@/views/utils/mapRecordGroupDefinitionsToViewGroups'; +import { moveArrayItem } from '~/utils/array/moveArrayItem'; +import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; + +type UseRecordGroupHandlersParams = { + objectNameSingular: string; + viewBarId: string; +}; + +export const useRecordGroupReorder = ({ + objectNameSingular, + viewBarId, +}: UseRecordGroupHandlersParams) => { + const setRecordGroupDefinitions = useSetRecoilComponentStateV2( + recordGroupDefinitionsComponentState, + ); + + const { visibleRecordGroups } = useRecordGroups({ + objectNameSingular, + }); + + const { saveViewGroups } = useSaveCurrentViewGroups(viewBarId); + + const handleOrderChange: OnDragEndResponder = useCallback( + (result) => { + if (!result.destination) { + return; + } + + const reorderedVisibleBoardGroups = moveArrayItem(visibleRecordGroups, { + fromIndex: result.source.index - 1, + toIndex: result.destination.index - 1, + }); + + if (isDeeplyEqual(visibleRecordGroups, reorderedVisibleBoardGroups)) + return; + + const updatedGroups = [...reorderedVisibleBoardGroups].map( + (group, index) => ({ ...group, position: index }), + ); + + setRecordGroupDefinitions(updatedGroups); + saveViewGroups(mapRecordGroupDefinitionsToViewGroups(updatedGroups)); + }, + [saveViewGroups, setRecordGroupDefinitions, visibleRecordGroups], + ); + + return { + visibleRecordGroups, + handleOrderChange, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupVisibility.ts b/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupVisibility.ts new file mode 100644 index 0000000000..c9ecbd7760 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupVisibility.ts @@ -0,0 +1,45 @@ +import { useCallback } from 'react'; + +import { recordGroupDefinitionsComponentState } from '@/object-record/record-group/states/recordGroupDefinitionsComponentState'; +import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; +import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; +import { useSaveCurrentViewGroups } from '@/views/hooks/useSaveCurrentViewGroups'; +import { mapRecordGroupDefinitionsToViewGroups } from '@/views/utils/mapRecordGroupDefinitionsToViewGroups'; + +type UseRecordGroupVisibilityParams = { + viewBarId: string; +}; + +export const useRecordGroupVisibility = ({ + viewBarId, +}: UseRecordGroupVisibilityParams) => { + const [recordGroupDefinitions, setRecordGroupDefinitions] = + useRecoilComponentStateV2(recordGroupDefinitionsComponentState); + + const { saveViewGroups } = useSaveCurrentViewGroups(viewBarId); + + const handleVisibilityChange = useCallback( + async (updatedRecordGroupDefinition: RecordGroupDefinition) => { + const updatedRecordGroupDefinitions = recordGroupDefinitions.map( + (groupDefinition) => + groupDefinition.id === updatedRecordGroupDefinition.id + ? { + ...groupDefinition, + isVisible: !groupDefinition.isVisible, + } + : groupDefinition, + ); + + setRecordGroupDefinitions(updatedRecordGroupDefinitions); + + saveViewGroups( + mapRecordGroupDefinitionsToViewGroups(updatedRecordGroupDefinitions), + ); + }, + [recordGroupDefinitions, setRecordGroupDefinitions, saveViewGroups], + ); + + return { + handleVisibilityChange, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroups.ts b/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroups.ts new file mode 100644 index 0000000000..8dcea64d73 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroups.ts @@ -0,0 +1,58 @@ +import { useMemo } from 'react'; + +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { recordGroupDefinitionsComponentState } from '@/object-record/record-group/states/recordGroupDefinitionsComponentState'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; + +type UseRecordGroupsParams = { + objectNameSingular: string; +}; + +export const useRecordGroups = ({ + objectNameSingular, +}: UseRecordGroupsParams) => { + const recordGroupDefinitions = useRecoilComponentValueV2( + recordGroupDefinitionsComponentState, + ); + + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular, + }); + + const viewGroupFieldMetadataItem = useMemo(() => { + if (recordGroupDefinitions.length === 0) return null; + // We're assuming that all groups have the same fieldMetadataId for now + const fieldMetadataId = + 'fieldMetadataId' in recordGroupDefinitions[0] + ? recordGroupDefinitions[0].fieldMetadataId + : null; + + if (!fieldMetadataId) return null; + + return objectMetadataItem.fields.find( + (field) => field.id === fieldMetadataId, + ); + }, [objectMetadataItem, recordGroupDefinitions]); + + const visibleRecordGroups = useMemo( + () => + recordGroupDefinitions + .filter((boardGroup) => boardGroup.isVisible) + .sort( + (boardGroupA, boardGroupB) => + boardGroupA.position - boardGroupB.position, + ), + [recordGroupDefinitions], + ); + + const hiddenRecordGroups = useMemo( + () => recordGroupDefinitions.filter((boardGroup) => !boardGroup.isVisible), + [recordGroupDefinitions], + ); + + return { + hiddenRecordGroups, + visibleRecordGroups, + viewGroupFieldMetadataItem, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-group/states/recordGroupDefinitionsComponentState.ts b/packages/twenty-front/src/modules/object-record/record-group/states/recordGroupDefinitionsComponentState.ts new file mode 100644 index 0000000000..56ec80fcc2 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-group/states/recordGroupDefinitionsComponentState.ts @@ -0,0 +1,11 @@ +import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; +import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; + +export const recordGroupDefinitionsComponentState = createComponentStateV2< + RecordGroupDefinition[] +>({ + key: 'recordGroupDefinitionsComponentState', + defaultValue: [], + componentInstanceContext: ViewComponentInstanceContext, +}); diff --git a/packages/twenty-front/src/modules/object-record/record-board/types/RecordBoardColumnAction.ts b/packages/twenty-front/src/modules/object-record/record-group/types/RecordGroupActions.ts similarity index 78% rename from packages/twenty-front/src/modules/object-record/record-board/types/RecordBoardColumnAction.ts rename to packages/twenty-front/src/modules/object-record/record-group/types/RecordGroupActions.ts index 46e880ff84..7fd2d731be 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/types/RecordBoardColumnAction.ts +++ b/packages/twenty-front/src/modules/object-record/record-group/types/RecordGroupActions.ts @@ -1,6 +1,6 @@ import { IconComponent } from 'twenty-ui'; -export type RecordBoardColumnAction = { +export type RecordGroupAction = { id: string; label: string; icon: IconComponent; diff --git a/packages/twenty-front/src/modules/object-record/record-group/types/RecordGroupDefinition.ts b/packages/twenty-front/src/modules/object-record/record-group/types/RecordGroupDefinition.ts new file mode 100644 index 0000000000..2c6884ce10 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-group/types/RecordGroupDefinition.ts @@ -0,0 +1,17 @@ +import { ThemeColor } from 'twenty-ui'; + +export const enum RecordGroupDefinitionType { + Value = 'value', + NoValue = 'no-value', +} + +export type RecordGroupDefinition = { + id: string; + fieldMetadataId: string; + type: RecordGroupDefinitionType; + title: string; + value: string | null; + color: ThemeColor | 'transparent'; + position: number; + isVisible: boolean; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardColumnLoaderEffect.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardColumnLoaderEffect.tsx index e9c909b724..6f6a2e2bed 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardColumnLoaderEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardColumnLoaderEffect.tsx @@ -9,14 +9,12 @@ import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/get export const RecordIndexBoardColumnLoaderEffect = ({ objectNameSingular, - boardFieldSelectValue, boardFieldMetadataId, recordBoardId, columnId, }: { recordBoardId: string; objectNameSingular: string; - boardFieldSelectValue: string | null; boardFieldMetadataId: string | null; columnId: string; }) => { @@ -40,7 +38,6 @@ export const RecordIndexBoardColumnLoaderEffect = ({ objectNameSingular, recordBoardId, boardFieldMetadataId, - columnFieldSelectValue: boardFieldSelectValue, columnId, }); @@ -70,7 +67,6 @@ export const RecordIndexBoardColumnLoaderEffect = ({ fetchMoreRecords, loading, shouldFetchMore, - boardFieldSelectValue, setLoadingRecordsForThisColumn, loadingRecordsForThisColumn, diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardDataLoader.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardDataLoader.tsx index 8bacfe0355..194580587d 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardDataLoader.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardDataLoader.tsx @@ -26,23 +26,18 @@ export const RecordIndexBoardDataLoader = ({ (field) => field.id === recordIndexKanbanFieldMetadataId, ); - const possibleKanbanSelectFieldValues = - recordIndexKanbanFieldMetadataItem?.options ?? []; - const { columnIdsState } = useRecordBoardStates(recordBoardId); - // TODO: we should make sure there's no way to have a mismatch between columnIds and possibleKanbanSelectFieldValues order const columnIds = useRecoilValue(columnIdsState); return ( <> - {possibleKanbanSelectFieldValues.map((option, index) => ( + {columnIds.map((columnId, index) => ( ))} @@ -50,7 +45,6 @@ export const RecordIndexBoardDataLoader = ({ diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardDataLoaderEffect.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardDataLoaderEffect.tsx index e034730304..d1ef9eef40 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardDataLoaderEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardDataLoaderEffect.tsx @@ -1,16 +1,14 @@ -import { useCallback, useEffect } from 'react'; -import { useLocation, useNavigate } from 'react-router-dom'; +import { useEffect } from 'react'; import { useRecoilValue, useSetRecoilState } from 'recoil'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug'; import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard'; +import { recordGroupDefinitionsComponentState } from '@/object-record/record-group/states/recordGroupDefinitionsComponentState'; import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState'; import { recordIndexIsCompactModeActiveState } from '@/object-record/record-index/states/recordIndexIsCompactModeActiveState'; import { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState'; -import { computeRecordBoardColumnDefinitionsFromObjectMetadata } from '@/object-record/utils/computeRecordBoardColumnDefinitionsFromObjectMetadata'; -import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { FieldMetadataType } from '~/generated-metadata/graphql'; import { isDefined } from '~/utils/isDefined'; @@ -32,6 +30,10 @@ export const RecordIndexBoardDataLoaderEffect = ({ recordIndexFieldDefinitionsState, ); + const recordIndexGroupDefinitions = useRecoilComponentValueV2( + recordGroupDefinitionsComponentState, + ); + const recordIndexKanbanFieldMetadataId = useRecoilValue( recordIndexKanbanFieldMetadataIdState, ); @@ -60,43 +62,17 @@ export const RecordIndexBoardDataLoaderEffect = ({ setFieldDefinitions(recordIndexFieldDefinitions); }, [recordIndexFieldDefinitions, setFieldDefinitions]); - const navigate = useNavigate(); - const location = useLocation(); - const setNavigationMemorizedUrl = useSetRecoilState( - navigationMemorizedUrlState, - ); - - const navigateToSelectSettings = useCallback(() => { - setNavigationMemorizedUrl(location.pathname + location.search); - navigate(`/settings/objects/${getObjectSlug(objectMetadataItem)}`); - }, [ - navigate, - objectMetadataItem, - location.pathname, - location.search, - setNavigationMemorizedUrl, - ]); - useEffect(() => { setObjectSingularName(objectNameSingular); }, [objectNameSingular, setObjectSingularName]); useEffect(() => { - setColumns( - computeRecordBoardColumnDefinitionsFromObjectMetadata( - objectMetadataItem, - recordIndexKanbanFieldMetadataId ?? '', - navigateToSelectSettings, - ), - ); - }, [ - navigateToSelectSettings, - objectMetadataItem, - objectNameSingular, - recordIndexKanbanFieldMetadataId, - setColumns, - ]); + setColumns(recordIndexGroupDefinitions); + }, [recordIndexGroupDefinitions, setColumns]); + // TODO: Remove this duplicate useEffect by ensuring it's not here because + // We want it to be triggered by a change of objectMetadataItem, which would be an anti-pattern + // As it is an unnecessary dependency useEffect(() => { setFieldDefinitions(recordIndexFieldDefinitions); }, [objectMetadataItem, setFieldDefinitions, recordIndexFieldDefinitions]); diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx index 1bd29294bb..78a046cb9d 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx @@ -24,13 +24,17 @@ import { SpreadsheetImportProvider } from '@/spreadsheet-import/provider/compone import { RecordIndexActionMenu } from '@/action-menu/components/RecordIndexActionMenu'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; +import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard'; +import { recordGroupDefinitionsComponentState } from '@/object-record/record-group/states/recordGroupDefinitionsComponentState'; +import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { ViewBar } from '@/views/components/ViewBar'; -import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; import { ViewField } from '@/views/types/ViewField'; +import { ViewGroup } from '@/views/types/ViewGroup'; import { ViewType } from '@/views/types/ViewType'; import { mapViewFieldsToColumnDefinitions } from '@/views/utils/mapViewFieldsToColumnDefinitions'; import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters'; +import { mapViewGroupsToRecordGroupDefinitions } from '@/views/utils/mapViewGroupsToRecordGroupDefinitions'; import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts'; import { useContext } from 'react'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; @@ -61,6 +65,10 @@ export const RecordIndexContainer = () => { objectNameSingular, } = useContext(RecordIndexRootPropsContext); + const recordGroupDefinitionsCallbackState = useRecoilComponentCallbackStateV2( + recordGroupDefinitionsComponentState, + ); + const { columnDefinitions, filterDefinitions, sortDefinitions } = useColumnDefinitionsFromFieldMetadata(objectMetadataItem); @@ -77,6 +85,8 @@ export const RecordIndexContainer = () => { recordTableId: recordIndexId, }); + const { setColumns } = useRecordBoard(recordIndexId); + const onViewFieldsChange = useRecoilCallback( ({ set, snapshot }) => (viewFields: ViewField[]) => { @@ -103,6 +113,32 @@ export const RecordIndexContainer = () => { [columnDefinitions, setTableColumns], ); + const onViewGroupsChange = useRecoilCallback( + ({ set, snapshot }) => + (viewGroups: ViewGroup[]) => { + const newGroupDefinitions = mapViewGroupsToRecordGroupDefinitions({ + objectMetadataItem, + viewGroups, + }); + + setColumns(newGroupDefinitions); + + const existingRecordIndexGroupDefinitions = snapshot + .getLoadable(recordGroupDefinitionsCallbackState) + .getValue(); + + if ( + !isDeeplyEqual( + existingRecordIndexGroupDefinitions, + newGroupDefinitions, + ) + ) { + set(recordGroupDefinitionsCallbackState, newGroupDefinitions); + } + }, + [objectMetadataItem, recordGroupDefinitionsCallbackState, setColumns], + ); + const setContextStoreTargetedRecordsRule = useSetRecoilComponentStateV2( contextStoreTargetedRecordsRuleComponentState, ); @@ -110,86 +146,83 @@ export const RecordIndexContainer = () => { return ( - - - - + + + + } + onCurrentViewChange={(view) => { + if (!view) { + return; } - onCurrentViewChange={(view) => { - if (!view) { - return; - } - onViewFieldsChange(view.viewFields); - setTableFilters( - mapViewFiltersToFilters(view.viewFilters, filterDefinitions), - ); - setRecordIndexFilters( - mapViewFiltersToFilters(view.viewFilters, filterDefinitions), - ); - setContextStoreTargetedRecordsRule((prev) => ({ - ...prev, - filters: mapViewFiltersToFilters( - view.viewFilters, - filterDefinitions, - ), - })); - setTableSorts( - mapViewSortsToSorts(view.viewSorts, sortDefinitions), - ); - setRecordIndexSorts( - mapViewSortsToSorts(view.viewSorts, sortDefinitions), - ); - setRecordIndexViewType(view.type); - setRecordIndexViewKanbanFieldMetadataIdState( - view.kanbanFieldMetadataId, - ); - setRecordIndexIsCompactModeActive(view.isCompact); - }} - /> - ({ + ...prev, + filters: mapViewFiltersToFilters( + view.viewFilters, + filterDefinitions, + ), + })); + setTableSorts( + mapViewSortsToSorts(view.viewSorts, sortDefinitions), + ); + setRecordIndexSorts( + mapViewSortsToSorts(view.viewSorts, sortDefinitions), + ); + setRecordIndexViewType(view.type); + setRecordIndexViewKanbanFieldMetadataIdState( + view.kanbanFieldMetadataId, + ); + setRecordIndexIsCompactModeActive(view.isCompact); + }} + /> + + + {recordIndexViewType === ViewType.Table && ( + <> + - - {recordIndexViewType === ViewType.Table && ( - <> - - - - )} - {recordIndexViewType === ViewType.Kanban && ( - - - - - - )} - - - + + + )} + {recordIndexViewType === ViewType.Kanban && ( + + + + + + )} + + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageKanbanAddButton.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageKanbanAddButton.tsx index 3b80799091..65c8b13039 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageKanbanAddButton.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageKanbanAddButton.tsx @@ -3,7 +3,7 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; import { useAddNewCard } from '@/object-record/record-board/record-board-column/hooks/useAddNewCard'; import { useIsOpportunitiesCompanyFieldDisabled } from '@/object-record/record-board/record-board-column/hooks/useIsOpportunitiesCompanyFieldDisabled'; -import { RecordBoardColumnDefinition } from '@/object-record/record-board/types/RecordBoardColumnDefinition'; +import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; import { RecordIndexPageKanbanAddMenuItem } from '@/object-record/record-index/components/RecordIndexPageKanbanAddMenuItem'; import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; import { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState'; @@ -58,7 +58,7 @@ export const RecordIndexPageKanbanAddButton = () => { const { handleAddNewCardClick } = useAddNewCard(); const handleItemClick = useCallback( - (columnDefinition: RecordBoardColumnDefinition) => { + (columnDefinition: RecordGroupDefinition) => { const isOpportunityEnabled = isOpportunity && !isOpportunitiesCompanyFieldDisabled; handleAddNewCardClick( diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageKanbanAddMenuItem.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageKanbanAddMenuItem.tsx index f7b2881914..facb36608e 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageKanbanAddMenuItem.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageKanbanAddMenuItem.tsx @@ -1,4 +1,4 @@ -import { RecordBoardColumnDefinitionType } from '@/object-record/record-board/types/RecordBoardColumnDefinition'; +import { RecordGroupDefinitionType } from '@/object-record/record-group/types/RecordGroupDefinition'; import { useRecordIndexPageKanbanAddMenuItem } from '@/object-record/record-index/hooks/useRecordIndexPageKanbanAddMenuItem'; import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; import styled from '@emotion/styled'; @@ -32,18 +32,18 @@ export const RecordIndexPageKanbanAddMenuItem = ({ text={ { + setColumns(recordIndexGroupDefinitions); + }, [recordIndexGroupDefinitions, setColumns]); + const recordIndexFilters = useRecoilValue(recordIndexFiltersState); const recordIndexSorts = useRecoilValue(recordIndexSortsState); const requestFilters = turnFiltersIntoQueryFilter( diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoardColumn.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoardColumn.ts index e77545fdcf..15c84578dd 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoardColumn.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoardColumn.ts @@ -11,12 +11,12 @@ import { recordIndexFiltersState } from '@/object-record/record-index/states/rec import { recordIndexSortsState } from '@/object-record/record-index/states/recordIndexSortsState'; import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore'; import { isDefined } from '~/utils/isDefined'; +import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; type UseLoadRecordIndexBoardProps = { objectNameSingular: string; boardFieldMetadataId: string | null; recordBoardId: string; - columnFieldSelectValue: string | null; columnId: string; }; @@ -24,17 +24,18 @@ export const useLoadRecordIndexBoardColumn = ({ objectNameSingular, boardFieldMetadataId, recordBoardId, - columnFieldSelectValue, columnId, }: UseLoadRecordIndexBoardProps) => { const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular, }); const { setRecordIdsForColumn } = useRecordBoard(recordBoardId); + const { columnsFamilySelector } = useRecordBoardStates(recordBoardId); const { upsertRecords: upsertRecordsInStore } = useUpsertRecordsInStore(); const recordIndexFilters = useRecoilValue(recordIndexFiltersState); const recordIndexSorts = useRecoilValue(recordIndexSortsState); + const columnDefinition = useRecoilValue(columnsFamilySelector(columnId)); const requestFilters = turnFiltersIntoQueryFilter( recordIndexFilters, objectMetadataItem?.fields ?? [], @@ -53,9 +54,9 @@ export const useLoadRecordIndexBoardColumn = ({ const filter = { ...requestFilters, [recordIndexKanbanFieldMetadataItem?.name ?? '']: isDefined( - columnFieldSelectValue, + columnDefinition?.value, ) - ? { in: [columnFieldSelectValue] } + ? { in: [columnDefinition?.value] } : { is: 'NULL' }, }; diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/components/RecordIndexOptionsDropdownContent.tsx b/packages/twenty-front/src/modules/object-record/record-index/options/components/RecordIndexOptionsDropdownContent.tsx index 5f036cc546..0375ab3408 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/options/components/RecordIndexOptionsDropdownContent.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/options/components/RecordIndexOptionsDropdownContent.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { Key } from 'ts-key-enum'; import { IconBaselineDensitySmall, @@ -10,6 +10,7 @@ import { IconSettings, IconTag, UndecoratedLink, + useIcons, } from 'twenty-ui'; import { useObjectNamePluralFromSingular } from '@/object-metadata/hooks/useObjectNamePluralFromSingular'; @@ -21,6 +22,9 @@ import { useExportRecordData, } from '@/action-menu/hooks/useExportRecordData'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { useRecordGroupReorder } from '@/object-record/record-group/hooks/useRecordGroupReorder'; +import { useRecordGroups } from '@/object-record/record-group/hooks/useRecordGroups'; +import { useRecordGroupVisibility } from '@/object-record/record-group/hooks/useRecordGroupVisibility'; import { useRecordIndexOptionsForBoard } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsForBoard'; import { useRecordIndexOptionsForTable } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsForTable'; import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope'; @@ -37,12 +41,17 @@ import { MenuItemToggle } from '@/ui/navigation/menu-item/components/MenuItemTog import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { ViewFieldsVisibilityDropdownSection } from '@/views/components/ViewFieldsVisibilityDropdownSection'; +import { ViewGroupsVisibilityDropdownSection } from '@/views/components/ViewGroupsVisibilityDropdownSection'; import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; import { ViewType } from '@/views/types/ViewType'; import { useLocation } from 'react-router-dom'; import { useSetRecoilState } from 'recoil'; -type RecordIndexOptionsMenu = 'fields' | 'hiddenFields'; +type RecordIndexOptionsMenu = + | 'viewGroups' + | 'hiddenViewGroups' + | 'fields' + | 'hiddenFields'; type RecordIndexOptionsDropdownContentProps = { recordIndexId: string; @@ -50,6 +59,7 @@ type RecordIndexOptionsDropdownContentProps = { viewType: ViewType; }; +// TODO: Break this component down export const RecordIndexOptionsDropdownContent = ({ viewType, recordIndexId, @@ -57,6 +67,8 @@ export const RecordIndexOptionsDropdownContent = ({ }: RecordIndexOptionsDropdownContentProps) => { const { currentViewWithCombinedFiltersAndSorts } = useGetCurrentView(); + const { getIcon } = useIcons(); + const { closeDropdown } = useDropdown(RECORD_INDEX_OPTIONS_DROPDOWN_ID); const [currentMenu, setCurrentMenu] = useState< @@ -111,6 +123,28 @@ export const RecordIndexOptionsDropdownContent = ({ viewBarId: recordIndexId, }); + const { + hiddenRecordGroups, + visibleRecordGroups, + viewGroupFieldMetadataItem, + } = useRecordGroups({ + objectNameSingular: objectMetadataItem.nameSingular, + }); + const { handleVisibilityChange: handleRecordGroupVisibilityChange } = + useRecordGroupVisibility({ + viewBarId: recordIndexId, + }); + const { handleOrderChange: handleRecordGroupOrderChange } = + useRecordGroupReorder({ + objectNameSingular: objectMetadataItem.nameSingular, + viewBarId: recordIndexId, + }); + + const viewGroupSettingsUrl = getSettingsPagePath(SettingsPath.ObjectDetail, { + id: viewGroupFieldMetadataItem?.name, + objectSlug: objectNamePlural, + }); + const visibleRecordFields = viewType === ViewType.Kanban ? visibleBoardFields : visibleTableColumns; @@ -143,10 +177,28 @@ export const RecordIndexOptionsDropdownContent = ({ navigationMemorizedUrlState, ); + const isViewGroupMenuItemVisible = + viewGroupFieldMetadataItem && + (visibleRecordGroups.length > 0 || hiddenRecordGroups.length > 0); + + useEffect(() => { + if (currentMenu === 'hiddenViewGroups' && hiddenRecordGroups.length === 0) { + setCurrentMenu('viewGroups'); + } + }, [hiddenRecordGroups, currentMenu]); + return ( <> {!currentMenu && ( + {isViewGroupMenuItemVisible && ( + handleSelectMenu('viewGroups')} + LeftIcon={getIcon(currentViewWithCombinedFiltersAndSorts?.icon)} + text={viewGroupFieldMetadataItem.label} + hasSubMenu + /> + )} handleSelectMenu('fields')} LeftIcon={IconTag} @@ -174,6 +226,34 @@ export const RecordIndexOptionsDropdownContent = ({ /> )} + {currentMenu === 'viewGroups' && ( + <> + + {viewGroupFieldMetadataItem?.label} + + + {hiddenRecordGroups.length > 0 && ( + <> + + + handleSelectMenu('hiddenViewGroups')} + LeftIcon={IconEyeOff} + text={`Hidden ${viewGroupFieldMetadataItem?.label ?? ''}`} + /> + + + )} + + )} {currentMenu === 'fields' && ( <> @@ -198,6 +278,36 @@ export const RecordIndexOptionsDropdownContent = ({ )} + {currentMenu === 'hiddenViewGroups' && ( + <> + setCurrentMenu('viewGroups')} + > + Hidden {viewGroupFieldMetadataItem?.label} + + + + { + setNavigationMemorizedUrl(location.pathname + location.search); + closeDropdown(); + }} + > + + + + + + )} {currentMenu === 'hiddenFields' && ( <> { - it('should correctly compute', () => { - const objectMetadataItem = generatedMockObjectMetadataItems.find( - (item) => item.nameSingular === 'opportunity', - ); - - const stageField = objectMetadataItem?.fields.find( - (field) => field.name === 'stage', - ); - - if (!objectMetadataItem) { - throw new Error('Object metadata item not found'); - } - - const res = computeRecordBoardColumnDefinitionsFromObjectMetadata( - objectMetadataItem, - stageField?.id, - () => null, - ); - expect(res.length).toEqual(stageField?.options?.length); - }); -}); diff --git a/packages/twenty-front/src/modules/object-record/utils/computeRecordBoardColumnDefinitionsFromObjectMetadata.ts b/packages/twenty-front/src/modules/object-record/utils/computeRecordBoardColumnDefinitionsFromObjectMetadata.ts deleted file mode 100644 index 51b3fa1d3b..0000000000 --- a/packages/twenty-front/src/modules/object-record/utils/computeRecordBoardColumnDefinitionsFromObjectMetadata.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { IconSettings } from 'twenty-ui'; - -import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { - RecordBoardColumnDefinition, - RecordBoardColumnDefinitionNoValue, - RecordBoardColumnDefinitionType, - RecordBoardColumnDefinitionValue, -} from '@/object-record/record-board/types/RecordBoardColumnDefinition'; -import { FieldMetadataType } from '~/generated-metadata/graphql'; - -export const computeRecordBoardColumnDefinitionsFromObjectMetadata = ( - objectMetadataItem: ObjectMetadataItem, - kanbanFieldMetadataId: string, - navigateToSelectSettings: () => void, -): RecordBoardColumnDefinition[] => { - const selectFieldMetadataItem = objectMetadataItem.fields.find( - (field) => - field.id === kanbanFieldMetadataId && - field.type === FieldMetadataType.Select, - ); - - if (!selectFieldMetadataItem) { - return []; - } - - if (!selectFieldMetadataItem.options) { - throw new Error( - `Select Field ${objectMetadataItem.nameSingular} has no options`, - ); - } - - const valueColumns = selectFieldMetadataItem.options.map( - (selectOption) => - ({ - id: selectOption.id, - type: RecordBoardColumnDefinitionType.Value, - title: selectOption.label, - value: selectOption.value, - color: selectOption.color, - position: selectOption.position, - actions: [ - { - id: 'edit', - label: 'Edit from Settings', - icon: IconSettings, - position: 0, - callback: navigateToSelectSettings, - }, - ], - }) satisfies RecordBoardColumnDefinitionValue, - ); - - const noValueColumn = { - id: 'no-value', - title: 'No Value', - type: RecordBoardColumnDefinitionType.NoValue, - value: null, - actions: [], - position: - selectFieldMetadataItem.options - .map((option) => option.position) - .reduce((a, b) => Math.max(a, b), 0) + 1, - } satisfies RecordBoardColumnDefinitionNoValue; - - if (selectFieldMetadataItem.isNullable === true) { - return [...valueColumns, noValueColumn]; - } - - return valueColumns; -}; diff --git a/packages/twenty-front/src/modules/prefetch/graphql/operation-signatures/factories/findAllViewsOperationSignatureFactory.ts b/packages/twenty-front/src/modules/prefetch/graphql/operation-signatures/factories/findAllViewsOperationSignatureFactory.ts index 3a4430a2cc..0e9aa76a02 100644 --- a/packages/twenty-front/src/modules/prefetch/graphql/operation-signatures/factories/findAllViewsOperationSignatureFactory.ts +++ b/packages/twenty-front/src/modules/prefetch/graphql/operation-signatures/factories/findAllViewsOperationSignatureFactory.ts @@ -20,5 +20,6 @@ export const findAllViewsOperationSignatureFactory: RecordGqlOperationSignatureF viewFilters: true, viewSorts: true, viewFields: true, + viewGroups: true, }, }); diff --git a/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemDraggable.tsx b/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemDraggable.tsx index c324e8ec48..430d45973b 100644 --- a/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemDraggable.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemDraggable.tsx @@ -5,14 +5,15 @@ import { StyledHoverableMenuItemBase } from '../internals/components/StyledMenuI import { MenuItemAccent } from '../types/MenuItemAccent'; import { MenuItemIconButton } from './MenuItem'; +import { ReactNode } from 'react'; export type MenuItemDraggableProps = { - LeftIcon: IconComponent | undefined; + LeftIcon?: IconComponent | undefined; accent?: MenuItemAccent; iconButtons?: MenuItemIconButton[]; isTooltipOpen?: boolean; onClick?: () => void; - text: string; + text: ReactNode; className?: string; isIconDisplayedOnHoverOnly?: boolean; showGrip?: boolean; diff --git a/packages/twenty-front/src/modules/views/components/ViewGroupsVisibilityDropdownSection.tsx b/packages/twenty-front/src/modules/views/components/ViewGroupsVisibilityDropdownSection.tsx new file mode 100644 index 0000000000..c7040f9ffe --- /dev/null +++ b/packages/twenty-front/src/modules/views/components/ViewGroupsVisibilityDropdownSection.tsx @@ -0,0 +1,192 @@ +import { + DropResult, + OnDragEndResponder, + ResponderProvided, +} from '@hello-pangea/dnd'; +import { useRef } from 'react'; +import { IconEye, IconEyeOff, Tag } from 'twenty-ui'; + +import { + RecordGroupDefinition, + RecordGroupDefinitionType, +} from '@/object-record/record-group/types/RecordGroupDefinition'; +import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem'; +import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList'; +import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; +import { StyledDropdownMenuSubheader } from '@/ui/layout/dropdown/components/StyledDropdownMenuSubheader'; +import { MenuItemDraggable } from '@/ui/navigation/menu-item/components/MenuItemDraggable'; +import { isDefined } from '~/utils/isDefined'; + +type ViewGroupsVisibilityDropdownSectionProps = { + viewGroups: RecordGroupDefinition[]; + isDraggable: boolean; + onDragEnd?: OnDragEndResponder; + onVisibilityChange: (viewGroup: RecordGroupDefinition) => void; + title: string; + showSubheader: boolean; + showDragGrip: boolean; +}; + +export const ViewGroupsVisibilityDropdownSection = ({ + viewGroups, + isDraggable, + onDragEnd, + onVisibilityChange, + title, + showSubheader = true, + showDragGrip, +}: ViewGroupsVisibilityDropdownSectionProps) => { + const handleOnDrag = (result: DropResult, provided: ResponderProvided) => { + onDragEnd?.(result, provided); + }; + + const getIconButtons = (index: number, viewGroup: RecordGroupDefinition) => { + const iconButtons = [ + { + Icon: viewGroup.isVisible ? IconEyeOff : IconEye, + onClick: () => onVisibilityChange(viewGroup), + }, + ].filter(isDefined); + + return iconButtons.length ? iconButtons : undefined; + }; + + const noValueViewGroups = + viewGroups.filter( + (viewGroup) => viewGroup.type === RecordGroupDefinitionType.NoValue, + ) ?? []; + + const viewGroupsWithoutNoValueGroups = viewGroups.filter( + (viewGroup) => viewGroup.type !== RecordGroupDefinitionType.NoValue, + ); + + const ref = useRef(null); + + return ( +
+ {showSubheader && ( + {title} + )} + + {!!viewGroups.length && ( + <> + {!isDraggable ? ( + viewGroupsWithoutNoValueGroups.map( + (viewGroup, viewGroupIndex) => ( + + } + iconButtons={getIconButtons(viewGroupIndex, viewGroup)} + accent={showDragGrip ? 'placeholder' : 'default'} + showGrip={showDragGrip} + isDragDisabled={!isDraggable} + /> + ), + ) + ) : ( + + {viewGroupsWithoutNoValueGroups.map( + (viewGroup, viewGroupIndex) => ( + + } + iconButtons={getIconButtons( + viewGroupIndex, + viewGroup, + )} + accent={showDragGrip ? 'placeholder' : 'default'} + showGrip={showDragGrip} + isDragDisabled={!isDraggable} + /> + } + /> + ), + )} + + } + /> + )} + {noValueViewGroups.map((viewGroup) => ( + + } + accent={showDragGrip ? 'placeholder' : 'default'} + showGrip={true} + isDragDisabled={true} + isHoverDisabled + /> + ))} + + )} + +
+ ); +}; diff --git a/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewGroupRecords.ts b/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewGroupRecords.ts new file mode 100644 index 0000000000..5582c771f3 --- /dev/null +++ b/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewGroupRecords.ts @@ -0,0 +1,118 @@ +import { useApolloClient } from '@apollo/client'; +import { useCallback } from 'react'; +import { v4 } from 'uuid'; + +import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect'; +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { useCreateOneRecordMutation } from '@/object-record/hooks/useCreateOneRecordMutation'; +import { useUpdateOneRecordMutation } from '@/object-record/hooks/useUpdateOneRecordMutation'; +import { GraphQLView } from '@/views/types/GraphQLView'; +import { ViewGroup } from '@/views/types/ViewGroup'; + +export const usePersistViewGroupRecords = () => { + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular: CoreObjectNameSingular.ViewGroup, + }); + + const { createOneRecordMutation } = useCreateOneRecordMutation({ + objectNameSingular: CoreObjectNameSingular.ViewGroup, + }); + + const { updateOneRecordMutation } = useUpdateOneRecordMutation({ + objectNameSingular: CoreObjectNameSingular.ViewGroup, + }); + + const { objectMetadataItems } = useObjectMetadataItems(); + + const apolloClient = useApolloClient(); + + const createViewGroupRecords = useCallback( + (viewGroupsToCreate: ViewGroup[], view: GraphQLView) => { + if (!viewGroupsToCreate.length) return; + + return Promise.all( + viewGroupsToCreate.map((viewGroup) => + apolloClient.mutate({ + mutation: createOneRecordMutation, + variables: { + input: { + fieldMetadataId: viewGroup.fieldMetadataId, + viewId: view.id, + isVisible: viewGroup.isVisible, + position: viewGroup.position, + id: v4(), + fieldValue: viewGroup.fieldValue, + }, + }, + update: (cache, { data }) => { + const record = data?.['createViewGroup']; + if (!record) return; + + triggerCreateRecordsOptimisticEffect({ + cache, + objectMetadataItem, + recordsToCreate: [record], + objectMetadataItems, + }); + }, + }), + ), + ); + }, + [ + apolloClient, + createOneRecordMutation, + objectMetadataItem, + objectMetadataItems, + ], + ); + + const updateViewGroupRecords = useCallback( + async (viewGroupsToUpdate: ViewGroup[]) => { + if (!viewGroupsToUpdate.length) return; + + const mutationPromises = viewGroupsToUpdate.map((viewGroup) => + apolloClient.mutate<{ updateViewGroup: ViewGroup }>({ + mutation: updateOneRecordMutation, + variables: { + idToUpdate: viewGroup.id, + input: { + isVisible: viewGroup.isVisible, + position: viewGroup.position, + }, + }, + // Avoid cache being updated with stale data + fetchPolicy: 'no-cache', + }), + ); + + const mutationResults = await Promise.all(mutationPromises); + + // FixMe: Using triggerCreateRecordsOptimisticEffect is actaully causing multiple records to be created + mutationResults.forEach(({ data }) => { + const record = data?.['updateViewGroup']; + + if (!record) return; + + apolloClient.cache.modify({ + id: apolloClient.cache.identify({ + __typename: 'ViewGroup', + id: record.id, + }), + fields: { + isVisible: () => record.isVisible, + position: () => record.position, + }, + }); + }); + }, + [apolloClient, updateOneRecordMutation], + ); + + return { + createViewGroupRecords, + updateViewGroupRecords, + }; +}; diff --git a/packages/twenty-front/src/modules/views/hooks/useCreateViewFiltersAndSorts.ts b/packages/twenty-front/src/modules/views/hooks/useCreateViewFiltersAndSorts.ts deleted file mode 100644 index 4f760b173c..0000000000 --- a/packages/twenty-front/src/modules/views/hooks/useCreateViewFiltersAndSorts.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { usePersistViewFilterRecords } from '@/views/hooks/internal/usePersistViewFilterRecords'; -import { usePersistViewSortRecords } from '@/views/hooks/internal/usePersistViewSortRecords'; - -import { useGetViewFromCache } from '@/views/hooks/useGetViewFromCache'; -import { ViewFilter } from '@/views/types/ViewFilter'; -import { ViewSort } from '@/views/types/ViewSort'; -import { isDefined } from '~/utils/isDefined'; - -export const useCreateViewFiltersAndSorts = () => { - const { getViewFromCache } = useGetViewFromCache(); - - const { createViewSortRecords } = usePersistViewSortRecords(); - - const { createViewFilterRecords } = usePersistViewFilterRecords(); - - const createViewFiltersAndSorts = async ( - viewIdToCreateOn: string, - filtersToCreate: ViewFilter[], - sortsToCreate: ViewSort[], - ) => { - const view = await getViewFromCache(viewIdToCreateOn); - - if (!isDefined(view)) { - return; - } - - await createViewSortRecords(sortsToCreate, view); - await createViewFilterRecords(filtersToCreate, view); - }; - - return { - createViewFiltersAndSorts, - }; -}; diff --git a/packages/twenty-front/src/modules/views/hooks/useCreateViewFromCurrentView.ts b/packages/twenty-front/src/modules/views/hooks/useCreateViewFromCurrentView.ts index fcf8a6c816..82f79e62aa 100644 --- a/packages/twenty-front/src/modules/views/hooks/useCreateViewFromCurrentView.ts +++ b/packages/twenty-front/src/modules/views/hooks/useCreateViewFromCurrentView.ts @@ -1,9 +1,12 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; +import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; import { usePersistViewFieldRecords } from '@/views/hooks/internal/usePersistViewFieldRecords'; -import { useCreateViewFiltersAndSorts } from '@/views/hooks/useCreateViewFiltersAndSorts'; +import { usePersistViewFilterRecords } from '@/views/hooks/internal/usePersistViewFilterRecords'; +import { usePersistViewGroupRecords } from '@/views/hooks/internal/usePersistViewGroupRecords'; +import { usePersistViewSortRecords } from '@/views/hooks/internal/usePersistViewSortRecords'; import { useGetViewFiltersCombined } from '@/views/hooks/useGetCombinedViewFilters'; import { useGetViewSortsCombined } from '@/views/hooks/useGetCombinedViewSorts'; import { useGetViewFromCache } from '@/views/hooks/useGetViewFromCache'; @@ -11,6 +14,10 @@ import { currentViewIdComponentState } from '@/views/states/currentViewIdCompone import { isPersistingViewFieldsComponentState } from '@/views/states/isPersistingViewFieldsComponentState'; import { GraphQLView } from '@/views/types/GraphQLView'; import { View } from '@/views/types/View'; +import { ViewGroup } from '@/views/types/ViewGroup'; +import { ViewType } from '@/views/types/ViewType'; +import { isNonEmptyArray } from '@sniptt/guards'; +import { useContext } from 'react'; import { useRecoilCallback } from 'recoil'; import { isDefined } from 'twenty-ui'; import { v4 } from 'uuid'; @@ -35,12 +42,18 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => { const { createViewFieldRecords } = usePersistViewFieldRecords(); - const { createViewFiltersAndSorts } = useCreateViewFiltersAndSorts(); - const { getViewSortsCombined } = useGetViewSortsCombined(viewBarComponentId); const { getViewFiltersCombined } = useGetViewFiltersCombined(viewBarComponentId); + const { createViewSortRecords } = usePersistViewSortRecords(); + + const { createViewGroupRecords } = usePersistViewGroupRecords(); + + const { createViewFilterRecords } = usePersistViewFilterRecords(); + + const { objectMetadataItem } = useContext(RecordIndexRootPropsContext); + const createViewFromCurrentView = useRecoilCallback( ({ snapshot, set }) => async ( @@ -93,20 +106,56 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => { await createViewFieldRecords(view.viewFields, newView); + if (type === ViewType.Kanban) { + if (!isNonEmptyArray(view.viewGroups)) { + if (!isDefined(kanbanFieldMetadataId)) { + throw new Error('Kanban view must have a kanban field'); + } + + const viewGroupsToCreate = + objectMetadataItem?.fields + ?.find((field) => field.id === kanbanFieldMetadataId) + ?.options?.map( + (option, index) => + ({ + id: v4(), + __typename: 'ViewGroup', + fieldMetadataId: kanbanFieldMetadataId, + fieldValue: option.value, + isVisible: true, + position: index, + }) satisfies ViewGroup, + ) ?? []; + + viewGroupsToCreate.push({ + __typename: 'ViewGroup', + id: v4(), + fieldValue: '', + position: viewGroupsToCreate.length, + isVisible: true, + fieldMetadataId: kanbanFieldMetadataId, + } satisfies ViewGroup); + + await createViewGroupRecords(viewGroupsToCreate, newView); + } else { + await createViewGroupRecords(view.viewGroups, newView); + } + } + if (shouldCopyFiltersAndSorts === true) { const sourceViewCombinedFilters = getViewFiltersCombined(view.id); const sourceViewCombinedSorts = getViewSortsCombined(view.id); - await createViewFiltersAndSorts( - newView.id, - sourceViewCombinedFilters, - sourceViewCombinedSorts, - ); + await createViewSortRecords(sourceViewCombinedSorts, view); + await createViewFilterRecords(sourceViewCombinedFilters, view); } set(isPersistingViewFieldsCallbackState, false); }, [ + objectMetadataItem, + createViewSortRecords, + createViewFilterRecords, createOneRecord, createViewFieldRecords, getViewSortsCombined, @@ -114,7 +163,7 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => { currentViewIdCallbackState, getViewFromCache, isPersistingViewFieldsCallbackState, - createViewFiltersAndSorts, + createViewGroupRecords, ], ); diff --git a/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewGroups.ts b/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewGroups.ts new file mode 100644 index 0000000000..384b262841 --- /dev/null +++ b/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewGroups.ts @@ -0,0 +1,96 @@ +import { useRecoilCallback } from 'recoil'; + +import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; +import { usePersistViewGroupRecords } from '@/views/hooks/internal/usePersistViewGroupRecords'; +import { useGetViewFromCache } from '@/views/hooks/useGetViewFromCache'; +import { currentViewIdComponentState } from '@/views/states/currentViewIdComponentState'; +import { ViewGroup } from '@/views/types/ViewGroup'; +import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; +import { isDefined } from '~/utils/isDefined'; +import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; + +export const useSaveCurrentViewGroups = (viewBarComponentId?: string) => { + const { createViewGroupRecords, updateViewGroupRecords } = + usePersistViewGroupRecords(); + + const { getViewFromCache } = useGetViewFromCache(); + + const currentViewIdCallbackState = useRecoilComponentCallbackStateV2( + currentViewIdComponentState, + viewBarComponentId, + ); + + const saveViewGroups = useRecoilCallback( + ({ snapshot }) => + async (viewGroupsToSave: ViewGroup[]) => { + const currentViewId = snapshot + .getLoadable(currentViewIdCallbackState) + .getValue(); + + if (!currentViewId) { + return; + } + + const view = await getViewFromCache(currentViewId); + + if (isUndefinedOrNull(view)) { + return; + } + + const currentViewGroups = view.viewGroups; + + const viewGroupsToUpdate = viewGroupsToSave + .map((viewGroupToSave) => { + const existingField = currentViewGroups.find( + (currentViewGroup) => + currentViewGroup.fieldValue === viewGroupToSave.fieldValue, + ); + + if (isUndefinedOrNull(existingField)) { + return undefined; + } + + if ( + isDeeplyEqual( + { + position: existingField.position, + isVisible: existingField.isVisible, + }, + { + position: viewGroupToSave.position, + isVisible: viewGroupToSave.isVisible, + }, + ) + ) { + return undefined; + } + + return { ...viewGroupToSave, id: existingField.id }; + }) + .filter(isDefined); + + const viewGroupsToCreate = viewGroupsToSave.filter( + (viewFieldToSave) => + !currentViewGroups.some( + (currentViewGroup) => + currentViewGroup.fieldValue === viewFieldToSave.fieldValue, + ), + ); + + await Promise.all([ + createViewGroupRecords(viewGroupsToCreate, view), + updateViewGroupRecords(viewGroupsToUpdate), + ]); + }, + [ + createViewGroupRecords, + currentViewIdCallbackState, + getViewFromCache, + updateViewGroupRecords, + ], + ); + + return { + saveViewGroups, + }; +}; diff --git a/packages/twenty-front/src/modules/views/types/GraphQLView.ts b/packages/twenty-front/src/modules/views/types/GraphQLView.ts index c657f83b57..fa3caa2f26 100644 --- a/packages/twenty-front/src/modules/views/types/GraphQLView.ts +++ b/packages/twenty-front/src/modules/views/types/GraphQLView.ts @@ -1,5 +1,6 @@ import { ViewField } from '@/views/types/ViewField'; import { ViewFilter } from '@/views/types/ViewFilter'; +import { ViewGroup } from '@/views/types/ViewGroup'; import { ViewKey } from '@/views/types/ViewKey'; import { ViewSort } from '@/views/types/ViewSort'; import { ViewType } from '@/views/types/ViewType'; @@ -15,6 +16,7 @@ export type GraphQLView = { viewFields: ViewField[]; viewFilters: ViewFilter[]; viewSorts: ViewSort[]; + viewGroups: ViewGroup[]; position: number; icon: string; }; diff --git a/packages/twenty-front/src/modules/views/types/View.ts b/packages/twenty-front/src/modules/views/types/View.ts index a3c9cac58b..03af1756f6 100644 --- a/packages/twenty-front/src/modules/views/types/View.ts +++ b/packages/twenty-front/src/modules/views/types/View.ts @@ -1,5 +1,6 @@ import { ViewField } from '@/views/types/ViewField'; import { ViewFilter } from '@/views/types/ViewFilter'; +import { ViewGroup } from '@/views/types/ViewGroup'; import { ViewKey } from '@/views/types/ViewKey'; import { ViewSort } from '@/views/types/ViewSort'; import { ViewType } from '@/views/types/ViewType'; @@ -12,6 +13,7 @@ export type View = { objectMetadataId: string; isCompact: boolean; viewFields: ViewField[]; + viewGroups: ViewGroup[]; viewFilters: ViewFilter[]; viewSorts: ViewSort[]; kanbanFieldMetadataId: string; diff --git a/packages/twenty-front/src/modules/views/types/ViewGroup.ts b/packages/twenty-front/src/modules/views/types/ViewGroup.ts new file mode 100644 index 0000000000..9f0cd38222 --- /dev/null +++ b/packages/twenty-front/src/modules/views/types/ViewGroup.ts @@ -0,0 +1,8 @@ +export type ViewGroup = { + __typename: 'ViewGroup'; + id: string; + fieldMetadataId: string; + isVisible: boolean; + fieldValue: string; + position: number; +}; diff --git a/packages/twenty-front/src/modules/views/utils/mapRecordGroupDefinitionsToViewGroups.ts b/packages/twenty-front/src/modules/views/utils/mapRecordGroupDefinitionsToViewGroups.ts new file mode 100644 index 0000000000..b925194519 --- /dev/null +++ b/packages/twenty-front/src/modules/views/utils/mapRecordGroupDefinitionsToViewGroups.ts @@ -0,0 +1,17 @@ +import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; +import { ViewGroup } from '@/views/types/ViewGroup'; + +export const mapRecordGroupDefinitionsToViewGroups = ( + groupDefinitions: RecordGroupDefinition[], +): ViewGroup[] => { + return groupDefinitions.map( + (groupDefinition): ViewGroup => ({ + __typename: 'ViewGroup', + id: groupDefinition.id, + fieldMetadataId: groupDefinition.fieldMetadataId, + position: groupDefinition.position, + isVisible: groupDefinition.isVisible ?? true, + fieldValue: groupDefinition.value ?? '', + }), + ); +}; diff --git a/packages/twenty-front/src/modules/views/utils/mapViewGroupsToRecordGroupDefinitions.ts b/packages/twenty-front/src/modules/views/utils/mapViewGroupsToRecordGroupDefinitions.ts new file mode 100644 index 0000000000..3767abe6a6 --- /dev/null +++ b/packages/twenty-front/src/modules/views/utils/mapViewGroupsToRecordGroupDefinitions.ts @@ -0,0 +1,79 @@ +import { isDefined } from '~/utils/isDefined'; + +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { + RecordGroupDefinition, + RecordGroupDefinitionType, +} from '@/object-record/record-group/types/RecordGroupDefinition'; +import { ViewGroup } from '@/views/types/ViewGroup'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; + +export const mapViewGroupsToRecordGroupDefinitions = ({ + objectMetadataItem, + viewGroups, +}: { + objectMetadataItem: ObjectMetadataItem; + viewGroups: ViewGroup[]; +}): RecordGroupDefinition[] => { + if (viewGroups?.length === 0) { + return []; + } + + const fieldMetadataId = viewGroups?.[0]?.fieldMetadataId; + const selectFieldMetadataItem = objectMetadataItem.fields.find( + (field) => + field.id === fieldMetadataId && field.type === FieldMetadataType.Select, + ); + + if (!selectFieldMetadataItem) { + return []; + } + + if (!selectFieldMetadataItem.options) { + throw new Error( + `Select Field ${objectMetadataItem.nameSingular} has no options`, + ); + } + + const recordGroupDefinitionsFromViewGroups = viewGroups + .map((viewGroup) => { + const selectedOption = selectFieldMetadataItem.options?.find( + (option) => option.value === viewGroup.fieldValue, + ); + + if (!selectedOption) return null; + + return { + id: viewGroup.id, + fieldMetadataId: viewGroup.fieldMetadataId, + type: RecordGroupDefinitionType.Value, + title: selectedOption.label, + value: selectedOption.value, + color: selectedOption.color, + position: viewGroup.position, + isVisible: viewGroup.isVisible, + } as RecordGroupDefinition; + }) + .filter(isDefined) + .sort((a, b) => a.position - b.position); + + if (selectFieldMetadataItem.isNullable === true) { + const noValueColumn = { + id: 'no-value', + title: 'No Value', + type: RecordGroupDefinitionType.NoValue, + value: null, + position: + recordGroupDefinitionsFromViewGroups + .map((option) => option.position) + .reduce((a, b) => Math.max(a, b), 0) + 1, + isVisible: true, + fieldMetadataId: selectFieldMetadataItem.id, + color: 'transparent', + } satisfies RecordGroupDefinition; + + return [...recordGroupDefinitionsFromViewGroups, noValueColumn]; + } + + return recordGroupDefinitionsFromViewGroups; +}; diff --git a/packages/twenty-front/src/pages/object-record/RecordIndexPage.tsx b/packages/twenty-front/src/pages/object-record/RecordIndexPage.tsx index c15536d8c5..6e7a2fb68d 100644 --- a/packages/twenty-front/src/pages/object-record/RecordIndexPage.tsx +++ b/packages/twenty-front/src/pages/object-record/RecordIndexPage.tsx @@ -16,6 +16,7 @@ import { useCreateNewTableRecord } from '@/object-record/record-table/hooks/useC import { PageBody } from '@/ui/layout/page/components/PageBody'; import { PageContainer } from '@/ui/layout/page/components/PageContainer'; import { PageTitle } from '@/ui/utilities/page-title/components/PageTitle'; +import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; import { useRecoilCallback } from 'recoil'; import { capitalize } from '~/utils/string/capitalize'; @@ -71,22 +72,26 @@ export const RecordIndexPage = () => { onCreateRecord: handleCreateRecord, }} > - - - - - - - - - - - - + + + + + + + + + + + + + + ); diff --git a/packages/twenty-front/src/pages/object-record/__stories__/RecordIndexPage.stories.tsx b/packages/twenty-front/src/pages/object-record/__stories__/RecordIndexPage.stories.tsx index 76b559961c..5101e66f25 100644 --- a/packages/twenty-front/src/pages/object-record/__stories__/RecordIndexPage.stories.tsx +++ b/packages/twenty-front/src/pages/object-record/__stories__/RecordIndexPage.stories.tsx @@ -32,7 +32,7 @@ export const Default: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); - await canvas.findByText('People', undefined, { timeout: 3000 }); + await canvas.findByText('People', undefined, { timeout: 10000 }); await canvas.findByText('Linkedin'); }, }; diff --git a/packages/twenty-front/src/pages/settings/data-model/__stories__/SettingsNewObject.stories.tsx b/packages/twenty-front/src/pages/settings/data-model/__stories__/SettingsNewObject.stories.tsx index 8815ff4564..a90e609b38 100644 --- a/packages/twenty-front/src/pages/settings/data-model/__stories__/SettingsNewObject.stories.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/__stories__/SettingsNewObject.stories.tsx @@ -29,7 +29,7 @@ export const WithStandardSelected: Story = { play: async () => { const canvas = within(document.body); - await canvas.findByText('New Object'); + await canvas.findByText('New Object', undefined, { timeout: 2000 }); const listingInput = await canvas.findByPlaceholderText('Listing'); const pluralInput = await canvas.findByPlaceholderText('Listings'); diff --git a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/view.ts b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/view.ts index 3b7e47b80d..1d059f8860 100644 --- a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/view.ts +++ b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/view.ts @@ -126,6 +126,33 @@ export const viewPrefillData = async ( ) .execute(); } + + if ( + 'groups' in viewDefinition && + viewDefinition.groups && + viewDefinition.groups.length > 0 + ) { + await entityManager + .createQueryBuilder() + .insert() + .into(`${schemaName}.viewGroup`, [ + 'fieldMetadataId', + 'isVisible', + 'fieldValue', + 'position', + 'viewId', + ]) + .values( + viewDefinition.groups.map((group: any) => ({ + fieldMetadataId: group.fieldMetadataId, + isVisible: group.isVisible, + fieldValue: group.fieldValue, + position: group.position, + viewId: viewDefinition.id, + })), + ) + .execute(); + } } return viewDefinitionsWithId; diff --git a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/opportunity-by-stage.view.ts b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/opportunity-by-stage.view.ts index 82d3b47912..2ac1b1279d 100644 --- a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/opportunity-by-stage.view.ts +++ b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/opportunity-by-stage.view.ts @@ -73,5 +73,52 @@ export const opportunitiesByStageView = ( size: 150, }, ], + groups: [ + { + fieldMetadataId: + objectMetadataMap[STANDARD_OBJECT_IDS.opportunity].fields[ + OPPORTUNITY_STANDARD_FIELD_IDS.stage + ], + isVisible: true, + fieldValue: 'NEW', + position: 0, + }, + { + fieldMetadataId: + objectMetadataMap[STANDARD_OBJECT_IDS.opportunity].fields[ + OPPORTUNITY_STANDARD_FIELD_IDS.stage + ], + isVisible: true, + fieldValue: 'SCREENING', + position: 1, + }, + { + fieldMetadataId: + objectMetadataMap[STANDARD_OBJECT_IDS.opportunity].fields[ + OPPORTUNITY_STANDARD_FIELD_IDS.stage + ], + isVisible: true, + fieldValue: 'MEETING', + position: 2, + }, + { + fieldMetadataId: + objectMetadataMap[STANDARD_OBJECT_IDS.opportunity].fields[ + OPPORTUNITY_STANDARD_FIELD_IDS.stage + ], + isVisible: true, + fieldValue: 'PROPOSAL', + position: 3, + }, + { + fieldMetadataId: + objectMetadataMap[STANDARD_OBJECT_IDS.opportunity].fields[ + OPPORTUNITY_STANDARD_FIELD_IDS.stage + ], + isVisible: true, + fieldValue: 'CUSTOMER', + position: 4, + }, + ], }; }; diff --git a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/tasks-by-status.view.ts b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/tasks-by-status.view.ts index 304b3ed011..e5e6665655 100644 --- a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/tasks-by-status.view.ts +++ b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/tasks-by-status.view.ts @@ -89,5 +89,34 @@ export const tasksByStatusView = ( }, */ ], + groups: [ + { + fieldMetadataId: + objectMetadataMap[STANDARD_OBJECT_IDS.task].fields[ + TASK_STANDARD_FIELD_IDS.status + ], + isVisible: true, + fieldValue: 'TODO', + position: 0, + }, + { + fieldMetadataId: + objectMetadataMap[STANDARD_OBJECT_IDS.task].fields[ + TASK_STANDARD_FIELD_IDS.status + ], + isVisible: true, + fieldValue: 'IN_PROGESS', + position: 1, + }, + { + fieldMetadataId: + objectMetadataMap[STANDARD_OBJECT_IDS.task].fields[ + TASK_STANDARD_FIELD_IDS.status + ], + isVisible: true, + fieldValue: 'DONE', + position: 2, + }, + ], }; }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts index ac9d921310..85a56e532b 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts @@ -368,6 +368,14 @@ export const VIEW_FIELD_STANDARD_FIELD_IDS = { view: '20202020-e8da-4521-afab-d6d231f9fa18', }; +export const VIEW_GROUP_STANDARD_FIELD_IDS = { + fieldMetadataId: '20202020-8f26-46ae-afed-fdacd7778682', + fieldValue: '20202020-175e-4596-b7a4-1cd9d14e5a30', + isVisible: '20202020-0fed-4b44-88fd-a064c4fcfce4', + position: '20202020-748e-4645-8f32-84aae7726c04', + view: '20202020-5bc7-4110-b23f-fb851fb133b4', +}; + export const VIEW_FILTER_STANDARD_FIELD_IDS = { fieldMetadataId: '20202020-c9aa-4c94-8d0e-9592f5008fb0', operand: '20202020-bd23-48c4-9fab-29d1ffb80310', @@ -392,6 +400,7 @@ export const VIEW_STANDARD_FIELD_IDS = { position: '20202020-e9db-4303-b271-e8250c450172', isCompact: '20202020-674e-4314-994d-05754ea7b22b', viewFields: '20202020-542b-4bdc-b177-b63175d48edf', + viewGroups: '20202020-e1a1-419f-ac81-1986a5ea59a8', viewFilters: '20202020-ff23-4154-b63c-21fb36cd0967', viewSorts: '20202020-891b-45c3-9fe1-80a75b4aa043', favorites: '20202020-c818-4a86-8284-9ec0ef0a59a5', diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids.ts index a13e78836b..98473a1732 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids.ts @@ -35,6 +35,7 @@ export const STANDARD_OBJECT_IDS = { taskTarget: '20202020-5a9a-44e8-95df-771cd06d0fb1', timelineActivity: '20202020-6736-4337-b5c4-8b39fae325a5', viewField: '20202020-4d19-4655-95bf-b2a04cf206d4', + viewGroup: '20202020-725f-47a4-8008-4255f9519f70', viewFilter: '20202020-6fb6-4631-aded-b7d67e952ec8', viewSort: '20202020-e46a-47a8-939a-e5d911f83531', view: '20202020-722e-4739-8e2c-0c372d661f49', diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-objects/index.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-objects/index.ts index 914ee491e4..98b5a062e0 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-objects/index.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-objects/index.ts @@ -28,6 +28,7 @@ import { BehavioralEventWorkspaceEntity } from 'src/modules/timeline/standard-ob import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity'; import { ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity'; import { ViewFilterWorkspaceEntity } from 'src/modules/view/standard-objects/view-filter.workspace-entity'; +import { ViewGroupWorkspaceEntity } from 'src/modules/view/standard-objects/view-group.workspace-entity'; import { ViewSortWorkspaceEntity } from 'src/modules/view/standard-objects/view-sort.workspace-entity'; import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity'; import { WebhookWorkspaceEntity } from 'src/modules/webhook/standard-objects/webhook.workspace-entity'; @@ -56,6 +57,7 @@ export const standardObjectMetadataDefinitions = [ FavoriteWorkspaceEntity, TimelineActivityWorkspaceEntity, ViewFieldWorkspaceEntity, + ViewGroupWorkspaceEntity, ViewFilterWorkspaceEntity, ViewSortWorkspaceEntity, ViewWorkspaceEntity, diff --git a/packages/twenty-server/src/modules/view/standard-objects/view-group.workspace-entity.ts b/packages/twenty-server/src/modules/view/standard-objects/view-group.workspace-entity.ts new file mode 100644 index 0000000000..93ff49b30f --- /dev/null +++ b/packages/twenty-server/src/modules/view/standard-objects/view-group.workspace-entity.ts @@ -0,0 +1,77 @@ +import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; +import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; +import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator'; +import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator'; +import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator'; +import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator'; +import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; +import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator'; +import { VIEW_GROUP_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; +import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; +import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity'; +import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator'; + +@WorkspaceEntity({ + standardId: STANDARD_OBJECT_IDS.viewGroup, + namePlural: 'viewGroups', + labelSingular: 'View Group', + labelPlural: 'View Groups', + description: '(System) View Groups', + icon: 'IconTag', +}) +@WorkspaceIsNotAuditLogged() +@WorkspaceIsSystem() +export class ViewGroupWorkspaceEntity extends BaseWorkspaceEntity { + @WorkspaceField({ + standardId: VIEW_GROUP_STANDARD_FIELD_IDS.fieldMetadataId, + type: FieldMetadataType.UUID, + label: 'Field Metadata Id', + description: 'View Group target field', + icon: 'IconTag', + }) + fieldMetadataId: string; + + @WorkspaceField({ + standardId: VIEW_GROUP_STANDARD_FIELD_IDS.isVisible, + type: FieldMetadataType.BOOLEAN, + label: 'Visible', + description: 'View Group visibility', + icon: 'IconEye', + defaultValue: true, + }) + isVisible: boolean; + + @WorkspaceField({ + standardId: VIEW_GROUP_STANDARD_FIELD_IDS.fieldValue, + type: FieldMetadataType.TEXT, + label: 'Field Value', + description: 'Group by this field value', + }) + fieldValue: string; + + @WorkspaceField({ + standardId: VIEW_GROUP_STANDARD_FIELD_IDS.position, + type: FieldMetadataType.NUMBER, + label: 'Position', + description: 'View Field position', + icon: 'IconList', + defaultValue: 0, + }) + position: number; + + @WorkspaceRelation({ + standardId: VIEW_GROUP_STANDARD_FIELD_IDS.view, + type: RelationMetadataType.MANY_TO_ONE, + label: 'View', + description: 'View Group related view', + icon: 'IconLayoutCollage', + inverseSideTarget: () => ViewWorkspaceEntity, + inverseSideFieldKey: 'viewGroups', + }) + @WorkspaceIsNullable() + view?: ViewWorkspaceEntity | null; + + @WorkspaceJoinColumn('view') + viewId: string | null; +} diff --git a/packages/twenty-server/src/modules/view/standard-objects/view.workspace-entity.ts b/packages/twenty-server/src/modules/view/standard-objects/view.workspace-entity.ts index 8c8431c00e..c673a1c1c8 100644 --- a/packages/twenty-server/src/modules/view/standard-objects/view.workspace-entity.ts +++ b/packages/twenty-server/src/modules/view/standard-objects/view.workspace-entity.ts @@ -18,6 +18,7 @@ import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/f import { ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity'; import { ViewFilterWorkspaceEntity } from 'src/modules/view/standard-objects/view-filter.workspace-entity'; import { ViewSortWorkspaceEntity } from 'src/modules/view/standard-objects/view-sort.workspace-entity'; +import { ViewGroupWorkspaceEntity } from 'src/modules/view/standard-objects/view-group.workspace-entity'; @WorkspaceEntity({ standardId: STANDARD_OBJECT_IDS.view, @@ -113,6 +114,18 @@ export class ViewWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceIsNullable() viewFields: Relation; + @WorkspaceRelation({ + standardId: VIEW_STANDARD_FIELD_IDS.viewGroups, + type: RelationMetadataType.ONE_TO_MANY, + label: 'View Groups', + description: 'View Groups', + icon: 'IconTag', + inverseSideTarget: () => ViewGroupWorkspaceEntity, + onDelete: RelationOnDeleteAction.SET_NULL, + }) + @WorkspaceIsNullable() + viewGroups: Relation; + @WorkspaceRelation({ standardId: VIEW_STANDARD_FIELD_IDS.viewFilters, type: RelationMetadataType.ONE_TO_MANY, From 084c15a564266a9fe5d8ec00a8ad8fa93712032b Mon Sep 17 00:00:00 2001 From: Shrey <147302693+shreykx@users.noreply.github.com> Date: Thu, 24 Oct 2024 19:47:52 +0530 Subject: [PATCH 02/75] feat: 20 guide (#8027) it's a local setup + hosting guide. --- .../3-write-selfthost-guide-blog-post-20.md | 1 + 1 file changed, 1 insertion(+) diff --git a/oss-gg/twenty-content-challenges/3-write-selfthost-guide-blog-post-20.md b/oss-gg/twenty-content-challenges/3-write-selfthost-guide-blog-post-20.md index fb1682663c..7ff4af84e7 100644 --- a/oss-gg/twenty-content-challenges/3-write-selfthost-guide-blog-post-20.md +++ b/oss-gg/twenty-content-challenges/3-write-selfthost-guide-blog-post-20.md @@ -20,6 +20,7 @@ Your turn 👇 » 21-October-2024 by [sateshcharan](https://oss.gg/sateshcharan) blog Link: [blog](https://dev.to/sateshcharan/streamlined-self-hosting-with-twenty-crm-1-click-docker-compose-setup-188o) +» 24-October-2024 by [Shrey](https://oss.gg/shreykx) guide link : [https://github.com/shreykx/newfolder/blob/8046bc7373b8632b7fc2bfa28c360b86f8890a81/twentyguide.md] » 23-October-2024 by [Thefool76](https://oss.gg/thefool76) blog Link: [blog](https://k5lo7h.hashnode.dev/a-detailed-guide-to-self-host-twenty-crm-on-you-local-server) » 24-October-2024 by [Khaan25](https://oss.gg/Khaan25) blog Link: [blog](https://medium.com/@ziaurzai/detailed-guide-on-self-hosting-twenty-crm-on-your-server-troubleshooting-and-best-practices-1f2ca15cd6eb) From 5fdf9acd08e56557a49c4890d01a8c60eee743bc Mon Sep 17 00:00:00 2001 From: Rajeev Dewangan <63413883+rajeevDewangan@users.noreply.github.com> Date: Thu, 24 Oct 2024 19:48:47 +0530 Subject: [PATCH 03/75] Update 3-create-custom-interfact-theme-20.md (#7974) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What are you solving : Create a custom theme for Twenty's interface Points : 300points Description : Duplicated the Figma file and customized the variables to create a new theme for Twenty’s interface. Proof : ![Screenshot 2024-10-22 232502](https://github.com/user-attachments/assets/a3e1c4ac-75ba-4583-90fe-99e04fc41a54) link : https://www.figma.com/design/XE21QdkFuy0IJHtmW7TURa/Twenty-(rajeevDewangan)?node-id=0-1&node-type=canvas&t=BYBulCT6hpJu6E8G-0 Co-authored-by: Thomas des Francs --- .../3-create-custom-interfact-theme-20.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oss-gg/twenty-design-challenges/3-create-custom-interfact-theme-20.md b/oss-gg/twenty-design-challenges/3-create-custom-interfact-theme-20.md index 736bb3f71f..2c8fb3b5c1 100644 --- a/oss-gg/twenty-design-challenges/3-create-custom-interfact-theme-20.md +++ b/oss-gg/twenty-design-challenges/3-create-custom-interfact-theme-20.md @@ -18,7 +18,7 @@ Your turn 👇 //////////////////////////// » 02-October-2024 by [yourhandle](https://oss.gg/yourhandle) Figma Link: [Figma](https://twenty.com/) - +» 22-October-2024 by [rajeevDewangan](https://oss.gg/rajeevDewangan) Figma Link: [Figma](https://www.figma.com/design/XE21QdkFuy0IJHtmW7TURa/Twenty-(rajeevDewangan)?node-id=0-1&node-type=canvas&t=BYBulCT6hpJu6E8G-0) » 24-October-2024 by [Khaan25](https://oss.gg/Khaan25) Figma Link: [Figma](https://www.figma.com/design/HqYQrzel3e2TjzujwfdCXZ/Twenty-(Copy)---Khaan25?node-id=478-19796&t=QTB8gzKTudbVNeNs-1) --- From 4ceee4ab8f592f379bdf049cde4f369f0a91928f Mon Sep 17 00:00:00 2001 From: "gitstart-app[bot]" <57568882+gitstart-app[bot]@users.noreply.github.com> Date: Thu, 24 Oct 2024 16:19:32 +0200 Subject: [PATCH 04/75] Migrate to twenty-ui - feedback/loader (#7997) This PR was created by [GitStart](https://gitstart.com/) to address the requirements from this ticket: [TWNTY-7528](https://clients.gitstart.com/twenty/5449/tickets/TWNTY-7528). --- ### Description - Move loader components to `twenty-ui` Fixes twentyhq/private-issues#90 --------- Co-authored-by: gitstart-twenty Co-authored-by: Charles Bochet --- packages/twenty-chrome-extension/src/options/Loading.tsx | 3 +-- .../src/modules/activities/emails/components/EmailLoader.tsx | 2 +- .../src/modules/auth/sign-in-up/components/SignInUpForm.tsx | 2 +- .../src/modules/ui/display/status/components/Status.tsx | 4 +--- packages/twenty-front/src/pages/auth/Invite.tsx | 3 +-- packages/twenty-front/src/pages/onboarding/ChooseYourPlan.tsx | 3 +-- .../twenty-front/src/pages/onboarding/CreateWorkspace.tsx | 3 +-- packages/twenty-ui/src/feedback/index.ts | 1 + .../src}/feedback/loader/components/Loader.tsx | 2 +- packages/twenty-ui/src/index.ts | 1 + 10 files changed, 10 insertions(+), 14 deletions(-) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/feedback/loader/components/Loader.tsx (96%) diff --git a/packages/twenty-chrome-extension/src/options/Loading.tsx b/packages/twenty-chrome-extension/src/options/Loading.tsx index 1fde24f2b9..a0543c9c6d 100644 --- a/packages/twenty-chrome-extension/src/options/Loading.tsx +++ b/packages/twenty-chrome-extension/src/options/Loading.tsx @@ -1,6 +1,5 @@ -import styled from '@emotion/styled'; - import { Loader } from '@/ui/display/loader/components/Loader'; +import styled from '@emotion/styled'; const StyledContainer = styled.div` align-items: center; diff --git a/packages/twenty-front/src/modules/activities/emails/components/EmailLoader.tsx b/packages/twenty-front/src/modules/activities/emails/components/EmailLoader.tsx index f301828ff4..444685852b 100644 --- a/packages/twenty-front/src/modules/activities/emails/components/EmailLoader.tsx +++ b/packages/twenty-front/src/modules/activities/emails/components/EmailLoader.tsx @@ -1,9 +1,9 @@ -import { Loader } from '@/ui/feedback/loader/components/Loader'; import { AnimatedPlaceholder, AnimatedPlaceholderEmptyContainer, AnimatedPlaceholderEmptyTextContainer, AnimatedPlaceholderEmptyTitle, + Loader, } from 'twenty-ui'; export const EmailLoader = ({ loadingText }: { loadingText?: string }) => ( diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpForm.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpForm.tsx index 38856d33d5..bed04b2e80 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpForm.tsx +++ b/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpForm.tsx @@ -9,7 +9,6 @@ import { SignInUpStep } from '@/auth/states/signInUpStepState'; import { isRequestingCaptchaTokenState } from '@/captcha/states/isRequestingCaptchaTokenState'; import { authProvidersState } from '@/client-config/states/authProvidersState'; import { captchaProviderState } from '@/client-config/states/captchaProviderState'; -import { Loader } from '@/ui/feedback/loader/components/Loader'; import { TextInput } from '@/ui/input/components/TextInput'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; @@ -23,6 +22,7 @@ import { IconGoogle, IconKey, IconMicrosoft, + Loader, MainButton, } from 'twenty-ui'; import { isDefined } from '~/utils/isDefined'; diff --git a/packages/twenty-front/src/modules/ui/display/status/components/Status.tsx b/packages/twenty-front/src/modules/ui/display/status/components/Status.tsx index 7c52e0eaaa..9a1bdaf165 100644 --- a/packages/twenty-front/src/modules/ui/display/status/components/Status.tsx +++ b/packages/twenty-front/src/modules/ui/display/status/components/Status.tsx @@ -1,7 +1,5 @@ import styled from '@emotion/styled'; -import { ThemeColor, themeColorSchema } from 'twenty-ui'; - -import { Loader } from '@/ui/feedback/loader/components/Loader'; +import { Loader, ThemeColor, themeColorSchema } from 'twenty-ui'; const StyledStatus = styled.h3<{ color: ThemeColor; diff --git a/packages/twenty-front/src/pages/auth/Invite.tsx b/packages/twenty-front/src/pages/auth/Invite.tsx index 737faa3442..d83e857901 100644 --- a/packages/twenty-front/src/pages/auth/Invite.tsx +++ b/packages/twenty-front/src/pages/auth/Invite.tsx @@ -5,13 +5,12 @@ import { SignInUpForm } from '@/auth/sign-in-up/components/SignInUpForm'; import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm'; import { useWorkspaceFromInviteHash } from '@/auth/sign-in-up/hooks/useWorkspaceFromInviteHash'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; -import { Loader } from '@/ui/feedback/loader/components/Loader'; import { useWorkspaceSwitching } from '@/ui/navigation/navigation-drawer/hooks/useWorkspaceSwitching'; import styled from '@emotion/styled'; import { useMemo } from 'react'; import { useSearchParams } from 'react-router-dom'; import { useRecoilValue } from 'recoil'; -import { AnimatedEaseIn, MainButton } from 'twenty-ui'; +import { AnimatedEaseIn, Loader, MainButton } from 'twenty-ui'; import { useAddUserToWorkspaceByInviteTokenMutation, useAddUserToWorkspaceMutation, diff --git a/packages/twenty-front/src/pages/onboarding/ChooseYourPlan.tsx b/packages/twenty-front/src/pages/onboarding/ChooseYourPlan.tsx index 19f1521e14..d40907bad7 100644 --- a/packages/twenty-front/src/pages/onboarding/ChooseYourPlan.tsx +++ b/packages/twenty-front/src/pages/onboarding/ChooseYourPlan.tsx @@ -5,7 +5,6 @@ import { SubscriptionBenefit } from '@/billing/components/SubscriptionBenefit'; import { SubscriptionCard } from '@/billing/components/SubscriptionCard'; import { billingState } from '@/client-config/states/billingState'; import { AppPath } from '@/types/AppPath'; -import { Loader } from '@/ui/feedback/loader/components/Loader'; import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { CardPicker } from '@/ui/input/components/CardPicker'; @@ -13,7 +12,7 @@ import styled from '@emotion/styled'; import { isNonEmptyString, isNumber } from '@sniptt/guards'; import { useState } from 'react'; import { useRecoilValue } from 'recoil'; -import { ActionLink, CAL_LINK, MainButton } from 'twenty-ui'; +import { ActionLink, CAL_LINK, Loader, MainButton } from 'twenty-ui'; import { ProductPriceEntity, SubscriptionInterval, diff --git a/packages/twenty-front/src/pages/onboarding/CreateWorkspace.tsx b/packages/twenty-front/src/pages/onboarding/CreateWorkspace.tsx index bb8f7d1d49..a0c861683f 100644 --- a/packages/twenty-front/src/pages/onboarding/CreateWorkspace.tsx +++ b/packages/twenty-front/src/pages/onboarding/CreateWorkspace.tsx @@ -4,7 +4,7 @@ import { useCallback } from 'react'; import { Controller, SubmitHandler, useForm } from 'react-hook-form'; import { useSetRecoilState } from 'recoil'; import { Key } from 'ts-key-enum'; -import { H2Title, MainButton } from 'twenty-ui'; +import { H2Title, Loader, MainButton } from 'twenty-ui'; import { z } from 'zod'; import { SubTitle } from '@/auth/components/SubTitle'; @@ -14,7 +14,6 @@ import { FIND_MANY_OBJECT_METADATA_ITEMS } from '@/object-metadata/graphql/queri import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient'; import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus'; import { WorkspaceLogoUploader } from '@/settings/workspace/components/WorkspaceLogoUploader'; -import { Loader } from '@/ui/feedback/loader/components/Loader'; import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { TextInputV2 } from '@/ui/input/components/TextInputV2'; diff --git a/packages/twenty-ui/src/feedback/index.ts b/packages/twenty-ui/src/feedback/index.ts index dd4bb77393..cabe6fa446 100644 --- a/packages/twenty-ui/src/feedback/index.ts +++ b/packages/twenty-ui/src/feedback/index.ts @@ -1,3 +1,4 @@ +export * from './loader/components/Loader'; export * from './progress-bar/components/CircularProgressBar'; export * from './progress-bar/components/ProgressBar'; export * from './progress-bar/hooks/useProgressAnimation'; diff --git a/packages/twenty-front/src/modules/ui/feedback/loader/components/Loader.tsx b/packages/twenty-ui/src/feedback/loader/components/Loader.tsx similarity index 96% rename from packages/twenty-front/src/modules/ui/feedback/loader/components/Loader.tsx rename to packages/twenty-ui/src/feedback/loader/components/Loader.tsx index 1df850e0c5..0ada8423f7 100644 --- a/packages/twenty-front/src/modules/ui/feedback/loader/components/Loader.tsx +++ b/packages/twenty-ui/src/feedback/loader/components/Loader.tsx @@ -1,6 +1,6 @@ import styled from '@emotion/styled'; +import { ThemeColor } from '@ui/theme'; import { motion } from 'framer-motion'; -import { ThemeColor } from 'twenty-ui'; const StyledLoaderContainer = styled.div<{ color?: ThemeColor; diff --git a/packages/twenty-ui/src/index.ts b/packages/twenty-ui/src/index.ts index 58024c5a0d..aa5fc4e653 100644 --- a/packages/twenty-ui/src/index.ts +++ b/packages/twenty-ui/src/index.ts @@ -1,6 +1,7 @@ export * from './accessibility'; export * from './components'; export * from './display'; +export * from './feedback'; export * from './input'; export * from './feedback'; export * from './layout'; From 4e8d8ce744a8acaa8a3152418daf5ef82b611d3c Mon Sep 17 00:00:00 2001 From: Harsh Singh Date: Thu, 24 Oct 2024 20:03:50 +0530 Subject: [PATCH 05/75] fix: relation picker should not move once openened (#8026) Fixes: #7959 ### Problem - When searching in the dropdown, the results list would shrink based on matching items - This dynamic height change caused the dropdown to flip its position on each keystroke ### Solution - Added ```hasMinHeight``` as optional props to the ```DropdownMenuItemsContainer``` to maintain consistent height - This prevents unwanted position recalculations and flipping while user types - The dropdown now stays in its initial position throughout the search interaction [Screencast from 2024-10-24 15-43-03.webm](https://github.com/user-attachments/assets/741317b7-fc5e-4874-8221-aa626a1a1747) --- .../relation-picker/components/MultiRecordSelect.tsx | 2 +- .../dropdown/components/DropdownMenuItemsContainer.tsx | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/components/MultiRecordSelect.tsx b/packages/twenty-front/src/modules/object-record/relation-picker/components/MultiRecordSelect.tsx index 266defb445..fedb01ae89 100644 --- a/packages/twenty-front/src/modules/object-record/relation-picker/components/MultiRecordSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/relation-picker/components/MultiRecordSelect.tsx @@ -113,7 +113,7 @@ export const MultiRecordSelect = ({ autoFocus /> - + {recordMultiSelectIsLoading ? ( ) : ( diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuItemsContainer.tsx b/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuItemsContainer.tsx index 652e43f148..86f43bbcd2 100644 --- a/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuItemsContainer.tsx +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuItemsContainer.tsx @@ -3,6 +3,7 @@ import styled from '@emotion/styled'; import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; const StyledDropdownMenuItemsExternalContainer = styled.div<{ + hasMinHeight?: boolean; hasMaxHeight?: boolean; }>` --padding: ${({ theme }) => theme.spacing(1)}; @@ -12,7 +13,7 @@ const StyledDropdownMenuItemsExternalContainer = styled.div<{ flex-direction: column; gap: 2px; - height: 100%; + min-height: ${({ hasMinHeight }) => (hasMinHeight ? '150px' : '100%')}; max-height: ${({ hasMaxHeight }) => (hasMaxHeight ? '188px' : 'none')}; overflow-y: auto; @@ -37,13 +38,18 @@ const StyledDropdownMenuItemsInternalContainer = styled.div` export const DropdownMenuItemsContainer = ({ children, + hasMinHeight, hasMaxHeight, }: { children: React.ReactNode; + hasMinHeight?: boolean; hasMaxHeight?: boolean; }) => { return ( - + {hasMaxHeight ? ( From 9b5d0e7850aeef8ecf1d1d1cf072f809d647a0d7 Mon Sep 17 00:00:00 2001 From: Bhavesh Mishra <69065938+thefool76@users.noreply.github.com> Date: Thu, 24 Oct 2024 20:05:41 +0530 Subject: [PATCH 06/75] Oss.gg Content creation promotional video on social site (#8033) I have created a promotional video of Twenty crm on YouTube and have shared it features. [Click here](https://youtube.com/shorts/lC4oqm7UlCI?si=Md-nsfK9F6Shzjkv) Points 750 --- .../4-create-promotional-video-20-share.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/oss-gg/twenty-content-challenges/4-create-promotional-video-20-share.md b/oss-gg/twenty-content-challenges/4-create-promotional-video-20-share.md index e52cb43a42..3fd28c143b 100644 --- a/oss-gg/twenty-content-challenges/4-create-promotional-video-20-share.md +++ b/oss-gg/twenty-content-challenges/4-create-promotional-video-20-share.md @@ -18,4 +18,6 @@ Your turn 👇 » 02-October-2024 by [yourhandle](https://oss.gg/yourhandle) video Link: [video](https://twenty.com/) ---- \ No newline at end of file +» 24-October-2024 by [Thefool76](https://oss.gg/thefool76) video Link: [video](https://youtube.com/shorts/lC4oqm7UlCI?si=Md-nsfK9F6Shzjkv) + +--- From 1dfeba39ebdef51b64cdcb429008331bcf0f3ec0 Mon Sep 17 00:00:00 2001 From: "gitstart-app[bot]" <57568882+gitstart-app[bot]@users.noreply.github.com> Date: Thu, 24 Oct 2024 16:36:06 +0200 Subject: [PATCH 07/75] Migrate to twenty-ui - layout/card (#8003) This PR was created by [GitStart](https://gitstart.com/) to address the requirements from this ticket: [TWNTY-7532](https://clients.gitstart.com/twenty/5449/tickets/TWNTY-7532). --- ### Description Migrate: - Card - CardContent - CardFooter - CardHeader ### Demo Card in Storybook ![](https://assets-service.gitstart.com/4814/d6759b99-7d5f-4177-acdf-1c57786330a3.png) ###### Fixes twentyhq/private-issues#86 --------- Co-authored-by: gitstart-twenty Co-authored-by: Charles Bochet --- .../calendar/components/CalendarDayCardContent.tsx | 2 +- .../calendar/components/CalendarEventRow.tsx | 4 ++-- .../calendar/components/CalendarMonthCard.tsx | 2 +- .../modules/activities/components/ActivityList.tsx | 2 +- .../src/modules/activities/components/ActivityRow.tsx | 2 +- .../timeline-activities/rows/components/EventCard.tsx | 2 +- .../SettingsAccountsCalendarChannelDetails.tsx | 3 +-- .../components/SettingsAccountsListEmptyStateCard.tsx | 5 +---- .../SettingsAccountsMessageChannelDetails.tsx | 3 +-- .../components/SettingsAccountsRadioSettingsCard.tsx | 3 +-- .../src/modules/settings/components/SettingsCard.tsx | 4 +--- .../modules/settings/components/SettingsListCard.tsx | 6 ++---- .../components/SettingsListItemCardContent.tsx | 6 ++---- .../settings/components/SettingsListSkeletonCard.tsx | 2 +- .../settings/components/SettingsOptionCardContent.tsx | 3 +-- .../modules/settings/components/SettingsRadioCard.tsx | 3 +-- .../settings/components/SettingsSummaryCard.tsx | 5 ++--- .../components/SettingsDataModelPreviewFormCard.tsx | 3 +-- .../components/SettingsDataModelFieldBooleanForm.tsx | 5 ++--- .../components/SettingsDataModelFieldCurrencyForm.tsx | 2 +- .../components/SettingsDataModelFieldDateForm.tsx | 3 +-- .../components/SettingsDataModelFieldNumberForm.tsx | 2 +- .../components/SettingsDataModelFieldSelectForm.tsx | 11 ++++++++--- .../components/SettingsDataModelFieldPreviewCard.tsx | 6 +++--- .../objects/components/SettingsObjectCoverImage.tsx | 4 +--- .../SettingsDataModelObjectSettingsFormCard.tsx | 3 +-- .../components/SettingsIntegrationPreview.tsx | 3 +-- ...ttingsSSOIdentitiesProvidersListEmptyStateCard.tsx | 5 +---- .../components/SettingsSecurityOptionsList.tsx | 3 +-- .../src}/layout/card/components/Card.tsx | 0 .../src}/layout/card/components/CardContent.tsx | 0 .../src}/layout/card/components/CardFooter.tsx | 0 .../src}/layout/card/components/CardHeader.tsx | 0 .../card/components/__stories__/Card.stories.tsx | 2 +- packages/twenty-ui/src/layout/index.ts | 4 ++++ 35 files changed, 48 insertions(+), 65 deletions(-) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/layout/card/components/Card.tsx (100%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/layout/card/components/CardContent.tsx (100%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/layout/card/components/CardFooter.tsx (100%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/layout/card/components/CardHeader.tsx (100%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/layout/card/components/__stories__/Card.stories.tsx (94%) diff --git a/packages/twenty-front/src/modules/activities/calendar/components/CalendarDayCardContent.tsx b/packages/twenty-front/src/modules/activities/calendar/components/CalendarDayCardContent.tsx index ca656f3b52..1ccf037161 100644 --- a/packages/twenty-front/src/modules/activities/calendar/components/CalendarDayCardContent.tsx +++ b/packages/twenty-front/src/modules/activities/calendar/components/CalendarDayCardContent.tsx @@ -4,7 +4,7 @@ import { differenceInSeconds, endOfDay, format } from 'date-fns'; import { CalendarEventRow } from '@/activities/calendar/components/CalendarEventRow'; import { getCalendarEventStartDate } from '@/activities/calendar/utils/getCalendarEventStartDate'; -import { CardContent } from '@/ui/layout/card/components/CardContent'; +import { CardContent } from 'twenty-ui'; import { TimelineCalendarEvent } from '~/generated/graphql'; type CalendarDayCardContentProps = { diff --git a/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventRow.tsx b/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventRow.tsx index cb9548f8ba..5874898e00 100644 --- a/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventRow.tsx +++ b/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventRow.tsx @@ -9,6 +9,8 @@ import { IconArrowRight, IconLock, isDefined, + Card, + CardContent, } from 'twenty-ui'; import { CalendarCurrentEventCursor } from '@/activities/calendar/components/CalendarCurrentEventCursor'; @@ -18,8 +20,6 @@ import { getCalendarEventEndDate } from '@/activities/calendar/utils/getCalendar import { getCalendarEventStartDate } from '@/activities/calendar/utils/getCalendarEventStartDate'; import { hasCalendarEventEnded } from '@/activities/calendar/utils/hasCalendarEventEnded'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; -import { Card } from '@/ui/layout/card/components/Card'; -import { CardContent } from '@/ui/layout/card/components/CardContent'; import { CalendarChannelVisibility, TimelineCalendarEvent, diff --git a/packages/twenty-front/src/modules/activities/calendar/components/CalendarMonthCard.tsx b/packages/twenty-front/src/modules/activities/calendar/components/CalendarMonthCard.tsx index f42f2e35f5..97ca1d0297 100644 --- a/packages/twenty-front/src/modules/activities/calendar/components/CalendarMonthCard.tsx +++ b/packages/twenty-front/src/modules/activities/calendar/components/CalendarMonthCard.tsx @@ -2,7 +2,7 @@ import { useContext } from 'react'; import { CalendarDayCardContent } from '@/activities/calendar/components/CalendarDayCardContent'; import { CalendarContext } from '@/activities/calendar/contexts/CalendarContext'; -import { Card } from '@/ui/layout/card/components/Card'; +import { Card } from 'twenty-ui'; type CalendarMonthCardProps = { dayTimes: number[]; diff --git a/packages/twenty-front/src/modules/activities/components/ActivityList.tsx b/packages/twenty-front/src/modules/activities/components/ActivityList.tsx index b8b8b2f61d..0bb0e367e5 100644 --- a/packages/twenty-front/src/modules/activities/components/ActivityList.tsx +++ b/packages/twenty-front/src/modules/activities/components/ActivityList.tsx @@ -1,5 +1,5 @@ -import { Card } from '@/ui/layout/card/components/Card'; import styled from '@emotion/styled'; +import { Card } from 'twenty-ui'; const StyledList = styled(Card)` & > :not(:last-child) { diff --git a/packages/twenty-front/src/modules/activities/components/ActivityRow.tsx b/packages/twenty-front/src/modules/activities/components/ActivityRow.tsx index 00fdbb1a68..075758fc82 100644 --- a/packages/twenty-front/src/modules/activities/components/ActivityRow.tsx +++ b/packages/twenty-front/src/modules/activities/components/ActivityRow.tsx @@ -1,5 +1,5 @@ -import { CardContent } from '@/ui/layout/card/components/CardContent'; import styled from '@emotion/styled'; +import { CardContent } from 'twenty-ui'; import React from 'react'; const StyledRowContent = styled(CardContent)<{ diff --git a/packages/twenty-front/src/modules/activities/timeline-activities/rows/components/EventCard.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/components/EventCard.tsx index 67e4f78cd7..52b8fa0de8 100644 --- a/packages/twenty-front/src/modules/activities/timeline-activities/rows/components/EventCard.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/rows/components/EventCard.tsx @@ -1,6 +1,6 @@ import styled from '@emotion/styled'; -import { Card } from '@/ui/layout/card/components/Card'; +import { Card } from 'twenty-ui'; type EventCardProps = { children: React.ReactNode; diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarChannelDetails.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarChannelDetails.tsx index 3152e98ba0..5d0fab05d6 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarChannelDetails.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarChannelDetails.tsx @@ -3,10 +3,9 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { SettingsAccountsEventVisibilitySettingsCard } from '@/settings/accounts/components/SettingsAccountsCalendarVisibilitySettingsCard'; import { SettingsOptionCardContent } from '@/settings/components/SettingsOptionCardContent'; -import { Card } from '@/ui/layout/card/components/Card'; import styled from '@emotion/styled'; import { Section } from '@react-email/components'; -import { H2Title, Toggle } from 'twenty-ui'; +import { H2Title, Toggle, Card } from 'twenty-ui'; import { CalendarChannelVisibility } from '~/generated-metadata/graphql'; const StyledDetailsContainer = styled.div` diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsListEmptyStateCard.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsListEmptyStateCard.tsx index 981bbcb6d5..8500264c41 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsListEmptyStateCard.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsListEmptyStateCard.tsx @@ -1,10 +1,7 @@ import styled from '@emotion/styled'; -import { Button, IconGoogle } from 'twenty-ui'; +import { Button, Card, CardContent, CardHeader, IconGoogle } from 'twenty-ui'; import { useTriggerGoogleApisOAuth } from '@/settings/accounts/hooks/useTriggerGoogleApisOAuth'; -import { Card } from '@/ui/layout/card/components/Card'; -import { CardContent } from '@/ui/layout/card/components/CardContent'; -import { CardHeader } from '@/ui/layout/card/components/CardHeader'; const StyledHeader = styled(CardHeader)` align-items: center; diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsMessageChannelDetails.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsMessageChannelDetails.tsx index cfe203a15a..3d4c81d0ed 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsMessageChannelDetails.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsMessageChannelDetails.tsx @@ -1,5 +1,5 @@ import styled from '@emotion/styled'; -import { H2Title, Toggle } from 'twenty-ui'; +import { H2Title, Toggle, Card } from 'twenty-ui'; import { MessageChannel, @@ -10,7 +10,6 @@ import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { SettingsAccountsMessageAutoCreationCard } from '@/settings/accounts/components/SettingsAccountsMessageAutoCreationCard'; import { SettingsAccountsMessageVisibilityCard } from '@/settings/accounts/components/SettingsAccountsMessageVisibilityCard'; import { SettingsOptionCardContent } from '@/settings/components/SettingsOptionCardContent'; -import { Card } from '@/ui/layout/card/components/Card'; import { Section } from '@/ui/layout/section/components/Section'; import { MessageChannelVisibility } from '~/generated-metadata/graphql'; diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsRadioSettingsCard.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsRadioSettingsCard.tsx index aaf856456f..81c8a66297 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsRadioSettingsCard.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsRadioSettingsCard.tsx @@ -2,8 +2,7 @@ import styled from '@emotion/styled'; import { ReactNode } from 'react'; import { Radio } from '@/ui/input/components/Radio'; -import { Card } from '@/ui/layout/card/components/Card'; -import { CardContent } from '@/ui/layout/card/components/CardContent'; +import { Card, CardContent } from 'twenty-ui'; type SettingsAccountsRadioSettingsCardProps