From 3ad1113173bf85b47aea9348a42977b787f82280 Mon Sep 17 00:00:00 2001 From: Harsh Singh Date: Wed, 27 Nov 2024 22:36:11 +0530 Subject: [PATCH 1/7] fix: scroll dropdown listing in hidden fields (#8738) Fixes: #8716 [Screencast from 2024-11-25 22-06-24.webm](https://github.com/user-attachments/assets/35bd66cc-942f-4903-abda-0d67a75b6582) --------- Co-authored-by: Lucas Bordeau --- .../RecordTableColumnHeadDropdownMenu.tsx | 79 ++++++++++--------- .../components/RecordTableHeaderCell.tsx | 2 + .../RecordTableHeaderPlusButtonContent.tsx | 29 ++++--- .../layout/dropdown/components/Dropdown.tsx | 46 +++++++---- .../dropdown/components/DropdownMenu.tsx | 2 + .../components/DropdownMenuItemsContainer.tsx | 20 +---- .../components/DropdownMenuSeparator.tsx | 2 +- .../DropdownComponeInstanceContext.ts | 4 + .../dropdownMaxHeightComponentStateV2.ts | 10 +++ 9 files changed, 106 insertions(+), 88 deletions(-) create mode 100644 packages/twenty-front/src/modules/ui/layout/dropdown/contexts/DropdownComponeInstanceContext.ts create mode 100644 packages/twenty-front/src/modules/ui/layout/dropdown/states/dropdownMaxHeightComponentStateV2.ts diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHeadDropdownMenu.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHeadDropdownMenu.tsx index 124d9a388ef7..31bcc917f192 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHeadDropdownMenu.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHeadDropdownMenu.tsx @@ -15,6 +15,7 @@ import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { onToggleColumnFilterComponentState } from '@/object-record/record-table/states/onToggleColumnFilterComponentState'; import { onToggleColumnSortComponentState } from '@/object-record/record-table/states/onToggleColumnSortComponentState'; import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; +import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useTableColumns } from '../../hooks/useTableColumns'; import { ColumnDefinition } from '../../types/ColumnDefinition'; @@ -91,43 +92,45 @@ export const RecordTableColumnHeadDropdownMenu = ({ const canHide = column.isLabelIdentifier !== true; return ( - - {isFilterable && ( - - )} - {isSortable && ( - - )} - {showSeparator && } - {canMoveLeft && ( - - )} - {canMoveRight && ( - - )} - {canHide && ( - - )} - + + + {isFilterable && ( + + )} + {isSortable && ( + + )} + {showSeparator && } + {canMoveLeft && ( + + )} + {canMoveRight && ( + + )} + {canHide && ( + + )} + + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx index 9b8ea55ccabe..5a3e4299a445 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx @@ -65,6 +65,8 @@ const StyledColumnHeaderCell = styled.th<{ }`; } }}; + + // TODO: refactor this, each component should own its CSS div { overflow: hidden; } diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderPlusButtonContent.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderPlusButtonContent.tsx index 6ffb331a05d5..ff78e7c3f094 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderPlusButtonContent.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderPlusButtonContent.tsx @@ -13,6 +13,7 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; +import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; export const RecordTableHeaderPlusButtonContent = () => { @@ -41,21 +42,19 @@ export const RecordTableHeaderPlusButtonContent = () => { return ( <> - {hiddenTableColumns.length > 0 && ( - <> - - {hiddenTableColumns.map((column) => ( - handleAddColumn(column)} - LeftIcon={getIcon(column.iconName)} - text={column.label} - /> - ))} - - - - )} + + + {hiddenTableColumns.map((column) => ( + handleAddColumn(column)} + LeftIcon={getIcon(column.iconName)} + text={column.label} + /> + ))} + + + { - elements.floating.style.maxHeight = - availableHeight >= elements.floating.scrollHeight - ? '' - : `${availableHeight}px`; - - elements.floating.style.height = 'auto'; + apply: ({ availableHeight }) => { + flushSync(() => { + setDropdownMaxHeight(availableHeight); + }); }, boundary: document.querySelector('#root') ?? undefined, }), @@ -149,8 +154,15 @@ export const Dropdown = ({ [closeDropdown, isDropdownOpen], ); + const dropdownMenuStyles = { + ...floatingStyles, + maxHeight: dropdownMaxHeight, + }; + return ( - <> +
{clickableComponent && ( @@ -175,7 +187,7 @@ export const Dropdown = ({ width={dropdownMenuWidth ?? dropdownWidth} data-select-disable ref={refs.setFloating} - style={floatingStyles} + style={dropdownMenuStyles} > {dropdownComponents} @@ -187,7 +199,7 @@ export const Dropdown = ({ width={dropdownMenuWidth ?? dropdownWidth} data-select-disable ref={refs.setFloating} - style={floatingStyles} + style={dropdownMenuStyles} > {dropdownComponents} @@ -199,6 +211,6 @@ export const Dropdown = ({
- +
); }; diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenu.tsx b/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenu.tsx index 2510c90c619a..26ef95317184 100644 --- a/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenu.tsx +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenu.tsx @@ -26,6 +26,8 @@ const StyledDropdownMenu = styled.div<{ z-index: 30; width: ${({ width = 200 }) => typeof width === 'number' ? `${width}px` : width}; + + overflow: hidden; `; export const DropdownMenu = StyledDropdownMenu; 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 1338d1c4d892..ca642bcc6d1d 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 @@ -1,7 +1,5 @@ import styled from '@emotion/styled'; -import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; - const StyledDropdownMenuItemsExternalContainer = styled.div<{ hasMaxHeight?: boolean; }>` @@ -18,10 +16,6 @@ const StyledDropdownMenuItemsExternalContainer = styled.div<{ width: calc(100% - 2 * var(--padding)); `; -const StyledScrollWrapper = styled(ScrollWrapper)` - width: 100%; -`; - const StyledDropdownMenuItemsInternalContainer = styled.div` align-items: stretch; display: flex; @@ -48,17 +42,9 @@ export const DropdownMenuItemsContainer = ({ hasMaxHeight={hasMaxHeight} className={className} > - {hasMaxHeight ? ( - - - {children} - - - ) : ( - - {children} - - )} + + {children} + ); }; diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuSeparator.tsx b/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuSeparator.tsx index 8c8501d45973..f2599f103c48 100644 --- a/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuSeparator.tsx +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuSeparator.tsx @@ -2,7 +2,7 @@ import styled from '@emotion/styled'; const StyledDropdownMenuSeparator = styled.div` background-color: ${({ theme }) => theme.border.color.light}; - height: 1px; + min-height: 1px; width: 100%; `; diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/contexts/DropdownComponeInstanceContext.ts b/packages/twenty-front/src/modules/ui/layout/dropdown/contexts/DropdownComponeInstanceContext.ts new file mode 100644 index 000000000000..243cbde49439 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/contexts/DropdownComponeInstanceContext.ts @@ -0,0 +1,4 @@ +import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext'; + +export const DropdownComponentInstanceContext = + createComponentInstanceContext(); diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/states/dropdownMaxHeightComponentStateV2.ts b/packages/twenty-front/src/modules/ui/layout/dropdown/states/dropdownMaxHeightComponentStateV2.ts new file mode 100644 index 000000000000..9cd8185c09d1 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/states/dropdownMaxHeightComponentStateV2.ts @@ -0,0 +1,10 @@ +import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponeInstanceContext'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; + +export const dropdownMaxHeightComponentStateV2 = createComponentStateV2< + number | undefined +>({ + key: 'dropdownMaxHeightComponentStateV2', + componentInstanceContext: DropdownComponentInstanceContext, + defaultValue: undefined, +}); From 2fab2266d5597efe93732416296f3de10fdc6fbb Mon Sep 17 00:00:00 2001 From: Antoine Moreaux Date: Wed, 27 Nov 2024 19:26:45 +0100 Subject: [PATCH 2/7] feat(front): improve logo component (#8720) --- .../src/modules/auth/components/Logo.tsx | 53 +++++++++---------- .../twenty-front/src/pages/auth/Invite.tsx | 2 +- .../src/utils/image/getImageAbsoluteURI.ts | 23 +++++--- 3 files changed, 43 insertions(+), 35 deletions(-) diff --git a/packages/twenty-front/src/modules/auth/components/Logo.tsx b/packages/twenty-front/src/modules/auth/components/Logo.tsx index 34ddf069cd91..9bc9c94aee94 100644 --- a/packages/twenty-front/src/modules/auth/components/Logo.tsx +++ b/packages/twenty-front/src/modules/auth/components/Logo.tsx @@ -3,65 +3,62 @@ import styled from '@emotion/styled'; import { getImageAbsoluteURI } from '~/utils/image/getImageAbsoluteURI'; type LogoProps = { - workspaceLogo?: string | null; + primaryLogo?: string | null; + secondaryLogo?: string | null; }; const StyledContainer = styled.div` - height: 48px; + height: ${({ theme }) => theme.spacing(12)}; margin-bottom: ${({ theme }) => theme.spacing(4)}; margin-top: ${({ theme }) => theme.spacing(4)}; position: relative; - width: 48px; + width: ${({ theme }) => theme.spacing(12)}; `; -const StyledTwentyLogo = styled.img` +const StyledSecondaryLogo = styled.img` border-radius: ${({ theme }) => theme.border.radius.xs}; - height: 24px; - width: 24px; + height: ${({ theme }) => theme.spacing(6)}; + width: ${({ theme }) => theme.spacing(6)}; `; -const StyledTwentyLogoContainer = styled.div` +const StyledSecondaryLogoContainer = styled.div` align-items: center; background-color: ${({ theme }) => theme.background.primary}; border-radius: ${({ theme }) => theme.border.radius.sm}; bottom: ${({ theme }) => `-${theme.spacing(3)}`}; display: flex; - height: 28px; + height: ${({ theme }) => theme.spacing(7)}; justify-content: center; position: absolute; right: ${({ theme }) => `-${theme.spacing(3)}`}; - width: 28px; + width: ${({ theme }) => theme.spacing(7)}; `; -type StyledMainLogoProps = { - logo?: string | null; -}; - -const StyledMainLogo = styled.div` - background: url(${(props) => props.logo}); +const StyledPrimaryLogo = styled.div<{ src: string }>` + background: url(${(props) => props.src}); background-size: cover; height: 100%; - width: 100%; `; -export const Logo = ({ workspaceLogo }: LogoProps) => { - if (!workspaceLogo) { - return ( - - - - ); - } +export const Logo = (props: LogoProps) => { + const defaultPrimaryLogoUrl = `${window.location.origin}/icons/android/android-launchericon-192-192.png`; + + const primaryLogoUrl = getImageAbsoluteURI( + props.primaryLogo ?? defaultPrimaryLogoUrl, + ); + const secondaryLogoUrl = getImageAbsoluteURI(props.secondaryLogo); return ( - - - - + + {secondaryLogoUrl && ( + + + + )} ); }; diff --git a/packages/twenty-front/src/pages/auth/Invite.tsx b/packages/twenty-front/src/pages/auth/Invite.tsx index d83e857901ea..d2488f480428 100644 --- a/packages/twenty-front/src/pages/auth/Invite.tsx +++ b/packages/twenty-front/src/pages/auth/Invite.tsx @@ -74,7 +74,7 @@ export const Invite = () => { return ( <> - + {title} {isDefined(currentWorkspace) ? ( diff --git a/packages/twenty-front/src/utils/image/getImageAbsoluteURI.ts b/packages/twenty-front/src/utils/image/getImageAbsoluteURI.ts index 819567796773..eb2665ec9411 100644 --- a/packages/twenty-front/src/utils/image/getImageAbsoluteURI.ts +++ b/packages/twenty-front/src/utils/image/getImageAbsoluteURI.ts @@ -1,15 +1,26 @@ import { REACT_APP_SERVER_BASE_URL } from '~/config'; -export const getImageAbsoluteURI = (imageUrl?: string | null) => { +type ImageAbsoluteURI = T extends string + ? string + : null; + +export const getImageAbsoluteURI = ( + imageUrl: T, +): ImageAbsoluteURI => { if (!imageUrl) { - return null; + return null as ImageAbsoluteURI; } - if (imageUrl?.startsWith('https:') || imageUrl?.startsWith('http:')) { - return imageUrl; + if (imageUrl.startsWith('https:') || imageUrl.startsWith('http:')) { + return imageUrl as ImageAbsoluteURI; } - const serverFilesUrl = REACT_APP_SERVER_BASE_URL; + const serverFilesUrl = new URL(REACT_APP_SERVER_BASE_URL); + + serverFilesUrl.pathname = `/files/`; + serverFilesUrl.pathname += imageUrl.startsWith('/') + ? imageUrl.slice(1) + : imageUrl; - return `${serverFilesUrl}/files/${imageUrl}`; + return serverFilesUrl.toString() as ImageAbsoluteURI; }; From c9fd19469516527de690d9bfaaeefea0c3e32ad6 Mon Sep 17 00:00:00 2001 From: Weiko Date: Thu, 28 Nov 2024 10:21:03 +0100 Subject: [PATCH 3/7] Fix index renaming (#8771) Fixes https://github.com/twentyhq/twenty/issues/8760 ## Context Index names are based on table names and column names, which means renaming an object (or a field) should also trigger a recompute of index names. As of today it raises a bug where you can't create an object with a name that was previously used. I also took the occasion to refactor a bit the part where we create and update (after renaming) relations. Basically the only relations we want to affect are standard relations so I've aligned the logic with sync-metadata which uses standardId as a source of truth to simplify the code. Note: We don't create index for custom relations Next step should be to do that and update that code to update the index name as well. Also note that we need to update the sync-metadata logic for that as well --- .../index-metadata/index-metadata.service.ts | 101 +++++ .../object-metadata/object-metadata.module.ts | 2 + .../object-metadata.service.ts | 113 +++--- .../object-metadata-migration.service.ts | 201 +++------ .../object-metadata-relation.service.ts | 381 +++++++++++++----- 5 files changed, 486 insertions(+), 312 deletions(-) diff --git a/packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.service.ts index e6d095e9ec55..c0ab6d4058fb 100644 --- a/packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.service.ts @@ -13,6 +13,7 @@ import { generateDeterministicIndexName } from 'src/engine/metadata-modules/inde import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util'; import { + WorkspaceMigrationIndexAction, WorkspaceMigrationIndexActionType, WorkspaceMigrationTableAction, WorkspaceMigrationTableActionType, @@ -103,6 +104,55 @@ export class IndexMetadataService { ); } + async recomputeIndexMetadataForObject( + workspaceId: string, + updatedObjectMetadata: ObjectMetadataEntity, + ) { + const indexesToRecompute = await this.indexMetadataRepository.find({ + where: { + objectMetadataId: updatedObjectMetadata.id, + workspaceId, + }, + relations: ['indexFieldMetadatas.fieldMetadata'], + }); + + const recomputedIndexes: { + indexMetadata: IndexMetadataEntity; + previousName: string; + newName: string; + }[] = []; + + for (const index of indexesToRecompute) { + const previousIndexName = index.name; + const tableName = computeObjectTargetTable(updatedObjectMetadata); + + const indexFieldsMetadataOrdered = index.indexFieldMetadatas.sort( + (a, b) => a.order - b.order, + ); + + const columnNames = indexFieldsMetadataOrdered.map( + (indexFieldMetadata) => indexFieldMetadata.fieldMetadata.name, + ); + + const newIndexName = `IDX_${generateDeterministicIndexName([ + tableName, + ...columnNames, + ])}`; + + await this.indexMetadataRepository.update(index.id, { + name: newIndexName, + }); + + recomputedIndexes.push({ + indexMetadata: index, + previousName: previousIndexName, + newName: newIndexName, + }); + } + + return recomputedIndexes; + } + async deleteIndexMetadata( workspaceId: string, objectMetadata: ObjectMetadataEntity, @@ -179,4 +229,55 @@ export class IndexMetadataService { [migration], ); } + + async createIndexRecomputeMigrations( + workspaceId: string, + objectMetadata: ObjectMetadataEntity, + recomputedIndexes: { + indexMetadata: IndexMetadataEntity; + previousName: string; + newName: string; + }[], + ) { + for (const recomputedIndex of recomputedIndexes) { + const { previousName, newName, indexMetadata } = recomputedIndex; + + const tableName = computeObjectTargetTable(objectMetadata); + + const indexFieldsMetadataOrdered = indexMetadata.indexFieldMetadatas.sort( + (a, b) => a.order - b.order, + ); + + const columnNames = indexFieldsMetadataOrdered.map( + (indexFieldMetadata) => indexFieldMetadata.fieldMetadata.name, + ); + + const migration = { + name: tableName, + action: WorkspaceMigrationTableActionType.ALTER_INDEXES, + indexes: [ + { + action: WorkspaceMigrationIndexActionType.DROP, + name: previousName, + columns: [], + isUnique: indexMetadata.isUnique, + } satisfies WorkspaceMigrationIndexAction, + { + action: WorkspaceMigrationIndexActionType.CREATE, + columns: columnNames, + name: newName, + isUnique: indexMetadata.isUnique, + where: indexMetadata.indexWhereClause, + type: indexMetadata.indexType, + } satisfies WorkspaceMigrationIndexAction, + ], + } satisfies WorkspaceMigrationTableAction; + + await this.workspaceMigrationService.createCustomMigration( + generateMigrationName(`update-${objectMetadata.nameSingular}-index`), + workspaceId, + [migration], + ); + } + } } diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts index 6b0074d1e683..2c2cac74a604 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts @@ -13,6 +13,7 @@ import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature- import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { IndexMetadataModule } from 'src/engine/metadata-modules/index-metadata/index-metadata.module'; import { BeforeUpdateOneObject } from 'src/engine/metadata-modules/object-metadata/hooks/before-update-one-object.hook'; import { ObjectMetadataGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/object-metadata/interceptors/object-metadata-graphql-api-exception.interceptor'; import { ObjectMetadataResolver } from 'src/engine/metadata-modules/object-metadata/object-metadata.resolver'; @@ -49,6 +50,7 @@ import { UpdateObjectPayload } from './dtos/update-object.input'; WorkspaceMetadataVersionModule, RemoteTableRelationsModule, SearchModule, + IndexMetadataModule, ], services: [ ObjectMetadataService, diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts index 9a82a9515520..cd5c8e37729b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts @@ -10,6 +10,7 @@ import { FindManyOptions, FindOneOptions, In, Not, Repository } from 'typeorm'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { IndexMetadataService } from 'src/engine/metadata-modules/index-metadata/index-metadata.service'; import { DeleteOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/delete-object.input'; import { UpdateOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input'; import { @@ -26,7 +27,6 @@ import { validateObjectMetadataInputOrThrow, } from 'src/engine/metadata-modules/object-metadata/utils/validate-object-metadata-input.util'; import { RemoteTableRelationsService } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/remote-table-relations.service'; -import { mapUdtNameToFieldType } from 'src/engine/metadata-modules/remote-server/remote-table/utils/udt-name-mapper.util'; import { SearchService } from 'src/engine/metadata-modules/search/search.service'; import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service'; import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; @@ -55,6 +55,7 @@ export class ObjectMetadataService extends TypeOrmQueryService - this.objectMetadataRelationService.createMetadata( - objectMetadataInput.workspaceId, - createdObjectMetadata, - mapUdtNameToFieldType( - objectMetadataInput.primaryKeyColumnType ?? 'uuid', - ), - objectMetadataInput.primaryKeyFieldMetadataSettings, - relationType, - ), - ), - ); - - await this.objectMetadataMigrationService.createRelationMigrations( - createdObjectMetadata, - createdRelatedObjectMetadata, - ); - } - private async handleObjectNameAndLabelUpdates( existingObjectMetadata: ObjectMetadataEntity, objectMetadataForUpdate: ObjectMetadataEntity, @@ -447,17 +430,37 @@ export class ObjectMetadataService extends TypeOrmQueryService - relations.map((relation) => relation.toObjectMetadataId), - ); - - const foreignKeyFieldMetadataForStandardRelation = - await this.fieldMetadataRepository.find({ - where: { - isCustom: false, - settings: { - isForeignKey: true, - }, - name: `${existingObjectMetadata.nameSingular}Id`, - workspaceId: workspaceId, - }, - }); - - await Promise.all( - foreignKeyFieldMetadataForStandardRelation.map( - async (foreignKeyFieldMetadata) => { - if ( - relatedObjectsIds.includes( - foreignKeyFieldMetadata.objectMetadataId, - ) - ) { - const relatedObject = - await this.objectMetadataRepository.findOneBy({ - id: foreignKeyFieldMetadata.objectMetadataId, - workspaceId: workspaceId, - }); - - if (relatedObject) { - // 1. Update to and from relation fieldMetadata - const toFieldRelationFieldMetadataId = - await this.fieldMetadataRepository - .findOneByOrFail({ - name: existingObjectMetadata.nameSingular, - objectMetadataId: relatedObject.id, - workspaceId: workspaceId, - }) - .then((field) => field.id); - - const { description: descriptionForToField } = - buildDescriptionForRelationFieldMetadataOnToField({ - relationObjectMetadataNamePlural: relatedObject.namePlural, - targetObjectLabelSingular: - updatedObjectMetadata.labelSingular, - }); - - await this.fieldMetadataRepository.update( - toFieldRelationFieldMetadataId, - { - name: updatedObjectMetadata.nameSingular, - label: updatedObjectMetadata.labelSingular, - description: descriptionForToField, - }, - ); - - const fromFieldRelationFieldMetadataId = - await this.relationMetadataRepository - .findOneByOrFail({ - fromObjectMetadataId: existingObjectMetadata.id, - toObjectMetadataId: relatedObject.id, - toFieldMetadataId: toFieldRelationFieldMetadataId, - workspaceId, - }) - .then((relation) => relation?.fromFieldMetadataId); - - await this.fieldMetadataRepository.update( - fromFieldRelationFieldMetadataId, - { - description: - buildDescriptionForRelationFieldMetadataOnFromField({ - relationObjectMetadataNamePlural: - relatedObject.namePlural, - targetObjectLabelSingular: - updatedObjectMetadata.labelSingular, - }).description, - }, - ); - - // 2. Update foreign key fieldMetadata - const { - name: updatedNameForForeignKeyFieldMetadata, - label: updatedLabelForForeignKeyFieldMetadata, - description: updatedDescriptionForForeignKeyFieldMetadata, - } = buildNameLabelAndDescriptionForForeignKeyFieldMetadata({ - targetObjectNameSingular: updatedObjectMetadata.nameSingular, - targetObjectLabelSingular: - updatedObjectMetadata.labelSingular, - relatedObjectLabelSingular: relatedObject.labelSingular, - }); - - await this.fieldMetadataRepository.update( - foreignKeyFieldMetadata.id, - { - name: updatedNameForForeignKeyFieldMetadata, - label: updatedLabelForForeignKeyFieldMetadata, - description: updatedDescriptionForForeignKeyFieldMetadata, - }, - ); - - const relatedObjectTableName = - computeObjectTargetTable(relatedObject); - const columnName = `${existingObjectMetadata.nameSingular}Id`; - const columnType = fieldMetadataTypeToColumnType( - foreignKeyFieldMetadata.type, - ); + for (const { + relatedObjectMetadata, + foreignKeyFieldMetadata, + } of relationsAndForeignKeysMetadata) { + const relatedObjectTableName = computeObjectTargetTable( + relatedObjectMetadata, + ); + const columnName = `${existingObjectMetadata.nameSingular}Id`; + const columnType = fieldMetadataTypeToColumnType( + foreignKeyFieldMetadata.type, + ); - await this.workspaceMigrationService.createCustomMigration( - generateMigrationName( - `rename-${existingObjectMetadata.nameSingular}-to-${updatedObjectMetadata.nameSingular}-in-${relatedObject.nameSingular}`, - ), - workspaceId, - [ - { - name: relatedObjectTableName, - action: WorkspaceMigrationTableActionType.ALTER, - columns: [ - { - action: WorkspaceMigrationColumnActionType.ALTER, - currentColumnDefinition: { - columnName, - columnType, - isNullable: true, - defaultValue: null, - }, - alteredColumnDefinition: { - columnName: `${updatedObjectMetadata.nameSingular}Id`, - columnType, - isNullable: true, - defaultValue: null, - }, - }, - ], - }, - ], - ); - } - } - }, + await this.workspaceMigrationService.createCustomMigration( + generateMigrationName( + `rename-${existingObjectMetadata.nameSingular}-to-${updatedObjectMetadata.nameSingular}-in-${relatedObjectMetadata.nameSingular}`, ), + workspaceId, + [ + { + name: relatedObjectTableName, + action: WorkspaceMigrationTableActionType.ALTER, + columns: [ + { + action: WorkspaceMigrationColumnActionType.ALTER, + currentColumnDefinition: { + columnName, + columnType, + isNullable: true, + defaultValue: null, + }, + alteredColumnDefinition: { + columnName: `${updatedObjectMetadata.nameSingular}Id`, + columnType, + isNullable: true, + defaultValue: null, + }, + }, + ], + }, + ], ); } } diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/services/object-metadata-relation.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/services/object-metadata-relation.service.ts index 74dac774e0f7..a720a59e8fc0 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/services/object-metadata-relation.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/services/object-metadata-relation.service.ts @@ -18,17 +18,27 @@ import { RelationMetadataType, RelationOnDeleteAction, } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; +import { mapUdtNameToFieldType } from 'src/engine/metadata-modules/remote-server/remote-table/utils/udt-name-mapper.util'; import { CUSTOM_OBJECT_STANDARD_FIELD_IDS, STANDARD_OBJECT_FIELD_IDS, } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; import { STANDARD_OBJECT_ICONS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-icons'; +import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; import { createForeignKeyDeterministicUuid, createRelationDeterministicUuid, } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/create-deterministic-uuid.util'; import { capitalize } from 'src/utils/capitalize'; +const DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS = [ + STANDARD_OBJECT_IDS.timelineActivity, + STANDARD_OBJECT_IDS.favorite, + STANDARD_OBJECT_IDS.attachment, + STANDARD_OBJECT_IDS.noteTarget, + STANDARD_OBJECT_IDS.taskTarget, +]; + @Injectable() export class ObjectMetadataRelationService { constructor( @@ -40,46 +50,63 @@ export class ObjectMetadataRelationService { private readonly relationMetadataRepository: Repository, ) {} - public async createMetadata( + public async createRelationsAndForeignKeysMetadata( + workspaceId: string, + createdObjectMetadata: ObjectMetadataEntity, + { primaryKeyFieldMetadataSettings, primaryKeyColumnType }, + ) { + const relatedObjectMetadataCollection = await Promise.all( + DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS.map( + async (relationObjectMetadataStandardId) => + this.createRelationAndForeignKeyMetadata( + workspaceId, + createdObjectMetadata, + mapUdtNameToFieldType(primaryKeyColumnType ?? 'uuid'), + primaryKeyFieldMetadataSettings, + relationObjectMetadataStandardId, + ), + ), + ); + + return relatedObjectMetadataCollection; + } + + private async createRelationAndForeignKeyMetadata( workspaceId: string, createdObjectMetadata: ObjectMetadataEntity, objectPrimaryKeyType: FieldMetadataType, objectPrimaryKeyFieldSettings: | FieldMetadataSettings | undefined, - relatedObjectMetadataName: string, + relationObjectMetadataStandardId: string, ) { const relatedObjectMetadata = await this.objectMetadataRepository.findOneByOrFail({ - nameSingular: relatedObjectMetadataName, + standardId: relationObjectMetadataStandardId, workspaceId: workspaceId, + isCustom: false, }); - await this.createForeignKeyFieldMetadata( - workspaceId, - createdObjectMetadata, - relatedObjectMetadata, - objectPrimaryKeyType, - objectPrimaryKeyFieldSettings, - ); - - const relationFieldMetadata = await this.createRelationFields( - workspaceId, - createdObjectMetadata, - relatedObjectMetadata, - ); + const relationFieldMetadataCollection = + await this.createRelationFieldMetadas( + workspaceId, + createdObjectMetadata, + relatedObjectMetadata, + objectPrimaryKeyType, + objectPrimaryKeyFieldSettings, + ); - await this.createRelationMetadata( + await this.createRelationMetadataFromFieldMetadatas( workspaceId, createdObjectMetadata, relatedObjectMetadata, - relationFieldMetadata, + relationFieldMetadataCollection, ); return relatedObjectMetadata; } - private async createForeignKeyFieldMetadata( + private async createRelationFieldMetadas( workspaceId: string, createdObjectMetadata: ObjectMetadataEntity, relatedObjectMetadata: ObjectMetadataEntity, @@ -88,99 +115,187 @@ export class ObjectMetadataRelationService { | FieldMetadataSettings | undefined, ) { - const customStandardFieldId = - STANDARD_OBJECT_FIELD_IDS[relatedObjectMetadata.nameSingular].custom; + return this.fieldMetadataRepository.save([ + this.buildFromFieldMetadata( + workspaceId, + createdObjectMetadata, + relatedObjectMetadata, + ), + this.buildToFieldMetadata( + workspaceId, + createdObjectMetadata, + relatedObjectMetadata, + ), + this.buildForeignKeyFieldMetadata( + workspaceId, + createdObjectMetadata, + relatedObjectMetadata, + objectPrimaryKeyType, + objectPrimaryKeyFieldSettings, + ), + ]); + } - if (!customStandardFieldId) { - throw new Error( - `Custom standard field ID not found for ${relatedObjectMetadata.nameSingular}`, - ); - } + public async updateRelationsAndForeignKeysMetadata( + workspaceId: string, + updatedObjectMetadata: ObjectMetadataEntity, + ): Promise< + { + relatedObjectMetadata: ObjectMetadataEntity; + foreignKeyFieldMetadata: FieldMetadataEntity; + toFieldMetadata: FieldMetadataEntity; + fromFieldMetadata: FieldMetadataEntity; + }[] + > { + return await Promise.all( + DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS.map( + async (relationObjectMetadataStandardId) => + this.updateRelationAndForeignKeyMetadata( + workspaceId, + updatedObjectMetadata, + relationObjectMetadataStandardId, + ), + ), + ); + } - const { name, label, description } = - buildNameLabelAndDescriptionForForeignKeyFieldMetadata({ - targetObjectNameSingular: createdObjectMetadata.nameSingular, - targetObjectLabelSingular: createdObjectMetadata.labelSingular, - relatedObjectLabelSingular: relatedObjectMetadata.labelSingular, + private async updateRelationAndForeignKeyMetadata( + workspaceId: string, + updatedObjectMetadata: ObjectMetadataEntity, + relationObjectMetadataStandardId: string, + ) { + const relatedObjectMetadata = + await this.objectMetadataRepository.findOneByOrFail({ + standardId: relationObjectMetadataStandardId, + workspaceId: workspaceId, + isCustom: false, }); - await this.fieldMetadataRepository.save({ + const toFieldMetadataUpdateCriteria = { + standardId: createRelationDeterministicUuid({ + objectId: updatedObjectMetadata.id, + standardId: + STANDARD_OBJECT_FIELD_IDS[relatedObjectMetadata.nameSingular].custom, + }), + objectMetadataId: relatedObjectMetadata.id, + workspaceId: workspaceId, + }; + const toFieldMetadataUpdateData = this.buildToFieldMetadata( + workspaceId, + updatedObjectMetadata, + relatedObjectMetadata, + true, + ); + const toFieldMetadataToUpdate = + await this.fieldMetadataRepository.findOneBy( + toFieldMetadataUpdateCriteria, + ); + const toFieldMetadata = await this.fieldMetadataRepository.save({ + ...toFieldMetadataToUpdate, + ...toFieldMetadataUpdateData, + }); + + const fromFieldMetadataUpdateCriteria = { + standardId: + CUSTOM_OBJECT_STANDARD_FIELD_IDS[relatedObjectMetadata.namePlural], + objectMetadataId: updatedObjectMetadata.id, + workspaceId: workspaceId, + }; + const fromFieldMetadataUpdateData = this.buildFromFieldMetadata( + workspaceId, + updatedObjectMetadata, + relatedObjectMetadata, + true, + ); + const fromFieldMetadataToUpdate = + await this.fieldMetadataRepository.findOneBy( + fromFieldMetadataUpdateCriteria, + ); + const fromFieldMetadata = await this.fieldMetadataRepository.save({ + ...fromFieldMetadataToUpdate, + ...fromFieldMetadataUpdateData, + }); + + const foreignKeyFieldMetadataUpdateCriteria = { standardId: createForeignKeyDeterministicUuid({ - objectId: createdObjectMetadata.id, - standardId: customStandardFieldId, + objectId: updatedObjectMetadata.id, + standardId: + STANDARD_OBJECT_FIELD_IDS[relatedObjectMetadata.nameSingular].custom, }), objectMetadataId: relatedObjectMetadata.id, workspaceId: workspaceId, - isCustom: false, - isActive: true, - type: objectPrimaryKeyType, - name, - label, - description, - icon: undefined, - isNullable: true, - isSystem: true, - defaultValue: undefined, - settings: { ...objectPrimaryKeyFieldSettings, isForeignKey: true }, + }; + const foreignKeyFieldMetadataUpdateData = this.buildForeignKeyFieldMetadata( + workspaceId, + updatedObjectMetadata, + relatedObjectMetadata, + FieldMetadataType.UUID, + undefined, + true, + ); + const foreignKeyFieldMetadataToUpdate = + await this.fieldMetadataRepository.findOneBy( + foreignKeyFieldMetadataUpdateCriteria, + ); + const foreignKeyFieldMetadata = await this.fieldMetadataRepository.save({ + ...foreignKeyFieldMetadataToUpdate, + ...foreignKeyFieldMetadataUpdateData, }); - } - private async createRelationFields( - workspaceId: string, - createdObjectMetadata: ObjectMetadataEntity, - relatedObjectMetadata: ObjectMetadataEntity, - ) { - return await this.fieldMetadataRepository.save([ - this.createFromField( - workspaceId, - createdObjectMetadata, - relatedObjectMetadata, - ), - this.createToField( - workspaceId, - createdObjectMetadata, - relatedObjectMetadata, - ), - ]); + return { + relatedObjectMetadata, + foreignKeyFieldMetadata, + toFieldMetadata, + fromFieldMetadata, + }; } - private createFromField( + private buildFromFieldMetadata( workspaceId: string, - createdObjectMetadata: ObjectMetadataEntity, + objectMetadata: ObjectMetadataEntity, relatedObjectMetadata: ObjectMetadataEntity, + isUpdate = false, ) { const relationObjectMetadataNamePlural = relatedObjectMetadata.namePlural; const { description } = buildDescriptionForRelationFieldMetadataOnFromField( { relationObjectMetadataNamePlural, - targetObjectLabelSingular: createdObjectMetadata.labelSingular, + targetObjectLabelSingular: objectMetadata.labelSingular, }, ); return { - standardId: - CUSTOM_OBJECT_STANDARD_FIELD_IDS[relationObjectMetadataNamePlural], - objectMetadataId: createdObjectMetadata.id, - workspaceId: workspaceId, - isCustom: false, - isActive: true, - isSystem: true, - type: FieldMetadataType.RELATION, - name: relatedObjectMetadata.namePlural, - label: capitalize(relationObjectMetadataNamePlural), description, - icon: - STANDARD_OBJECT_ICONS[relatedObjectMetadata.nameSingular] || - 'IconBuildingSkyscraper', - isNullable: true, + ...(!isUpdate + ? { + standardId: + CUSTOM_OBJECT_STANDARD_FIELD_IDS[ + relationObjectMetadataNamePlural + ], + objectMetadataId: objectMetadata.id, + workspaceId: workspaceId, + isCustom: false, + isActive: true, + isSystem: true, + type: FieldMetadataType.RELATION, + name: relatedObjectMetadata.namePlural, + label: capitalize(relationObjectMetadataNamePlural), + description, + icon: + STANDARD_OBJECT_ICONS[relatedObjectMetadata.nameSingular] || + 'IconBuildingSkyscraper', + isNullable: true, + } + : {}), }; } - private createToField( + private buildToFieldMetadata( workspaceId: string, - createdObjectMetadata: ObjectMetadataEntity, + objectMetadata: ObjectMetadataEntity, relatedObjectMetadata: ObjectMetadataEntity, + isUpdate = false, ) { const customStandardFieldId = STANDARD_OBJECT_FIELD_IDS[relatedObjectMetadata.nameSingular].custom; @@ -193,35 +308,96 @@ export class ObjectMetadataRelationService { const { description } = buildDescriptionForRelationFieldMetadataOnToField({ relationObjectMetadataNamePlural: relatedObjectMetadata.namePlural, - targetObjectLabelSingular: createdObjectMetadata.labelSingular, + targetObjectLabelSingular: objectMetadata.labelSingular, }); return { - standardId: createRelationDeterministicUuid({ - objectId: createdObjectMetadata.id, - standardId: customStandardFieldId, - }), - objectMetadataId: relatedObjectMetadata.id, - workspaceId: workspaceId, - isCustom: false, - isActive: true, - isSystem: true, - type: FieldMetadataType.RELATION, - name: createdObjectMetadata.nameSingular, - label: createdObjectMetadata.labelSingular, + name: objectMetadata.nameSingular, + label: objectMetadata.labelSingular, + description, + ...(!isUpdate + ? { + standardId: createRelationDeterministicUuid({ + objectId: objectMetadata.id, + standardId: customStandardFieldId, + }), + objectMetadataId: relatedObjectMetadata.id, + workspaceId: workspaceId, + isCustom: false, + isActive: true, + isSystem: true, + type: FieldMetadataType.RELATION, + name: objectMetadata.nameSingular, + label: objectMetadata.labelSingular, + description, + icon: 'IconBuildingSkyscraper', + isNullable: true, + } + : {}), + }; + } + + private buildForeignKeyFieldMetadata( + workspaceId: string, + objectMetadata: ObjectMetadataEntity, + relatedObjectMetadata: ObjectMetadataEntity, + objectPrimaryKeyType: FieldMetadataType, + objectPrimaryKeyFieldSettings: + | FieldMetadataSettings + | undefined, + isUpdate = false, + ) { + const customStandardFieldId = + STANDARD_OBJECT_FIELD_IDS[relatedObjectMetadata.nameSingular].custom; + + if (!customStandardFieldId) { + throw new Error( + `Custom standard field ID not found for ${relatedObjectMetadata.nameSingular}`, + ); + } + + const { name, label, description } = + buildNameLabelAndDescriptionForForeignKeyFieldMetadata({ + targetObjectNameSingular: objectMetadata.nameSingular, + targetObjectLabelSingular: objectMetadata.labelSingular, + relatedObjectLabelSingular: relatedObjectMetadata.labelSingular, + }); + + return { + name, + label, description, - icon: 'IconBuildingSkyscraper', - isNullable: true, + ...(!isUpdate + ? { + standardId: createForeignKeyDeterministicUuid({ + objectId: objectMetadata.id, + standardId: customStandardFieldId, + }), + objectMetadataId: relatedObjectMetadata.id, + workspaceId: workspaceId, + isCustom: false, + isActive: true, + type: objectPrimaryKeyType, + name, + label, + description, + icon: undefined, + isNullable: true, + isSystem: true, + defaultValue: undefined, + settings: { ...objectPrimaryKeyFieldSettings, isForeignKey: true }, + } + : {}), }; } - private async createRelationMetadata( + private async createRelationMetadataFromFieldMetadatas( workspaceId: string, createdObjectMetadata: ObjectMetadataEntity, relatedObjectMetadata: ObjectMetadataEntity, - relationFieldMetadata: FieldMetadataEntity[], + relationFieldMetadataCollection: FieldMetadataEntity[], ) { - const relationFieldMetadataMap = relationFieldMetadata.reduce( + const relationFieldMetadataMap = relationFieldMetadataCollection.reduce( (acc, fieldMetadata: FieldMetadataEntity) => { if (fieldMetadata.type === FieldMetadataType.RELATION) { acc[fieldMetadata.objectMetadataId] = fieldMetadata; @@ -247,7 +423,10 @@ export class ObjectMetadataRelationService { ]); } - async updateObjectRelationships(objectMetadataId: string, isActive: boolean) { + async updateObjectRelationshipsActivationStatus( + objectMetadataId: string, + isActive: boolean, + ) { const affectedRelations = await this.relationMetadataRepository.find({ where: [ { fromObjectMetadataId: objectMetadataId }, From 525a2c2bed11576f278d0d993ee7ea0bb33862b1 Mon Sep 17 00:00:00 2001 From: nitin <142569587+ehconitin@users.noreply.github.com> Date: Thu, 28 Nov 2024 15:39:00 +0530 Subject: [PATCH 4/7] minor-fix: console errors (#8782) --- ...riteFolderNavigationDrawerItemDropdown.tsx | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/packages/twenty-front/src/modules/favorites/components/FavoriteFolderNavigationDrawerItemDropdown.tsx b/packages/twenty-front/src/modules/favorites/components/FavoriteFolderNavigationDrawerItemDropdown.tsx index a7419d7c8f24..d3ca031163f2 100644 --- a/packages/twenty-front/src/modules/favorites/components/FavoriteFolderNavigationDrawerItemDropdown.tsx +++ b/packages/twenty-front/src/modules/favorites/components/FavoriteFolderNavigationDrawerItemDropdown.tsx @@ -1,13 +1,10 @@ import { FavoriteFolderHotkeyScope } from '@/favorites/constants/FavoriteFolderRightIconDropdownHotkeyScope'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; -import { - IconDotsVertical, - IconPencil, - IconTrash, - LightIconButton, - MenuItem, -} from 'twenty-ui'; +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; + +import { IconDotsVertical, IconPencil, IconTrash, MenuItem } from 'twenty-ui'; type FavoriteFolderNavigationDrawerItemDropdownProps = { folderId: string; @@ -16,12 +13,24 @@ type FavoriteFolderNavigationDrawerItemDropdownProps = { closeDropdown: () => void; }; +const StyledIconContainer = styled.div` + align-items: center; + background: transparent; + height: 24px; + width: 24px; + justify-content: center; + transition: background 0.1s ease; + display: flex; +`; + export const FavoriteFolderNavigationDrawerItemDropdown = ({ folderId, onRename, onDelete, closeDropdown, }: FavoriteFolderNavigationDrawerItemDropdownProps) => { + const theme = useTheme(); + const handleRename = () => { onRename(); closeDropdown(); @@ -41,7 +50,12 @@ export const FavoriteFolderNavigationDrawerItemDropdown = ({ usePortal data-select-disable clickableComponent={ - + + + } dropdownPlacement="right" dropdownOffset={{ y: -15 }} From abe9185f482a01af4b9e6043fe2351a5339af417 Mon Sep 17 00:00:00 2001 From: gotjoshua Date: Thu, 28 Nov 2024 10:33:22 +0000 Subject: [PATCH 5/7] Devenv setup via devenvious (#8774) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds a devenv template based on [devenv.sh](https://devenv.sh/), this is a nice to have for devs that use nix, direnv, and devenv. It provides a quick and easy way to ensure that all packages that are needed to start up the dev environment are installed. I've initialized using devenvious, which is a @tennox creation: [source on gitlab](https://gitlab.com/txlab/dx/devenvious). I acknowledge that this "clutters" the root dir with some less common files, but they are harmless for those that don't use the tools, and helpful for those that do... a bit like .vscode and .idea folders. --------- Co-authored-by: Félix Malfait --- .gitignore | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.gitignore b/.gitignore index 20f974922280..48296f0cb14f 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,13 @@ storybook-static test-results/ dump.rdb .tinyb + +.notes +/data/ +/.devenv/ +/.direnv/ +/.pre-commit-config.yaml +/.envrc +/devenv.nix +/flake.lock +/flake.nix From e96ad9a1f2bdae40ec633fa8b4b02cd27cd3c88d Mon Sep 17 00:00:00 2001 From: nitin <142569587+ehconitin@users.noreply.github.com> Date: Thu, 28 Nov 2024 18:13:11 +0530 Subject: [PATCH 6/7] Admin panel init (#8742) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WIP Related issues - #7090 #8547 Master issue - #4499 --------- Co-authored-by: Félix Malfait --- .../twenty-front/src/generated/graphql.tsx | 176 +++++++++++++ ...sePageChangeEffectNavigateLocation.test.ts | 11 - .../src/modules/app/components/AppRouter.tsx | 6 + .../modules/app/components/SettingsRoutes.tsx | 25 ++ .../modules/app/hooks/useCreateAppRouter.tsx | 4 +- .../src/modules/auth/hooks/useAuth.ts | 93 +++---- .../SettingsAdminImpersonateUsers.tsx | 67 +++++ .../SettingsAdminFeatureFlagsTabs.ts | 2 + .../mutations/updateWorkspaceFeatureFlag.ts | 15 ++ .../graphql/mutations/userLookupAdminPanel.ts | 30 +++ .../hooks/useFeatureFlagsManagement.ts | 91 +++++++ .../admin-panel/hooks/useImpersonate.ts | 60 +++++ .../settings/admin-panel/types/FeatureFlag.ts | 4 + .../settings/admin-panel/types/UserLookup.ts | 11 + .../admin-panel/types/WorkspaceInfo.ts | 15 ++ .../SettingsNavigationDrawerItems.tsx | 11 + .../twenty-front/src/modules/types/AppPath.ts | 3 - .../src/modules/types/SettingsPath.ts | 2 + .../hooks/__tests__/useShowAuthModal.test.tsx | 11 - .../modules/ui/layout/tab/components/Tab.tsx | 7 + .../ui/layout/tab/components/TabList.tsx | 2 + .../hooks/useWorkspaceSwitching.ts | 4 +- .../pages/impersonate/ImpersonateEffect.tsx | 64 ----- .../__stories__/ImpersonateEffect.stories.tsx | 34 --- .../settings/admin-panel/SettingsAdmin.tsx | 39 +++ .../admin-panel/SettingsAdminFeatureFlags.tsx | 240 ++++++++++++++++++ .../admin-panel/admin-panel.module.ts | 19 ++ .../admin-panel/admin-panel.resolver.ts | 57 +++++ .../admin-panel/admin-panel.service.ts | 179 +++++++++++++ .../dtos}/impersonate.input.ts | 0 .../update-workspace-feature-flag.input.ts | 21 ++ .../admin-panel/dtos/user-lookup.entity.ts | 48 ++++ .../admin-panel/dtos/user-lookup.input.ts | 11 + .../engine/core-modules/auth/auth.module.ts | 4 +- .../engine/core-modules/auth/auth.resolver.ts | 10 - .../auth/services/auth.service.ts | 47 ---- .../engine/core-modules/core-engine.module.ts | 2 + .../display/icon/components/TablerIcons.ts | 4 +- 38 files changed, 1197 insertions(+), 232 deletions(-) create mode 100644 packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminImpersonateUsers.tsx create mode 100644 packages/twenty-front/src/modules/settings/admin-panel/constants/SettingsAdminFeatureFlagsTabs.ts create mode 100644 packages/twenty-front/src/modules/settings/admin-panel/graphql/mutations/updateWorkspaceFeatureFlag.ts create mode 100644 packages/twenty-front/src/modules/settings/admin-panel/graphql/mutations/userLookupAdminPanel.ts create mode 100644 packages/twenty-front/src/modules/settings/admin-panel/hooks/useFeatureFlagsManagement.ts create mode 100644 packages/twenty-front/src/modules/settings/admin-panel/hooks/useImpersonate.ts create mode 100644 packages/twenty-front/src/modules/settings/admin-panel/types/FeatureFlag.ts create mode 100644 packages/twenty-front/src/modules/settings/admin-panel/types/UserLookup.ts create mode 100644 packages/twenty-front/src/modules/settings/admin-panel/types/WorkspaceInfo.ts delete mode 100644 packages/twenty-front/src/pages/impersonate/ImpersonateEffect.tsx delete mode 100644 packages/twenty-front/src/pages/impersonate/__stories__/ImpersonateEffect.stories.tsx create mode 100644 packages/twenty-front/src/pages/settings/admin-panel/SettingsAdmin.tsx create mode 100644 packages/twenty-front/src/pages/settings/admin-panel/SettingsAdminFeatureFlags.tsx create mode 100644 packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.module.ts create mode 100644 packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.resolver.ts create mode 100644 packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts rename packages/twenty-server/src/engine/core-modules/{auth/dto => admin-panel/dtos}/impersonate.input.ts (100%) create mode 100644 packages/twenty-server/src/engine/core-modules/admin-panel/dtos/update-workspace-feature-flag.input.ts create mode 100644 packages/twenty-server/src/engine/core-modules/admin-panel/dtos/user-lookup.entity.ts create mode 100644 packages/twenty-server/src/engine/core-modules/admin-panel/dtos/user-lookup.input.ts diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index de35163609bc..513273ddde0e 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -477,10 +477,12 @@ export type Mutation = { updateOneServerlessFunction: ServerlessFunction; updatePasswordViaResetToken: InvalidatePassword; updateWorkspace: Workspace; + updateWorkspaceFeatureFlag: Scalars['Boolean']; uploadFile: Scalars['String']; uploadImage: Scalars['String']; uploadProfilePicture: Scalars['String']; uploadWorkspaceLogo: Scalars['String']; + userLookupAdminPanel: UserLookup; verify: Verify; }; @@ -679,6 +681,13 @@ export type MutationUpdateWorkspaceArgs = { }; +export type MutationUpdateWorkspaceFeatureFlagArgs = { + featureFlag: Scalars['String']; + value: Scalars['Boolean']; + workspaceId: Scalars['String']; +}; + + export type MutationUploadFileArgs = { file: Scalars['Upload']; fileFolder?: InputMaybe; @@ -701,6 +710,11 @@ export type MutationUploadWorkspaceLogoArgs = { }; +export type MutationUserLookupAdminPanelArgs = { + userIdentifier: Scalars['String']; +}; + + export type MutationVerifyArgs = { loginToken: Scalars['String']; }; @@ -1247,6 +1261,20 @@ export type UserExists = { exists: Scalars['Boolean']; }; +export type UserInfo = { + __typename?: 'UserInfo'; + email: Scalars['String']; + firstName?: Maybe; + id: Scalars['String']; + lastName?: Maybe; +}; + +export type UserLookup = { + __typename?: 'UserLookup'; + user: UserInfo; + workspaces: Array; +}; + export type UserMappingOptionsUser = { __typename?: 'UserMappingOptionsUser'; user?: Maybe; @@ -1285,6 +1313,7 @@ export type Workspace = { __typename?: 'Workspace'; activationStatus: WorkspaceActivationStatus; allowImpersonation: Scalars['Boolean']; + billingEntitlements?: Maybe>; billingSubscriptions?: Maybe>; createdAt: Scalars['DateTime']; currentBillingSubscription?: Maybe; @@ -1305,6 +1334,12 @@ export type Workspace = { }; +export type WorkspaceBillingEntitlementsArgs = { + filter?: BillingEntitlementFilter; + sorting?: Array; +}; + + export type WorkspaceBillingSubscriptionsArgs = { filter?: BillingSubscriptionFilter; sorting?: Array; @@ -1331,6 +1366,16 @@ export type WorkspaceEdge = { node: Workspace; }; +export type WorkspaceInfo = { + __typename?: 'WorkspaceInfo'; + featureFlags: Array; + id: Scalars['String']; + logo?: Maybe; + name: Scalars['String']; + totalUsers: Scalars['Float']; + users: Array; +}; + export type WorkspaceInvitation = { __typename?: 'WorkspaceInvitation'; email: Scalars['String']; @@ -1376,6 +1421,30 @@ export type WorkspaceNameAndId = { id: Scalars['String']; }; +export type BillingEntitlement = { + __typename?: 'billingEntitlement'; + id: Scalars['UUID']; + key: Scalars['String']; + value: Scalars['Boolean']; + workspaceId: Scalars['String']; +}; + +export type BillingEntitlementFilter = { + and?: InputMaybe>; + id?: InputMaybe; + or?: InputMaybe>; +}; + +export type BillingEntitlementSort = { + direction: SortDirection; + field: BillingEntitlementSortFields; + nulls?: InputMaybe; +}; + +export enum BillingEntitlementSortFields { + Id = 'id' +} + export type Field = { __typename?: 'field'; createdAt: Scalars['DateTime']; @@ -1787,6 +1856,22 @@ export type SkipSyncEmailOnboardingStepMutationVariables = Exact<{ [key: string] export type SkipSyncEmailOnboardingStepMutation = { __typename?: 'Mutation', skipSyncEmailOnboardingStep: { __typename?: 'OnboardingStepSuccess', success: boolean } }; +export type UpdateWorkspaceFeatureFlagMutationVariables = Exact<{ + workspaceId: Scalars['String']; + featureFlag: Scalars['String']; + value: Scalars['Boolean']; +}>; + + +export type UpdateWorkspaceFeatureFlagMutation = { __typename?: 'Mutation', updateWorkspaceFeatureFlag: boolean }; + +export type UserLookupAdminPanelMutationVariables = Exact<{ + userIdentifier: Scalars['String']; +}>; + + +export type UserLookupAdminPanelMutation = { __typename?: 'Mutation', userLookupAdminPanel: { __typename?: 'UserLookup', user: { __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null }, workspaces: Array<{ __typename?: 'WorkspaceInfo', id: string, name: string, logo?: string | null, totalUsers: number, users: Array<{ __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null }>, featureFlags: Array<{ __typename?: 'FeatureFlag', key: string, value: boolean }> }> } }; + export type CreateOidcIdentityProviderMutationVariables = Exact<{ input: SetupOidcSsoInput; }>; @@ -3178,6 +3263,97 @@ export function useSkipSyncEmailOnboardingStepMutation(baseOptions?: Apollo.Muta export type SkipSyncEmailOnboardingStepMutationHookResult = ReturnType; export type SkipSyncEmailOnboardingStepMutationResult = Apollo.MutationResult; export type SkipSyncEmailOnboardingStepMutationOptions = Apollo.BaseMutationOptions; +export const UpdateWorkspaceFeatureFlagDocument = gql` + mutation UpdateWorkspaceFeatureFlag($workspaceId: String!, $featureFlag: String!, $value: Boolean!) { + updateWorkspaceFeatureFlag( + workspaceId: $workspaceId + featureFlag: $featureFlag + value: $value + ) +} + `; +export type UpdateWorkspaceFeatureFlagMutationFn = Apollo.MutationFunction; + +/** + * __useUpdateWorkspaceFeatureFlagMutation__ + * + * To run a mutation, you first call `useUpdateWorkspaceFeatureFlagMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useUpdateWorkspaceFeatureFlagMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [updateWorkspaceFeatureFlagMutation, { data, loading, error }] = useUpdateWorkspaceFeatureFlagMutation({ + * variables: { + * workspaceId: // value for 'workspaceId' + * featureFlag: // value for 'featureFlag' + * value: // value for 'value' + * }, + * }); + */ +export function useUpdateWorkspaceFeatureFlagMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(UpdateWorkspaceFeatureFlagDocument, options); + } +export type UpdateWorkspaceFeatureFlagMutationHookResult = ReturnType; +export type UpdateWorkspaceFeatureFlagMutationResult = Apollo.MutationResult; +export type UpdateWorkspaceFeatureFlagMutationOptions = Apollo.BaseMutationOptions; +export const UserLookupAdminPanelDocument = gql` + mutation UserLookupAdminPanel($userIdentifier: String!) { + userLookupAdminPanel(userIdentifier: $userIdentifier) { + user { + id + email + firstName + lastName + } + workspaces { + id + name + logo + totalUsers + users { + id + email + firstName + lastName + } + featureFlags { + key + value + } + } + } +} + `; +export type UserLookupAdminPanelMutationFn = Apollo.MutationFunction; + +/** + * __useUserLookupAdminPanelMutation__ + * + * To run a mutation, you first call `useUserLookupAdminPanelMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useUserLookupAdminPanelMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [userLookupAdminPanelMutation, { data, loading, error }] = useUserLookupAdminPanelMutation({ + * variables: { + * userIdentifier: // value for 'userIdentifier' + * }, + * }); + */ +export function useUserLookupAdminPanelMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(UserLookupAdminPanelDocument, options); + } +export type UserLookupAdminPanelMutationHookResult = ReturnType; +export type UserLookupAdminPanelMutationResult = Apollo.MutationResult; +export type UserLookupAdminPanelMutationOptions = Apollo.BaseMutationOptions; export const CreateOidcIdentityProviderDocument = gql` mutation CreateOIDCIdentityProvider($input: SetupOIDCSsoInput!) { createOIDCIdentityProvider(input: $input) { diff --git a/packages/twenty-front/src/hooks/__tests__/usePageChangeEffectNavigateLocation.test.ts b/packages/twenty-front/src/hooks/__tests__/usePageChangeEffectNavigateLocation.test.ts index a7b683e660f7..d5166179e1ca 100644 --- a/packages/twenty-front/src/hooks/__tests__/usePageChangeEffectNavigateLocation.test.ts +++ b/packages/twenty-front/src/hooks/__tests__/usePageChangeEffectNavigateLocation.test.ts @@ -234,17 +234,6 @@ const testCases = [ { loc: AppPath.DevelopersCatchAll, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam }, { loc: AppPath.DevelopersCatchAll, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: undefined }, - { loc: AppPath.Impersonate, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired }, - { loc: AppPath.Impersonate, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, - { loc: AppPath.Impersonate, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, - { loc: AppPath.Impersonate, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: undefined }, - { loc: AppPath.Impersonate, isLoggedIn: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: AppPath.SignInUp }, - { loc: AppPath.Impersonate, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace }, - { loc: AppPath.Impersonate, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile }, - { loc: AppPath.Impersonate, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails }, - { loc: AppPath.Impersonate, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam }, - { loc: AppPath.Impersonate, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: undefined }, - { loc: AppPath.Authorize, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired }, { loc: AppPath.Authorize, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, { loc: AppPath.Authorize, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, diff --git a/packages/twenty-front/src/modules/app/components/AppRouter.tsx b/packages/twenty-front/src/modules/app/components/AppRouter.tsx index 45aa98098643..1491921e32fa 100644 --- a/packages/twenty-front/src/modules/app/components/AppRouter.tsx +++ b/packages/twenty-front/src/modules/app/components/AppRouter.tsx @@ -1,4 +1,5 @@ import { useCreateAppRouter } from '@/app/hooks/useCreateAppRouter'; +import { currentUserState } from '@/auth/states/currentUserState'; import { billingState } from '@/client-config/states/billingState'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { RouterProvider } from 'react-router-dom'; @@ -16,6 +17,10 @@ export const AppRouter = () => { const isBillingPageEnabled = billing?.isBillingEnabled && !isFreeAccessEnabled; + const currentUser = useRecoilValue(currentUserState); + + const isAdminPageEnabled = currentUser?.canImpersonate; + return ( { isCRMMigrationEnabled, isServerlessFunctionSettingsEnabled, isSSOEnabled, + isAdminPageEnabled, )} /> ); diff --git a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx index b758acdc1177..f8286c398b7f 100644 --- a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx +++ b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx @@ -242,11 +242,26 @@ const SettingsSecuritySSOIdentifyProvider = lazy(() => ), ); +const SettingsAdmin = lazy(() => + import('~/pages/settings/admin-panel/SettingsAdmin').then((module) => ({ + default: module.SettingsAdmin, + })), +); + +const SettingsAdminFeatureFlags = lazy(() => + import('~/pages/settings/admin-panel/SettingsAdminFeatureFlags').then( + (module) => ({ + default: module.SettingsAdminFeatureFlags, + }), + ), +); + type SettingsRoutesProps = { isBillingEnabled?: boolean; isCRMMigrationEnabled?: boolean; isServerlessFunctionSettingsEnabled?: boolean; isSSOEnabled?: boolean; + isAdminPageEnabled?: boolean; }; export const SettingsRoutes = ({ @@ -254,6 +269,7 @@ export const SettingsRoutes = ({ isCRMMigrationEnabled, isServerlessFunctionSettingsEnabled, isSSOEnabled, + isAdminPageEnabled, }: SettingsRoutesProps) => ( }> @@ -375,6 +391,15 @@ export const SettingsRoutes = ({ /> )} + {isAdminPageEnabled && ( + <> + } /> + } + /> + + )} ); diff --git a/packages/twenty-front/src/modules/app/hooks/useCreateAppRouter.tsx b/packages/twenty-front/src/modules/app/hooks/useCreateAppRouter.tsx index 0aa19e6e16cb..80afc3c8af56 100644 --- a/packages/twenty-front/src/modules/app/hooks/useCreateAppRouter.tsx +++ b/packages/twenty-front/src/modules/app/hooks/useCreateAppRouter.tsx @@ -14,7 +14,6 @@ import { Authorize } from '~/pages/auth/Authorize'; import { Invite } from '~/pages/auth/Invite'; import { PasswordReset } from '~/pages/auth/PasswordReset'; import { SignInUp } from '~/pages/auth/SignInUp'; -import { ImpersonateEffect } from '~/pages/impersonate/ImpersonateEffect'; import { NotFound } from '~/pages/not-found/NotFound'; import { RecordIndexPage } from '~/pages/object-record/RecordIndexPage'; import { RecordShowPage } from '~/pages/object-record/RecordShowPage'; @@ -30,6 +29,7 @@ export const useCreateAppRouter = ( isCRMMigrationEnabled?: boolean, isServerlessFunctionSettingsEnabled?: boolean, isSSOEnabled?: boolean, + isAdminPageEnabled?: boolean, ) => createBrowserRouter( createRoutesFromElements( @@ -54,7 +54,6 @@ export const useCreateAppRouter = ( element={} /> } /> - } /> } /> } /> } /> diff --git a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts index e8ef0aab3606..33b15e83d6d3 100644 --- a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts +++ b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts @@ -69,6 +69,49 @@ export const useAuth = () => { const setDateTimeFormat = useSetRecoilState(dateTimeFormatState); + const clearSession = useRecoilCallback( + ({ snapshot }) => + async () => { + const emptySnapshot = snapshot_UNSTABLE(); + const iconsValue = snapshot.getLoadable(iconsState).getValue(); + const authProvidersValue = snapshot + .getLoadable(authProvidersState) + .getValue(); + const billing = snapshot.getLoadable(billingState).getValue(); + const isSignInPrefilled = snapshot + .getLoadable(isSignInPrefilledState) + .getValue(); + const supportChat = snapshot.getLoadable(supportChatState).getValue(); + const isDebugMode = snapshot.getLoadable(isDebugModeState).getValue(); + const captchaProvider = snapshot + .getLoadable(captchaProviderState) + .getValue(); + const clientConfigApiStatus = snapshot + .getLoadable(clientConfigApiStatusState) + .getValue(); + const isCurrentUserLoaded = snapshot + .getLoadable(isCurrentUserLoadedState) + .getValue(); + const initialSnapshot = emptySnapshot.map(({ set }) => { + set(iconsState, iconsValue); + set(authProvidersState, authProvidersValue); + set(billingState, billing); + set(isSignInPrefilledState, isSignInPrefilled); + set(supportChatState, supportChat); + set(isDebugModeState, isDebugMode); + set(captchaProviderState, captchaProvider); + set(clientConfigApiStatusState, clientConfigApiStatus); + set(isCurrentUserLoadedState, isCurrentUserLoaded); + return undefined; + }); + goToRecoilSnapshot(initialSnapshot); + await client.clearStore(); + sessionStorage.clear(); + localStorage.clear(); + }, + [client, goToRecoilSnapshot], + ); + const handleChallenge = useCallback( async (email: string, password: string, captchaToken?: string) => { const challengeResult = await challenge({ @@ -212,51 +255,9 @@ export const useAuth = () => { [handleChallenge, handleVerify, setIsVerifyPendingState], ); - const handleSignOut = useRecoilCallback( - ({ snapshot }) => - async () => { - const emptySnapshot = snapshot_UNSTABLE(); - const iconsValue = snapshot.getLoadable(iconsState).getValue(); - const authProvidersValue = snapshot - .getLoadable(authProvidersState) - .getValue(); - const billing = snapshot.getLoadable(billingState).getValue(); - const isSignInPrefilled = snapshot - .getLoadable(isSignInPrefilledState) - .getValue(); - const supportChat = snapshot.getLoadable(supportChatState).getValue(); - const isDebugMode = snapshot.getLoadable(isDebugModeState).getValue(); - const captchaProvider = snapshot - .getLoadable(captchaProviderState) - .getValue(); - const clientConfigApiStatus = snapshot - .getLoadable(clientConfigApiStatusState) - .getValue(); - const isCurrentUserLoaded = snapshot - .getLoadable(isCurrentUserLoadedState) - .getValue(); - - const initialSnapshot = emptySnapshot.map(({ set }) => { - set(iconsState, iconsValue); - set(authProvidersState, authProvidersValue); - set(billingState, billing); - set(isSignInPrefilledState, isSignInPrefilled); - set(supportChatState, supportChat); - set(isDebugModeState, isDebugMode); - set(captchaProviderState, captchaProvider); - set(clientConfigApiStatusState, clientConfigApiStatus); - set(isCurrentUserLoadedState, isCurrentUserLoaded); - return undefined; - }); - - goToRecoilSnapshot(initialSnapshot); - - await client.clearStore(); - sessionStorage.clear(); - localStorage.clear(); - }, - [client, goToRecoilSnapshot], - ); + const handleSignOut = useCallback(async () => { + await clearSession(); + }, [clearSession]); const handleCredentialsSignUp = useCallback( async ( @@ -340,7 +341,7 @@ export const useAuth = () => { verify: handleVerify, checkUserExists: { checkUserExistsData, checkUserExistsQuery }, - + clearSession, signOut: handleSignOut, signUpWithCredentials: handleCredentialsSignUp, signInWithCredentials: handleCrendentialsSignIn, diff --git a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminImpersonateUsers.tsx b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminImpersonateUsers.tsx new file mode 100644 index 000000000000..146734e75ad1 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminImpersonateUsers.tsx @@ -0,0 +1,67 @@ +import { useImpersonate } from '@/settings/admin-panel/hooks/useImpersonate'; +import { TextInput } from '@/ui/input/components/TextInput'; +import styled from '@emotion/styled'; +import { useState } from 'react'; +import { Button, H2Title, IconUser, Section } from 'twenty-ui'; + +const StyledLinkContainer = styled.div` + margin-right: ${({ theme }) => theme.spacing(2)}; + width: 100%; +`; + +const StyledContainer = styled.div` + align-items: center; + display: flex; + flex-direction: row; +`; + +const StyledErrorSection = styled.div` + color: ${({ theme }) => theme.font.color.danger}; + margin-top: ${({ theme }) => theme.spacing(2)}; +`; + +export const SettingsAdminImpersonateUsers = () => { + const [userId, setUserId] = useState(''); + const { handleImpersonate, isLoading, error, canImpersonate } = + useImpersonate(); + + if (!canImpersonate) { + return ( +
+ +
+ ); + } + + return ( +
+ + + + handleImpersonate(userId)} + /> + +
+ ); +}; diff --git a/packages/twenty-front/src/modules/settings/admin-panel/constants/SettingsAdminFeatureFlagsTabs.ts b/packages/twenty-front/src/modules/settings/admin-panel/constants/SettingsAdminFeatureFlagsTabs.ts new file mode 100644 index 000000000000..e2e90e825119 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/admin-panel/constants/SettingsAdminFeatureFlagsTabs.ts @@ -0,0 +1,2 @@ +export const SETTINGS_ADMIN_FEATURE_FLAGS_TAB_ID = + 'settings-admin-feature-flags-tab-id'; diff --git a/packages/twenty-front/src/modules/settings/admin-panel/graphql/mutations/updateWorkspaceFeatureFlag.ts b/packages/twenty-front/src/modules/settings/admin-panel/graphql/mutations/updateWorkspaceFeatureFlag.ts new file mode 100644 index 000000000000..8077e86c261e --- /dev/null +++ b/packages/twenty-front/src/modules/settings/admin-panel/graphql/mutations/updateWorkspaceFeatureFlag.ts @@ -0,0 +1,15 @@ +import { gql } from '@apollo/client'; + +export const UPDATE_WORKSPACE_FEATURE_FLAG = gql` + mutation UpdateWorkspaceFeatureFlag( + $workspaceId: String! + $featureFlag: String! + $value: Boolean! + ) { + updateWorkspaceFeatureFlag( + workspaceId: $workspaceId + featureFlag: $featureFlag + value: $value + ) + } +`; diff --git a/packages/twenty-front/src/modules/settings/admin-panel/graphql/mutations/userLookupAdminPanel.ts b/packages/twenty-front/src/modules/settings/admin-panel/graphql/mutations/userLookupAdminPanel.ts new file mode 100644 index 000000000000..a4f14c5bd6d2 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/admin-panel/graphql/mutations/userLookupAdminPanel.ts @@ -0,0 +1,30 @@ +import { gql } from '@apollo/client'; + +export const USER_LOOKUP_ADMIN_PANEL = gql` + mutation UserLookupAdminPanel($userIdentifier: String!) { + userLookupAdminPanel(userIdentifier: $userIdentifier) { + user { + id + email + firstName + lastName + } + workspaces { + id + name + logo + totalUsers + users { + id + email + firstName + lastName + } + featureFlags { + key + value + } + } + } + } +`; diff --git a/packages/twenty-front/src/modules/settings/admin-panel/hooks/useFeatureFlagsManagement.ts b/packages/twenty-front/src/modules/settings/admin-panel/hooks/useFeatureFlagsManagement.ts new file mode 100644 index 000000000000..22ccd2b388e3 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/admin-panel/hooks/useFeatureFlagsManagement.ts @@ -0,0 +1,91 @@ +import { UserLookup } from '@/settings/admin-panel/types/UserLookup'; +import { useState } from 'react'; +import { isDefined } from 'twenty-ui'; +import { + useUpdateWorkspaceFeatureFlagMutation, + useUserLookupAdminPanelMutation, +} from '~/generated/graphql'; + +export const useFeatureFlagsManagement = () => { + const [userLookupResult, setUserLookupResult] = useState( + null, + ); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const [userLookup] = useUserLookupAdminPanelMutation({ + onCompleted: (data) => { + setIsLoading(false); + if (isDefined(data?.userLookupAdminPanel)) { + setUserLookupResult(data.userLookupAdminPanel); + } + }, + onError: (error) => { + setIsLoading(false); + setError(error.message); + }, + }); + + const [updateFeatureFlag] = useUpdateWorkspaceFeatureFlagMutation(); + + const handleUserLookup = async (userIdentifier: string) => { + setError(null); + setIsLoading(true); + setUserLookupResult(null); + + const response = await userLookup({ + variables: { userIdentifier }, + }); + + return response.data?.userLookupAdminPanel; + }; + + const handleFeatureFlagUpdate = async ( + workspaceId: string, + featureFlag: string, + value: boolean, + ) => { + setError(null); + const previousState = userLookupResult; + + if (isDefined(userLookupResult)) { + setUserLookupResult({ + ...userLookupResult, + workspaces: userLookupResult.workspaces.map((workspace) => + workspace.id === workspaceId + ? { + ...workspace, + featureFlags: workspace.featureFlags.map((flag) => + flag.key === featureFlag ? { ...flag, value } : flag, + ), + } + : workspace, + ), + }); + } + + const response = await updateFeatureFlag({ + variables: { + workspaceId, + featureFlag, + value, + }, + onError: (error) => { + if (isDefined(previousState)) { + setUserLookupResult(previousState); + } + setError(error.message); + }, + }); + + return !!response.data; + }; + + return { + userLookupResult, + handleUserLookup, + handleFeatureFlagUpdate, + isLoading, + error, + }; +}; diff --git a/packages/twenty-front/src/modules/settings/admin-panel/hooks/useImpersonate.ts b/packages/twenty-front/src/modules/settings/admin-panel/hooks/useImpersonate.ts new file mode 100644 index 000000000000..1046c70c917f --- /dev/null +++ b/packages/twenty-front/src/modules/settings/admin-panel/hooks/useImpersonate.ts @@ -0,0 +1,60 @@ +import { useAuth } from '@/auth/hooks/useAuth'; +import { currentUserState } from '@/auth/states/currentUserState'; +import { tokenPairState } from '@/auth/states/tokenPairState'; +import { AppPath } from '@/types/AppPath'; +import { useState } from 'react'; +import { useRecoilState, useSetRecoilState } from 'recoil'; +import { useImpersonateMutation } from '~/generated/graphql'; +import { isDefined } from '~/utils/isDefined'; +import { sleep } from '~/utils/sleep'; + +export const useImpersonate = () => { + const { clearSession } = useAuth(); + const [currentUser, setCurrentUser] = useRecoilState(currentUserState); + const setTokenPair = useSetRecoilState(tokenPairState); + const [impersonate] = useImpersonateMutation(); + + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const handleImpersonate = async (userId: string) => { + if (!userId.trim()) { + setError('Please enter a user ID'); + return; + } + + setIsLoading(true); + setError(null); + + try { + const impersonateResult = await impersonate({ + variables: { userId }, + }); + + if (isDefined(impersonateResult.errors)) { + throw impersonateResult.errors; + } + + if (!impersonateResult.data?.impersonate) { + throw new Error('No impersonate result'); + } + + const { user, tokens } = impersonateResult.data.impersonate; + await clearSession(); + setCurrentUser(user); + setTokenPair(tokens); + await sleep(0); // This hacky workaround is necessary to ensure the tokens stored in the cookie are updated correctly. + window.location.href = AppPath.Index; + } catch (error) { + setError('Failed to impersonate user. Please try again.'); + setIsLoading(false); + } + }; + + return { + handleImpersonate, + isLoading, + error, + canImpersonate: currentUser?.canImpersonate, + }; +}; diff --git a/packages/twenty-front/src/modules/settings/admin-panel/types/FeatureFlag.ts b/packages/twenty-front/src/modules/settings/admin-panel/types/FeatureFlag.ts new file mode 100644 index 000000000000..2c9ab89136fc --- /dev/null +++ b/packages/twenty-front/src/modules/settings/admin-panel/types/FeatureFlag.ts @@ -0,0 +1,4 @@ +export type FeatureFlag = { + key: string; + value: boolean; +}; diff --git a/packages/twenty-front/src/modules/settings/admin-panel/types/UserLookup.ts b/packages/twenty-front/src/modules/settings/admin-panel/types/UserLookup.ts new file mode 100644 index 000000000000..0cb66e283f18 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/admin-panel/types/UserLookup.ts @@ -0,0 +1,11 @@ +import { WorkspaceInfo } from '@/settings/admin-panel/types/WorkspaceInfo'; + +export type UserLookup = { + user: { + id: string; + email: string; + firstName?: string | null; + lastName?: string | null; + }; + workspaces: WorkspaceInfo[]; +}; diff --git a/packages/twenty-front/src/modules/settings/admin-panel/types/WorkspaceInfo.ts b/packages/twenty-front/src/modules/settings/admin-panel/types/WorkspaceInfo.ts new file mode 100644 index 000000000000..3d36fe20336d --- /dev/null +++ b/packages/twenty-front/src/modules/settings/admin-panel/types/WorkspaceInfo.ts @@ -0,0 +1,15 @@ +import { FeatureFlag } from '@/settings/admin-panel/types/FeatureFlag'; + +export type WorkspaceInfo = { + id: string; + name: string; + logo?: string | null; + totalUsers: number; + users: { + id: string; + email: string; + firstName?: string | null; + lastName?: string | null; + }[]; + featureFlags: FeatureFlag[]; +}; diff --git a/packages/twenty-front/src/modules/settings/components/SettingsNavigationDrawerItems.tsx b/packages/twenty-front/src/modules/settings/components/SettingsNavigationDrawerItems.tsx index 54a46360a51b..4910e7a2aa8c 100644 --- a/packages/twenty-front/src/modules/settings/components/SettingsNavigationDrawerItems.tsx +++ b/packages/twenty-front/src/modules/settings/components/SettingsNavigationDrawerItems.tsx @@ -13,6 +13,7 @@ import { IconKey, IconMail, IconRocket, + IconServer, IconSettings, IconTool, IconUserCircle, @@ -21,6 +22,7 @@ import { } from 'twenty-ui'; import { useAuth } from '@/auth/hooks/useAuth'; +import { currentUserState } from '@/auth/states/currentUserState'; import { billingState } from '@/client-config/states/billingState'; import { SettingsNavigationDrawerItem } from '@/settings/components/SettingsNavigationDrawerItem'; import { useExpandedAnimation } from '@/settings/hooks/useExpandedAnimation'; @@ -84,6 +86,8 @@ export const SettingsNavigationDrawerItems = () => { const isBillingPageEnabled = billing?.isBillingEnabled && !isFreeAccessEnabled; + const currentUser = useRecoilValue(currentUserState); + const isAdminPageEnabled = currentUser?.canImpersonate; // TODO: Refactor this part to only have arrays of navigation items const currentPathName = useLocation().pathname; @@ -230,6 +234,13 @@ export const SettingsNavigationDrawerItems = () => { + {isAdminPageEnabled && ( + + )} theme.background.quaternary}; } `; +const StyledLogo = styled.img` + height: 14px; + width: 14px; +`; export const Tab = ({ id, @@ -72,6 +77,7 @@ export const Tab = ({ disabled, pill, to, + logo, }: TabProps) => { const theme = useTheme(); return ( @@ -85,6 +91,7 @@ export const Tab = ({ to={to} > + {logo && } {Icon && } {title} {pill && typeof pill === 'string' ? : pill} diff --git a/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx b/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx index 7d724d67e918..7dc93e7ee624 100644 --- a/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx +++ b/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx @@ -19,6 +19,7 @@ export type SingleTabProps = { disabled?: boolean; pill?: string | React.ReactElement; cards?: LayoutCard[]; + logo?: string; }; type TabListProps = { @@ -71,6 +72,7 @@ export const TabList = ({ key={tab.id} title={tab.title} Icon={tab.Icon} + logo={tab.logo} active={tab.id === activeTabId} disabled={tab.disabled ?? loading} pill={tab.pill} diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/hooks/useWorkspaceSwitching.ts b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/hooks/useWorkspaceSwitching.ts index b7e7abf9f325..976362e46fc0 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/hooks/useWorkspaceSwitching.ts +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/hooks/useWorkspaceSwitching.ts @@ -23,7 +23,7 @@ export const useWorkspaceSwitching = () => { availableSSOIdentityProvidersState, ); const setSignInUpStep = useSetRecoilState(signInUpStepState); - const { signOut } = useAuth(); + const { clearSession } = useAuth(); const switchWorkspace = async (workspaceId: string) => { if (currentWorkspace?.id === workspaceId) return; @@ -50,7 +50,7 @@ export const useWorkspaceSwitching = () => { } if (jwt.data.generateJWT.availableSSOIDPs.length > 1) { - await signOut(); + await clearSession(); setAvailableWorkspacesForSSOState( jwt.data.generateJWT.availableSSOIDPs, ); diff --git a/packages/twenty-front/src/pages/impersonate/ImpersonateEffect.tsx b/packages/twenty-front/src/pages/impersonate/ImpersonateEffect.tsx deleted file mode 100644 index 6ac9d3f6c59f..000000000000 --- a/packages/twenty-front/src/pages/impersonate/ImpersonateEffect.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { isNonEmptyString } from '@sniptt/guards'; -import { useCallback, useEffect } from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; -import { useRecoilState, useSetRecoilState } from 'recoil'; - -import { useIsLogged } from '@/auth/hooks/useIsLogged'; -import { currentUserState } from '@/auth/states/currentUserState'; -import { tokenPairState } from '@/auth/states/tokenPairState'; -import { AppPath } from '@/types/AppPath'; -import { useImpersonateMutation } from '~/generated/graphql'; -import { isDefined } from '~/utils/isDefined'; - -export const ImpersonateEffect = () => { - const navigate = useNavigate(); - const { userId } = useParams(); - - const [currentUser, setCurrentUser] = useRecoilState(currentUserState); - const setTokenPair = useSetRecoilState(tokenPairState); - - const [impersonate] = useImpersonateMutation(); - - const isLogged = useIsLogged(); - - const handleImpersonate = useCallback(async () => { - if (!isNonEmptyString(userId)) { - return; - } - - const impersonateResult = await impersonate({ - variables: { userId }, - }); - - if (isDefined(impersonateResult.errors)) { - throw impersonateResult.errors; - } - - if (!impersonateResult.data?.impersonate) { - throw new Error('No impersonate result'); - } - - setCurrentUser({ - ...impersonateResult.data.impersonate.user, - // Todo also set WorkspaceMember - }); - setTokenPair(impersonateResult.data?.impersonate.tokens); - - return impersonateResult.data?.impersonate; - }, [userId, impersonate, setCurrentUser, setTokenPair]); - - useEffect(() => { - if ( - isLogged && - currentUser?.canImpersonate === true && - isNonEmptyString(userId) - ) { - handleImpersonate(); - } else { - // User is not allowed to impersonate or not logged in - navigate(AppPath.Index); - } - }, [userId, currentUser, isLogged, handleImpersonate, navigate]); - - return <>; -}; diff --git a/packages/twenty-front/src/pages/impersonate/__stories__/ImpersonateEffect.stories.tsx b/packages/twenty-front/src/pages/impersonate/__stories__/ImpersonateEffect.stories.tsx deleted file mode 100644 index b1b44773ee76..000000000000 --- a/packages/twenty-front/src/pages/impersonate/__stories__/ImpersonateEffect.stories.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { Meta, StoryObj } from '@storybook/react'; - -import { - PageDecorator, - PageDecoratorArgs, -} from '~/testing/decorators/PageDecorator'; -import { graphqlMocks } from '~/testing/graphqlMocks'; -import { sleep } from '~/utils/sleep'; - -import { AppPath } from '@/types/AppPath'; -import { ImpersonateEffect } from '../ImpersonateEffect'; - -const meta: Meta = { - title: 'Pages/Impersonate/Impersonate', - component: ImpersonateEffect, - decorators: [PageDecorator], - args: { - routePath: AppPath.Impersonate, - routeParams: { ':userId': '1' }, - }, - parameters: { - msw: graphqlMocks, - }, -}; - -export default meta; - -export type Story = StoryObj; - -export const Default: Story = { - play: async () => { - await sleep(100); - }, -}; diff --git a/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdmin.tsx b/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdmin.tsx new file mode 100644 index 000000000000..3060866ce350 --- /dev/null +++ b/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdmin.tsx @@ -0,0 +1,39 @@ +import { SettingsAdminImpersonateUsers } from '@/settings/admin-panel/components/SettingsAdminImpersonateUsers'; +import { SettingsCard } from '@/settings/components/SettingsCard'; +import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; +import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; +import { SettingsPath } from '@/types/SettingsPath'; +import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; +import { useTheme } from '@emotion/react'; +import { IconFlag, UndecoratedLink } from 'twenty-ui'; + +export const SettingsAdmin = () => { + const theme = useTheme(); + return ( + + + + + + } + title="Feature Flags" + /> + + + + ); +}; diff --git a/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdminFeatureFlags.tsx b/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdminFeatureFlags.tsx new file mode 100644 index 000000000000..e4cf5a5e7445 --- /dev/null +++ b/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdminFeatureFlags.tsx @@ -0,0 +1,240 @@ +import { SETTINGS_ADMIN_FEATURE_FLAGS_TAB_ID } from '@/settings/admin-panel/constants/SettingsAdminFeatureFlagsTabs'; +import { useFeatureFlagsManagement } from '@/settings/admin-panel/hooks/useFeatureFlagsManagement'; +import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; +import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; +import { SettingsPath } from '@/types/SettingsPath'; +import { TextInput } from '@/ui/input/components/TextInput'; +import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; +import { TabList } from '@/ui/layout/tab/components/TabList'; +import { useTabList } from '@/ui/layout/tab/hooks/useTabList'; +import { Table } from '@/ui/layout/table/components/Table'; +import { TableCell } from '@/ui/layout/table/components/TableCell'; +import { TableHeader } from '@/ui/layout/table/components/TableHeader'; +import { TableRow } from '@/ui/layout/table/components/TableRow'; +import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo'; +import styled from '@emotion/styled'; +import { useState } from 'react'; +import { useRecoilValue } from 'recoil'; +import { + Button, + getImageAbsoluteURI, + H1Title, + H1TitleFontColor, + H2Title, + IconSearch, + isDefined, + Section, + Toggle, +} from 'twenty-ui'; + +const StyledLinkContainer = styled.div` + margin-right: ${({ theme }) => theme.spacing(2)}; + width: 100%; +`; + +const StyledContainer = styled.div` + align-items: center; + display: flex; + flex-direction: row; +`; + +const StyledErrorSection = styled.div` + color: ${({ theme }) => theme.font.color.danger}; + margin-top: ${({ theme }) => theme.spacing(2)}; +`; + +const StyledUserInfo = styled.div` + margin-bottom: ${({ theme }) => theme.spacing(5)}; +`; + +const StyledTable = styled(Table)` + margin-top: ${({ theme }) => theme.spacing(0.5)}; +`; + +const StyledTabListContainer = styled.div` + align-items: center; + border-bottom: ${({ theme }) => `1px solid ${theme.border.color.light}`}; + box-sizing: border-box; + display: flex; + gap: ${({ theme }) => theme.spacing(2)}; +`; + +const StyledContentContainer = styled.div` + flex: 1; + width: 100%; + padding: ${({ theme }) => theme.spacing(4)} 0; +`; + +export const SettingsAdminFeatureFlags = () => { + const [userIdentifier, setUserIdentifier] = useState(''); + + const { activeTabIdState, setActiveTabId } = useTabList( + SETTINGS_ADMIN_FEATURE_FLAGS_TAB_ID, + ); + const activeTabId = useRecoilValue(activeTabIdState); + + const { + userLookupResult, + handleUserLookup, + handleFeatureFlagUpdate, + isLoading, + error, + } = useFeatureFlagsManagement(); + + const handleSearch = async () => { + setActiveTabId(''); + + const result = await handleUserLookup(userIdentifier); + + if ( + isDefined(result?.workspaces) && + result.workspaces.length > 0 && + !error + ) { + setActiveTabId(result.workspaces[0].id); + } + }; + + const shouldShowUserData = userLookupResult && !error; + + const activeWorkspace = userLookupResult?.workspaces.find( + (workspace) => workspace.id === activeTabId, + ); + + const tabs = + userLookupResult?.workspaces.map((workspace) => ({ + id: workspace.id, + title: workspace.name, + logo: + getImageAbsoluteURI( + workspace.logo === null ? DEFAULT_WORKSPACE_LOGO : workspace.logo, + ) ?? '', + })) ?? []; + + const renderWorkspaceContent = () => { + if (!activeWorkspace) return null; + + return ( + <> + + 1 ? 'Users' : 'User' + }`} + description={'Total Users'} + /> + + + Feature Flag + Status + + + {activeWorkspace.featureFlags.map((flag) => ( + + {flag.key} + + + handleFeatureFlagUpdate( + activeWorkspace.id, + flag.key, + newValue, + ) + } + /> + + + ))} + + + ); + }; + + return ( + + +
+ + + + + + +
+ + {shouldShowUserData && ( +
+ + + + + + + + + + + + + {renderWorkspaceContent()} + +
+ )} +
+
+ ); +}; diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.module.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.module.ts new file mode 100644 index 000000000000..375a507c229f --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.module.ts @@ -0,0 +1,19 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { AdminPanelResolver } from 'src/engine/core-modules/admin-panel/admin-panel.resolver'; +import { AdminPanelService } from 'src/engine/core-modules/admin-panel/admin-panel.service'; +import { AuthModule } from 'src/engine/core-modules/auth/auth.module'; +import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; +import { User } from 'src/engine/core-modules/user/user.entity'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([User, Workspace, FeatureFlagEntity], 'core'), + AuthModule, + ], + providers: [AdminPanelResolver, AdminPanelService], + exports: [AdminPanelService], +}) +export class AdminPanelModule {} diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.resolver.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.resolver.ts new file mode 100644 index 000000000000..2bc2f099cadf --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.resolver.ts @@ -0,0 +1,57 @@ +import { UseFilters, UseGuards } from '@nestjs/common'; +import { Args, Mutation, Resolver } from '@nestjs/graphql'; + +import { AdminPanelService } from 'src/engine/core-modules/admin-panel/admin-panel.service'; +import { ImpersonateInput } from 'src/engine/core-modules/admin-panel/dtos/impersonate.input'; +import { UpdateWorkspaceFeatureFlagInput } from 'src/engine/core-modules/admin-panel/dtos/update-workspace-feature-flag.input'; +import { UserLookup } from 'src/engine/core-modules/admin-panel/dtos/user-lookup.entity'; +import { UserLookupInput } from 'src/engine/core-modules/admin-panel/dtos/user-lookup.input'; +import { Verify } from 'src/engine/core-modules/auth/dto/verify.entity'; +import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter'; +import { User } from 'src/engine/core-modules/user/user.entity'; +import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator'; +import { UserAuthGuard } from 'src/engine/guards/user-auth.guard'; +import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; + +@Resolver() +@UseFilters(AuthGraphqlApiExceptionFilter) +export class AdminPanelResolver { + constructor(private adminService: AdminPanelService) {} + + @UseGuards(WorkspaceAuthGuard, UserAuthGuard) + @Mutation(() => Verify) + async impersonate( + @Args() impersonateInput: ImpersonateInput, + @AuthUser() user: User, + ): Promise { + return await this.adminService.impersonate(impersonateInput.userId, user); + } + + @UseGuards(WorkspaceAuthGuard, UserAuthGuard) + @Mutation(() => UserLookup) + async userLookupAdminPanel( + @Args() userLookupInput: UserLookupInput, + @AuthUser() user: User, + ): Promise { + return await this.adminService.userLookup( + userLookupInput.userIdentifier, + user, + ); + } + + @UseGuards(WorkspaceAuthGuard, UserAuthGuard) + @Mutation(() => Boolean) + async updateWorkspaceFeatureFlag( + @Args() updateFlagInput: UpdateWorkspaceFeatureFlagInput, + @AuthUser() user: User, + ): Promise { + await this.adminService.updateWorkspaceFeatureFlags( + updateFlagInput.workspaceId, + updateFlagInput.featureFlag, + user, + updateFlagInput.value, + ); + + return true; + } +} diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts new file mode 100644 index 000000000000..7679e4b54c53 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts @@ -0,0 +1,179 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { Repository } from 'typeorm'; + +import { UserLookup } from 'src/engine/core-modules/admin-panel/dtos/user-lookup.entity'; +import { + AuthException, + AuthExceptionCode, +} from 'src/engine/core-modules/auth/auth.exception'; +import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service'; +import { RefreshTokenService } from 'src/engine/core-modules/auth/token/services/refresh-token.service'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; +import { User } from 'src/engine/core-modules/user/user.entity'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; + +@Injectable() +export class AdminPanelService { + constructor( + private readonly accessTokenService: AccessTokenService, + private readonly refreshTokenService: RefreshTokenService, + @InjectRepository(User, 'core') + private readonly userRepository: Repository, + @InjectRepository(Workspace, 'core') + private readonly workspaceRepository: Repository, + @InjectRepository(FeatureFlagEntity, 'core') + private readonly featureFlagRepository: Repository, + ) {} + + async impersonate(userIdentifier: string, userImpersonating: User) { + if (!userImpersonating.canImpersonate) { + throw new AuthException( + 'User cannot impersonate', + AuthExceptionCode.FORBIDDEN_EXCEPTION, + ); + } + + const isEmail = userIdentifier.includes('@'); + + const user = await this.userRepository.findOne({ + where: isEmail ? { email: userIdentifier } : { id: userIdentifier }, + relations: ['defaultWorkspace', 'workspaces', 'workspaces.workspace'], + }); + + if (!user) { + throw new AuthException( + 'User not found', + AuthExceptionCode.INVALID_INPUT, + ); + } + + if (!user.defaultWorkspace.allowImpersonation) { + throw new AuthException( + 'Impersonation not allowed', + AuthExceptionCode.FORBIDDEN_EXCEPTION, + ); + } + + const accessToken = await this.accessTokenService.generateAccessToken( + user.id, + user.defaultWorkspaceId, + ); + const refreshToken = await this.refreshTokenService.generateRefreshToken( + user.id, + user.defaultWorkspaceId, + ); + + return { + user, + tokens: { + accessToken, + refreshToken, + }, + }; + } + + async userLookup( + userIdentifier: string, + userImpersonating: User, + ): Promise { + if (!userImpersonating.canImpersonate) { + throw new AuthException( + 'User cannot access user info', + AuthExceptionCode.FORBIDDEN_EXCEPTION, + ); + } + + const isEmail = userIdentifier.includes('@'); + + const targetUser = await this.userRepository.findOne({ + where: isEmail ? { email: userIdentifier } : { id: userIdentifier }, + relations: [ + 'workspaces', + 'workspaces.workspace', + 'workspaces.workspace.workspaceUsers', + 'workspaces.workspace.workspaceUsers.user', + 'workspaces.workspace.featureFlags', + ], + }); + + if (!targetUser) { + throw new AuthException( + 'User not found', + AuthExceptionCode.INVALID_INPUT, + ); + } + + const allFeatureFlagKeys = Object.values(FeatureFlagKey); + + return { + user: { + id: targetUser.id, + email: targetUser.email, + firstName: targetUser.firstName, + lastName: targetUser.lastName, + }, + workspaces: targetUser.workspaces.map((userWorkspace) => ({ + id: userWorkspace.workspace.id, + name: userWorkspace.workspace.displayName ?? '', + totalUsers: userWorkspace.workspace.workspaceUsers.length, + logo: userWorkspace.workspace.logo, + users: userWorkspace.workspace.workspaceUsers.map((workspaceUser) => ({ + id: workspaceUser.user.id, + email: workspaceUser.user.email, + firstName: workspaceUser.user.firstName, + lastName: workspaceUser.user.lastName, + })), + featureFlags: allFeatureFlagKeys.map((key) => ({ + key, + value: + userWorkspace.workspace.featureFlags?.find( + (flag) => flag.key === key, + )?.value ?? false, + })) as FeatureFlagEntity[], + })), + }; + } + + async updateWorkspaceFeatureFlags( + workspaceId: string, + featureFlag: FeatureFlagKey, + userImpersonating: User, + value: boolean, + ) { + if (!userImpersonating.canImpersonate) { + throw new AuthException( + 'User cannot update feature flags', + AuthExceptionCode.FORBIDDEN_EXCEPTION, + ); + } + + const workspace = await this.workspaceRepository.findOne({ + where: { id: workspaceId }, + relations: ['featureFlags'], + }); + + if (!workspace) { + throw new AuthException( + 'Workspace not found', + AuthExceptionCode.INVALID_INPUT, + ); + } + + const existingFlag = workspace.featureFlags?.find( + (flag) => flag.key === featureFlag, + ); + + if (existingFlag) { + await this.featureFlagRepository.update(existingFlag.id, { value }); + } else { + await this.featureFlagRepository.save({ + key: featureFlag, + value, + workspaceId: workspace.id, + }); + } + } +} diff --git a/packages/twenty-server/src/engine/core-modules/auth/dto/impersonate.input.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/impersonate.input.ts similarity index 100% rename from packages/twenty-server/src/engine/core-modules/auth/dto/impersonate.input.ts rename to packages/twenty-server/src/engine/core-modules/admin-panel/dtos/impersonate.input.ts diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/update-workspace-feature-flag.input.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/update-workspace-feature-flag.input.ts new file mode 100644 index 000000000000..5f134b7d9326 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/update-workspace-feature-flag.input.ts @@ -0,0 +1,21 @@ +import { ArgsType, Field } from '@nestjs/graphql'; + +import { IsBoolean, IsNotEmpty, IsString } from 'class-validator'; + +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; + +@ArgsType() +export class UpdateWorkspaceFeatureFlagInput { + @Field(() => String) + @IsNotEmpty() + @IsString() + workspaceId: string; + + @Field(() => String) + @IsNotEmpty() + featureFlag: FeatureFlagKey; + + @Field(() => Boolean) + @IsBoolean() + value: boolean; +} diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/user-lookup.entity.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/user-lookup.entity.ts new file mode 100644 index 000000000000..f67fe31a471d --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/user-lookup.entity.ts @@ -0,0 +1,48 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; + +@ObjectType() +class UserInfo { + @Field(() => String) + id: string; + + @Field(() => String) + email: string; + + @Field(() => String, { nullable: true }) + firstName?: string; + + @Field(() => String, { nullable: true }) + lastName?: string; +} + +@ObjectType() +class WorkspaceInfo { + @Field(() => String) + id: string; + + @Field(() => String) + name: string; + + @Field(() => String, { nullable: true }) + logo?: string; + + @Field(() => Number) + totalUsers: number; + + @Field(() => [UserInfo]) + users: UserInfo[]; + + @Field(() => [FeatureFlagEntity]) + featureFlags: FeatureFlagEntity[]; +} + +@ObjectType() +export class UserLookup { + @Field(() => UserInfo) + user: UserInfo; + + @Field(() => [WorkspaceInfo]) + workspaces: WorkspaceInfo[]; +} diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/user-lookup.input.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/user-lookup.input.ts new file mode 100644 index 000000000000..971c18635290 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/user-lookup.input.ts @@ -0,0 +1,11 @@ +import { ArgsType, Field } from '@nestjs/graphql'; + +import { IsNotEmpty, IsString } from 'class-validator'; + +@ArgsType() +export class UserLookupInput { + @Field(() => String) + @IsNotEmpty() + @IsString() + userIdentifier: string; +} diff --git a/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts b/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts index 2d6fc31b65a0..e3387c5c6100 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts @@ -22,6 +22,7 @@ import { SwitchWorkspaceService } from 'src/engine/core-modules/auth/services/sw import { SamlAuthStrategy } from 'src/engine/core-modules/auth/strategies/saml.auth.strategy'; import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service'; import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service'; +import { RefreshTokenService } from 'src/engine/core-modules/auth/token/services/refresh-token.service'; import { TransientTokenService } from 'src/engine/core-modules/auth/token/services/transient-token.service'; import { TokenModule } from 'src/engine/core-modules/auth/token/token.module'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; @@ -96,6 +97,7 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy'; MicrosoftAPIsService, AppTokenService, AccessTokenService, + RefreshTokenService, LoginTokenService, ResetPasswordService, SwitchWorkspaceService, @@ -103,6 +105,6 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy'; ApiKeyService, OAuthService, ], - exports: [AccessTokenService, LoginTokenService], + exports: [AccessTokenService, LoginTokenService, RefreshTokenService], }) export class AuthModule {} diff --git a/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts b/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts index d819bc84c582..c74afe573db7 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts @@ -38,7 +38,6 @@ import { UserAuthGuard } from 'src/engine/guards/user-auth.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { ChallengeInput } from './dto/challenge.input'; -import { ImpersonateInput } from './dto/impersonate.input'; import { LoginToken } from './dto/login-token.entity'; import { SignUpInput } from './dto/sign-up.input'; import { ApiKeyToken, AuthTokens } from './dto/token.entity'; @@ -228,15 +227,6 @@ export class AuthResolver { return { tokens: tokens }; } - @UseGuards(WorkspaceAuthGuard, UserAuthGuard) - @Mutation(() => Verify) - async impersonate( - @Args() impersonateInput: ImpersonateInput, - @AuthUser() user: User, - ): Promise { - return await this.authService.impersonate(impersonateInput.userId, user); - } - @UseGuards(WorkspaceAuthGuard) @Mutation(() => ApiKeyToken) async generateApiKeyToken( diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts index 8a99656954a5..7de331e5d6b1 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts @@ -188,53 +188,6 @@ export class AuthService { return { isValid: !!workspace }; } - async impersonate(userIdToImpersonate: string, userImpersonating: User) { - if (!userImpersonating.canImpersonate) { - throw new AuthException( - 'User cannot impersonate', - AuthExceptionCode.FORBIDDEN_EXCEPTION, - ); - } - - const user = await this.userRepository.findOne({ - where: { - id: userIdToImpersonate, - }, - relations: ['defaultWorkspace', 'workspaces', 'workspaces.workspace'], - }); - - if (!user) { - throw new AuthException( - 'User not found', - AuthExceptionCode.USER_NOT_FOUND, - ); - } - - if (!user.defaultWorkspace.allowImpersonation) { - throw new AuthException( - 'Impersonation not allowed', - AuthExceptionCode.FORBIDDEN_EXCEPTION, - ); - } - - const accessToken = await this.accessTokenService.generateAccessToken( - user.id, - user.defaultWorkspaceId, - ); - const refreshToken = await this.refreshTokenService.generateRefreshToken( - user.id, - user.defaultWorkspaceId, - ); - - return { - user, - tokens: { - accessToken, - refreshToken, - }, - }; - } - async generateAuthorizationCode( authorizeAppInput: AuthorizeAppInput, user: User, diff --git a/packages/twenty-server/src/engine/core-modules/core-engine.module.ts b/packages/twenty-server/src/engine/core-modules/core-engine.module.ts index a04e6cc67524..7ba8899150ce 100644 --- a/packages/twenty-server/src/engine/core-modules/core-engine.module.ts +++ b/packages/twenty-server/src/engine/core-modules/core-engine.module.ts @@ -3,6 +3,7 @@ import { HttpAdapterHost } from '@nestjs/core'; import { EventEmitterModule } from '@nestjs/event-emitter'; import { ActorModule } from 'src/engine/core-modules/actor/actor.module'; +import { AdminPanelModule } from 'src/engine/core-modules/admin-panel/admin-panel.module'; import { AppTokenModule } from 'src/engine/core-modules/app-token/app-token.module'; import { AuthModule } from 'src/engine/core-modules/auth/auth.module'; import { BillingModule } from 'src/engine/core-modules/billing/billing.module'; @@ -70,6 +71,7 @@ import { FileModule } from './file/file.module'; WorkspaceEventEmitterModule, ActorModule, TelemetryModule, + AdminPanelModule, EnvironmentModule.forRoot({}), RedisClientModule, FileStorageModule.forRootAsync({ diff --git a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts index 2f29a2788327..bbede9ec949e 100644 --- a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts +++ b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts @@ -130,6 +130,7 @@ export { IconFilter, IconFilterCog, IconFilterOff, + IconFlag, IconFocusCentered, IconFolder, IconFolderPlus, @@ -215,10 +216,11 @@ export { IconRotate2, IconSearch, IconSend, + IconServer, IconSettings, IconSettingsAutomation, - IconSortAZ, IconSlash, + IconSortAZ, IconSortDescending, IconSortZA, IconSparkles, From 812ed6ed6974b24b32e50bea9f847bd1f787cf55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20M?= Date: Thu, 28 Nov 2024 13:44:21 +0100 Subject: [PATCH 7/7] feat: record board component state refactor (#8779) Fix #8758 This PR is migrating the recoil component state from v1 to v2 for board. It also now share some states and logics between board and table, further can be done later. Lastly this PR fix an issue since the PR #8613 that was treating no-value as a normal record-group. --- ...tionsDropdownHiddenRecordGroupsContent.tsx | 30 ++-- .../ObjectOptionsDropdownMenuContent.tsx | 13 +- ...ptionsDropdownRecordGroupFieldsContent.tsx | 13 +- ...tOptionsDropdownRecordGroupSortContent.tsx | 21 ++- ...jectOptionsDropdownRecordGroupsContent.tsx | 50 +++---- .../hooks/useObjectOptionsForBoard.ts | 12 +- .../record-board/components/RecordBoard.tsx | 140 ++++++++++-------- .../components/RecordBoardHeader.tsx | 18 ++- .../hooks/internal/useRecordBoardStates.ts | 103 ------------- .../internal/useSetRecordBoardColumns.ts | 58 -------- .../internal/useSetRecordBoardRecordIds.ts | 63 -------- .../internal/useSetRecordIdsForColumn.ts | 55 ------- .../record-board/hooks/useRecordBoard.ts | 43 ------ .../hooks/useRecordBoardSelection.ts | 27 +++- .../hooks/useSetRecordBoardRecordIds.ts | 110 ++++++++++++++ .../hooks/useSetRecordIdsForColumn.ts | 109 ++++++++++++++ .../components/RecordBoardCard.tsx | 37 +++-- .../components/RecordBoardColumn.tsx | 27 ++-- .../RecordBoardColumnCardsContainer.tsx | 16 +- .../RecordBoardColumnDropdownMenu.tsx | 5 +- .../RecordBoardColumnFetchMoreLoader.tsx | 17 ++- .../RecordBoardColumnHeaderWrapper.tsx | 24 +-- .../hooks/useColumnNewCardActions.ts | 10 +- .../RecordBoardComponentInstanceContext.ts | 4 + ...rdBoardCardSelectedComponentFamilyState.ts | 6 +- ...ordBoardCompactModeActiveComponentState.ts | 6 +- ...BoardFetchingRecordsByColumnFamilyState.ts | 15 +- ...ecordBoardFetchingRecordsComponentState.ts | 7 - .../recordBoardColumnIdsComponentState.ts | 8 - .../recordBoardColumnsComponentFamilyState.ts | 8 - ...cordBoardFieldDefinitionsComponentState.ts | 6 +- .../recordBoardFiltersComponentState.ts | 7 - ...rdKanbanFieldMetadataNameComponentState.ts | 7 - ...RecordIdsByColumnIdComponentFamilyState.ts | 7 - ...ldFetchMoreInColumnComponentFamilyState.ts | 8 +- .../states/recordBoardSortsComponentState.ts | 7 - ...ecordBoardAllRecordIdsComponentSelector.ts | 26 ---- ...cordBoardColumnsComponentFamilySelector.ts | 41 ----- ...BoardSelectedRecordIdsComponentSelector.ts | 30 ++-- ...dShouldFetchMoreComponentFamilySelector.ts | 28 ---- ...isibleFieldDefinitionsComponentSelector.ts | 12 +- .../RecordGroupMenuItemDraggable.tsx | 22 ++- .../RecordGroupsVisibilityDropdownSection.tsx | 21 +-- .../hooks/useCurrentRecordGroupDefinition.ts | 35 +---- .../hooks/useRecordGroupActions.ts | 27 ++-- .../hooks/useRecordGroupReorder.ts | 92 ++++++++---- .../hooks/useRecordGroupVisibility.ts | 128 ++++++++-------- .../record-group/hooks/useRecordGroups.ts | 58 -------- .../record-group/hooks/useSetRecordGroup.ts | 83 +++++++++++ .../recordGroupDefinitionFamilyState.ts | 10 ++ .../recordGroupFieldMetadataComponentState.ts | 11 ++ ...ate.ts => recordGroupIdsComponentState.ts} | 6 +- .../hasRecordGroupsComponentSelector.ts} | 12 +- .../hiddenRecordGroupIdsComponentSelector.ts | 34 +++++ ...recordGroupDefinitionsComponentSelector.ts | 37 +++++ .../visibleRecordGroupIdsComponentSelector.ts | 63 ++++++++ .../utils/sortRecordGroupDefinitions.ts | 8 +- .../record-group/utils/sortedInsert.ts | 20 +++ .../RecordIndexBoardColumnLoaderEffect.tsx | 18 +-- .../components/RecordIndexBoardDataLoader.tsx | 15 +- .../RecordIndexBoardDataLoaderEffect.tsx | 85 +++-------- .../components/RecordIndexContainer.tsx | 49 ++---- .../RecordIndexPageKanbanAddButton.tsx | 24 +-- .../RecordIndexPageKanbanAddMenuItem.tsx | 26 ++-- .../__tests__/useExportFetchRecords.test.ts | 50 ++++--- .../export/hooks/useExportFetchRecords.ts | 12 +- .../hooks/useLoadRecordIndexBoard.ts | 41 +++-- .../hooks/useLoadRecordIndexBoardColumn.ts | 16 +- .../hooks/useLoadRecordIndexTable.ts | 3 +- .../hooks/useRecordBoardRecordGqlFields.ts | 22 +-- .../useRecordIndexPageKanbanAddMenuItem.ts | 12 -- .../recordIndexAllRowIdsComponentState.ts | 10 ++ ...IndexRowIdsByGroupComponentFamilyState.ts} | 8 +- .../record-table/components/RecordTable.tsx | 16 +- .../RecordTableNoRecordGroupRows.tsx | 8 +- .../components/RecordTableRecordGroupRows.tsx | 10 +- .../components/RecordTableEmptyHandler.tsx | 8 +- .../internal/useResetTableRowSelection.ts | 12 +- .../hooks/internal/useSelectAllRows.ts | 14 +- .../hooks/internal/useSetRecordTableData.ts | 57 +++---- .../hooks/useRecordTableMoveFocus.ts | 14 +- .../RecordTableBodyDragDropContext.tsx | 8 +- .../RecordTableNoRecordGroupBody.tsx | 8 +- .../RecordTableRecordGroupBodyEffect.tsx | 2 +- .../RecordTableRecordGroupBodyEffects.tsx | 13 +- .../RecordTableRecordGroupsBody.tsx | 28 ++-- .../allRowsSelectedStatusComponentSelector.ts | 9 +- .../selectedRowIdsComponentSelector.ts | 9 +- .../unselectedRowIdsComponentSelector.ts | 5 +- .../states/tableAllRowIdsComponentState.ts | 8 - .../hooks/useRecoilComponentFamilyStateV2.ts | 30 ++++ .../views/hooks/useSaveCurrentViewGroups.ts | 51 +++++++ .../mapRecordGroupDefinitionsToViewGroups.ts | 12 +- .../mapViewGroupsToRecordGroupDefinitions.ts | 42 ++++-- .../utils/recordGroupDefinitionToViewGroup.ts | 15 ++ 95 files changed, 1355 insertions(+), 1316 deletions(-) delete mode 100644 packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useRecordBoardStates.ts delete mode 100644 packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useSetRecordBoardColumns.ts delete mode 100644 packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useSetRecordBoardRecordIds.ts delete mode 100644 packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useSetRecordIdsForColumn.ts delete mode 100644 packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoard.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-board/hooks/useSetRecordBoardRecordIds.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-board/hooks/useSetRecordIdsForColumn.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext.ts delete mode 100644 packages/twenty-front/src/modules/object-record/record-board/states/isRecordBoardFetchingRecordsComponentState.ts delete mode 100644 packages/twenty-front/src/modules/object-record/record-board/states/recordBoardColumnIdsComponentState.ts delete mode 100644 packages/twenty-front/src/modules/object-record/record-board/states/recordBoardColumnsComponentFamilyState.ts delete mode 100644 packages/twenty-front/src/modules/object-record/record-board/states/recordBoardFiltersComponentState.ts delete mode 100644 packages/twenty-front/src/modules/object-record/record-board/states/recordBoardKanbanFieldMetadataNameComponentState.ts delete mode 100644 packages/twenty-front/src/modules/object-record/record-board/states/recordBoardRecordIdsByColumnIdComponentFamilyState.ts delete mode 100644 packages/twenty-front/src/modules/object-record/record-board/states/recordBoardSortsComponentState.ts delete mode 100644 packages/twenty-front/src/modules/object-record/record-board/states/selectors/recordBoardAllRecordIdsComponentSelector.ts delete mode 100644 packages/twenty-front/src/modules/object-record/record-board/states/selectors/recordBoardColumnsComponentFamilySelector.ts delete mode 100644 packages/twenty-front/src/modules/object-record/record-board/states/selectors/recordBoardShouldFetchMoreComponentFamilySelector.ts delete 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/hooks/useSetRecordGroup.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-group/states/recordGroupDefinitionFamilyState.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-group/states/recordGroupFieldMetadataComponentState.ts rename packages/twenty-front/src/modules/object-record/record-group/states/{recordGroupDefinitionsComponentState.ts => recordGroupIdsComponentState.ts} (72%) rename packages/twenty-front/src/modules/object-record/record-group/states/{hasRecordGroupDefinitionsComponentSelector.ts => selectors/hasRecordGroupsComponentSelector.ts} (52%) create mode 100644 packages/twenty-front/src/modules/object-record/record-group/states/selectors/hiddenRecordGroupIdsComponentSelector.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-group/states/selectors/recordGroupDefinitionsComponentSelector.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-group/utils/sortedInsert.ts delete mode 100644 packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexPageKanbanAddMenuItem.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-index/states/recordIndexAllRowIdsComponentState.ts rename packages/twenty-front/src/modules/object-record/{record-table/states/tableRowIdsByGroupComponentFamilyState.ts => record-index/states/recordIndexRowIdsByGroupComponentFamilyState.ts} (50%) delete mode 100644 packages/twenty-front/src/modules/object-record/record-table/states/tableAllRowIdsComponentState.ts create mode 100644 packages/twenty-front/src/modules/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyStateV2.ts create mode 100644 packages/twenty-front/src/modules/views/utils/recordGroupDefinitionToViewGroup.ts diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownHiddenRecordGroupsContent.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownHiddenRecordGroupsContent.tsx index 5e6810cfb0ce..35161f2fe3ba 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownHiddenRecordGroupsContent.tsx +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownHiddenRecordGroupsContent.tsx @@ -10,46 +10,50 @@ import { useObjectNamePluralFromSingular } from '@/object-metadata/hooks/useObje import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown'; import { RecordGroupsVisibilityDropdownSection } from '@/object-record/record-group/components/RecordGroupsVisibilityDropdownSection'; -import { useRecordGroups } from '@/object-record/record-group/hooks/useRecordGroups'; import { useRecordGroupVisibility } from '@/object-record/record-group/hooks/useRecordGroupVisibility'; +import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState'; +import { hiddenRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/hiddenRecordGroupIdsComponentSelector'; import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useLocation } from 'react-router-dom'; import { useSetRecoilState } from 'recoil'; export const ObjectOptionsDropdownHiddenRecordGroupsContent = () => { const { currentContentId, - viewType, recordIndexId, objectMetadataItem, onContentChange, closeDropdown, } = useOptionsDropdown(); - const { objectNamePlural } = useObjectNamePluralFromSingular({ - objectNameSingular: objectMetadataItem.nameSingular, - }); + const recordGroupFieldMetadata = useRecoilComponentValueV2( + recordGroupFieldMetadataComponentState, + ); - const { hiddenRecordGroups, viewGroupFieldMetadataItem } = useRecordGroups({ + const hiddenRecordGroupIds = useRecoilComponentValueV2( + hiddenRecordGroupIdsComponentSelector, + ); + + const { objectNamePlural } = useObjectNamePluralFromSingular({ objectNameSingular: objectMetadataItem.nameSingular, }); const { handleVisibilityChange: handleRecordGroupVisibilityChange } = useRecordGroupVisibility({ viewBarId: recordIndexId, - viewType, }); const viewGroupSettingsUrl = getSettingsPagePath( SettingsPath.ObjectFieldEdit, { objectSlug: objectNamePlural, - fieldSlug: viewGroupFieldMetadataItem?.name ?? '', + fieldSlug: recordGroupFieldMetadata?.name ?? '', }, ); @@ -61,11 +65,11 @@ export const ObjectOptionsDropdownHiddenRecordGroupsContent = () => { useEffect(() => { if ( currentContentId === 'hiddenRecordGroups' && - hiddenRecordGroups.length === 0 + hiddenRecordGroupIds.length === 0 ) { onContentChange('recordGroups'); } - }, [hiddenRecordGroups, currentContentId, onContentChange]); + }, [hiddenRecordGroupIds, currentContentId, onContentChange]); return ( <> @@ -74,13 +78,13 @@ export const ObjectOptionsDropdownHiddenRecordGroupsContent = () => { StartIcon={IconChevronLeft} onClick={() => onContentChange('recordGroups')} > - Hidden {viewGroupFieldMetadataItem?.label} + Hidden {recordGroupFieldMetadata?.label}
{ objectNameSingular: objectMetadataItem.nameSingular, }); + const recordGroupFieldMetadata = useRecoilComponentValueV2( + recordGroupFieldMetadataComponentState, + ); + useScopedHotkeys( [Key.Escape], () => { @@ -64,10 +69,6 @@ export const ObjectOptionsDropdownMenuContent = () => { viewBarId: recordIndexId, }); - const { viewGroupFieldMetadataItem } = useRecordGroups({ - objectNameSingular: objectMetadataItem.nameSingular, - }); - const { openObjectRecordsSpreasheetImportDialog } = useOpenObjectRecordsSpreadsheetImportDialog( objectMetadataItem.nameSingular, @@ -113,7 +114,7 @@ export const ObjectOptionsDropdownMenuContent = () => { onClick={() => onContentChange('recordGroups')} LeftIcon={IconLayoutList} text="Group by" - contextualText={viewGroupFieldMetadataItem?.label} + contextualText={recordGroupFieldMetadata?.label} hasSubMenu /> )} diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupFieldsContent.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupFieldsContent.tsx index bb866024237d..238442058285 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupFieldsContent.tsx +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupFieldsContent.tsx @@ -12,7 +12,7 @@ import { useObjectNamePluralFromSingular } from '@/object-metadata/hooks/useObje import { StyledInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect'; import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown'; import { useSearchRecordGroupField } from '@/object-record/object-options-dropdown/hooks/useSearchRecordGroupField'; -import { useRecordGroups } from '@/object-record/record-group/hooks/useRecordGroups'; +import { hiddenRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/hiddenRecordGroupIdsComponentSelector'; import { useHandleRecordGroupField } from '@/object-record/record-index/hooks/useHandleRecordGroupField'; import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; @@ -20,6 +20,7 @@ import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenu import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useLocation } from 'react-router-dom'; import { useSetRecoilState } from 'recoil'; @@ -38,9 +39,9 @@ export const ObjectOptionsDropdownRecordGroupFieldsContent = () => { objectNameSingular: objectMetadataItem.nameSingular, }); - const { hiddenRecordGroups } = useRecordGroups({ - objectNameSingular: objectMetadataItem.nameSingular, - }); + const hiddenRecordGroupIds = useRecoilComponentValueV2( + hiddenRecordGroupIdsComponentSelector, + ); const { recordGroupFieldSearchInput, @@ -68,11 +69,11 @@ export const ObjectOptionsDropdownRecordGroupFieldsContent = () => { useEffect(() => { if ( currentContentId === 'hiddenRecordGroups' && - hiddenRecordGroups.length === 0 + hiddenRecordGroupIds.length === 0 ) { onContentChange('recordGroups'); } - }, [hiddenRecordGroups, currentContentId, onContentChange]); + }, [hiddenRecordGroupIds, currentContentId, onContentChange]); return ( <> diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupSortContent.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupSortContent.tsx index fcfeea029b31..b2a8c623da0d 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupSortContent.tsx +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupSortContent.tsx @@ -8,24 +8,21 @@ import { } from 'twenty-ui'; import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown'; -import { useRecordGroups } from '@/object-record/record-group/hooks/useRecordGroups'; +import { hiddenRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/hiddenRecordGroupIdsComponentSelector'; import { RecordGroupSort } from '@/object-record/record-group/types/RecordGroupSort'; import { recordIndexRecordGroupSortComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupSortComponentState'; import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; export const ObjectOptionsDropdownRecordGroupSortContent = () => { - const { - currentContentId, - objectMetadataItem, - onContentChange, - closeDropdown, - } = useOptionsDropdown(); + const { currentContentId, onContentChange, closeDropdown } = + useOptionsDropdown(); - const { hiddenRecordGroups } = useRecordGroups({ - objectNameSingular: objectMetadataItem.nameSingular, - }); + const hiddenRecordGroupIds = useRecoilComponentValueV2( + hiddenRecordGroupIdsComponentSelector, + ); const setRecordGroupSort = useSetRecoilComponentStateV2( recordIndexRecordGroupSortComponentState, @@ -39,11 +36,11 @@ export const ObjectOptionsDropdownRecordGroupSortContent = () => { useEffect(() => { if ( currentContentId === 'hiddenRecordGroups' && - hiddenRecordGroups.length === 0 + hiddenRecordGroupIds.length === 0 ) { onContentChange('recordGroups'); } - }, [hiddenRecordGroups, currentContentId, onContentChange]); + }, [hiddenRecordGroupIds, currentContentId, onContentChange]); return ( <> diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupsContent.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupsContent.tsx index 94db1268f1c4..19a892be1651 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupsContent.tsx +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupsContent.tsx @@ -13,8 +13,10 @@ import { import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown'; import { RecordGroupsVisibilityDropdownSection } from '@/object-record/record-group/components/RecordGroupsVisibilityDropdownSection'; 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 { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState'; +import { hiddenRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/hiddenRecordGroupIdsComponentSelector'; +import { visibleRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector'; import { recordIndexRecordGroupHideComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupHideComponentState'; import { recordIndexRecordGroupIsDraggableSortComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexRecordGroupIsDraggableSortComponentSelector'; import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader'; @@ -26,22 +28,20 @@ import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; export const ObjectOptionsDropdownRecordGroupsContent = () => { const isViewGroupEnabled = useIsFeatureEnabled('IS_VIEW_GROUPS_ENABLED'); - const { - currentContentId, - viewType, - recordIndexId, - objectMetadataItem, - onContentChange, - resetContent, - } = useOptionsDropdown(); + const { currentContentId, recordIndexId, onContentChange, resetContent } = + useOptionsDropdown(); - const { - hiddenRecordGroups, - visibleRecordGroups, - viewGroupFieldMetadataItem, - } = useRecordGroups({ - objectNameSingular: objectMetadataItem.nameSingular, - }); + const recordGroupFieldMetadata = useRecoilComponentValueV2( + recordGroupFieldMetadataComponentState, + ); + + const visibleRecordGroupIds = useRecoilComponentValueV2( + visibleRecordGroupIdsComponentSelector, + ); + + const hiddenRecordGroupIds = useRecoilComponentValueV2( + hiddenRecordGroupIdsComponentSelector, + ); const isDragableSortRecordGroup = useRecoilComponentValueV2( recordIndexRecordGroupIsDraggableSortComponentSelector, @@ -56,23 +56,21 @@ export const ObjectOptionsDropdownRecordGroupsContent = () => { handleHideEmptyRecordGroupChange, } = useRecordGroupVisibility({ viewBarId: recordIndexId, - viewType, }); const { handleOrderChange: handleRecordGroupOrderChange } = useRecordGroupReorder({ - objectNameSingular: objectMetadataItem.nameSingular, viewBarId: recordIndexId, }); useEffect(() => { if ( currentContentId === 'hiddenRecordGroups' && - hiddenRecordGroups.length === 0 + hiddenRecordGroupIds.length === 0 ) { onContentChange('recordGroups'); } - }, [hiddenRecordGroups, currentContentId, onContentChange]); + }, [hiddenRecordGroupIds, currentContentId, onContentChange]); return ( <> @@ -86,9 +84,9 @@ export const ObjectOptionsDropdownRecordGroupsContent = () => { onClick={() => onContentChange('recordGroupFields')} LeftIcon={IconLayoutList} text={ - !viewGroupFieldMetadataItem + !recordGroupFieldMetadata ? 'Group by' - : `Group by "${viewGroupFieldMetadataItem.label}"` + : `Group by "${recordGroupFieldMetadata.label}"` } hasSubMenu /> @@ -108,12 +106,12 @@ export const ObjectOptionsDropdownRecordGroupsContent = () => { toggleSize="small" /> - {visibleRecordGroups.length > 0 && ( + {visibleRecordGroupIds.length > 0 && ( <> { /> )} - {hiddenRecordGroups.length > 0 && ( + {hiddenRecordGroupIds.length > 0 && ( <> onContentChange('hiddenRecordGroups')} LeftIcon={IconEyeOff} - text={`Hidden ${viewGroupFieldMetadataItem?.label ?? ''}`} + text={`Hidden ${recordGroupFieldMetadata?.label ?? ''}`} /> diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard.ts b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard.ts index ccb19275b709..3dba54dc3835 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard.ts +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard.ts @@ -4,10 +4,11 @@ import { useRecoilState } from 'recoil'; import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard'; +import { isRecordBoardCompactModeActiveComponentState } from '@/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState'; import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; +import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; import { useSaveCurrentViewFields } from '@/views/hooks/useSaveCurrentViewFields'; import { useUpdateCurrentView } from '@/views/hooks/useUpdateCurrentView'; import { GraphQLView } from '@/views/types/GraphQLView'; @@ -32,11 +33,12 @@ export const useObjectOptionsForBoard = ({ const { saveViewFields } = useSaveCurrentViewFields(viewBarId); const { updateCurrentView } = useUpdateCurrentView(viewBarId); - const { isCompactModeActiveState } = useRecordBoard(recordBoardId); - const [isCompactModeActive, setIsCompactModeActive] = useRecoilState( - isCompactModeActiveState, - ); + const [isCompactModeActive, setIsCompactModeActive] = + useRecoilComponentStateV2( + isRecordBoardCompactModeActiveComponentState, + recordBoardId, + ); const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular, 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 4c5076d9f8af..a28c71b8ee02 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 @@ -1,7 +1,7 @@ import styled from '@emotion/styled'; import { DragDropContext, OnDragEndResponder } from '@hello-pangea/dnd'; // Atlassian dnd does not support StrictMode from RN 18, so we use a fork @hello-pangea/dnd https://github.com/atlassian/react-beautiful-dnd/issues/2350 import { useContext, useRef } from 'react'; -import { useRecoilCallback, useRecoilValue } from 'recoil'; +import { useRecoilCallback } from 'recoil'; import { Key } from 'ts-key-enum'; import { ActionBarHotkeyScope } from '@/action-menu/types/ActionBarHotKeyScope'; @@ -9,11 +9,15 @@ import { RecordBoardHeader } from '@/object-record/record-board/components/Recor import { RecordBoardStickyHeaderEffect } from '@/object-record/record-board/components/RecordBoardStickyHeaderEffect'; import { RECORD_BOARD_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-board/constants/RecordBoardClickOutsideListenerId'; import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext'; -import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; import { useRecordBoardSelection } from '@/object-record/record-board/hooks/useRecordBoardSelection'; import { RecordBoardColumn } from '@/object-record/record-board/record-board-column/components/RecordBoardColumn'; import { RecordBoardScope } from '@/object-record/record-board/scopes/RecordBoardScope'; +import { RecordBoardComponentInstanceContext } from '@/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext'; import { getDraggedRecordPosition } from '@/object-record/record-board/utils/getDraggedRecordPosition'; +import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; +import { visibleRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector'; +import { recordIndexAllRowIdsComponentState } from '@/object-record/record-index/states/recordIndexAllRowIdsComponentState'; +import { recordIndexRowIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRowIdsByGroupComponentFamilyState'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope'; import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect'; @@ -21,6 +25,9 @@ import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useListenClickOutsideV2 } from '@/ui/utilities/pointer-event/hooks/useListenClickOutsideV2'; import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId'; import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; +import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; import { useScrollRestoration } from '~/hooks/useScrollRestoration'; const StyledContainer = styled.div` @@ -58,14 +65,17 @@ export const RecordBoard = () => { useContext(RecordBoardContext); const boardRef = useRef(null); - const { - columnIdsState, - columnsFamilySelector, - recordIdsByColumnIdFamilyState, - allRecordIdsSelector, - } = useRecordBoardStates(recordBoardId); + const visibleRecordGroupIds = useRecoilComponentValueV2( + visibleRecordGroupIdsComponentSelector, + ); + + const recordIndexRowIdsByGroupFamilyState = useRecoilComponentCallbackStateV2( + recordIndexRowIdsByGroupComponentFamilyState, + ); - const columnIds = useRecoilValue(columnIdsState); + const recordIndexAllRowIdsState = useRecoilComponentCallbackStateV2( + recordIndexAllRowIdsComponentState, + ); const { resetRecordSelection, setRecordAsSelected } = useRecordBoardSelection(recordBoardId); @@ -85,15 +95,16 @@ export const RecordBoard = () => { const selectAll = useRecoilCallback( ({ snapshot }) => () => { - const allRecordIds = snapshot - .getLoadable(allRecordIdsSelector()) - .getValue(); + const allRecordIds = getSnapshotValue( + snapshot, + recordIndexAllRowIdsState, + ); for (const recordId of allRecordIds) { setRecordAsSelected(recordId, true); } }, - [allRecordIdsSelector, setRecordAsSelected], + [recordIndexAllRowIdsState, setRecordAsSelected], ); useScopedHotkeys('ctrl+a,meta+a', selectAll, TableHotkeyScope.Table); @@ -111,42 +122,40 @@ export const RecordBoard = () => { if (!result.destination) return; const draggedRecordId = result.draggableId; - const sourceColumnId = result.source.droppableId; - const destinationColumnId = result.destination.droppableId; + const sourceRecordGroupId = result.source.droppableId; + const destinationRecordGroupId = result.destination.droppableId; const destinationIndexInColumn = result.destination.index; - if (!destinationColumnId || !selectFieldMetadataItem) return; + if (!destinationRecordGroupId || !selectFieldMetadataItem) return; - const column = snapshot - .getLoadable(columnsFamilySelector(destinationColumnId)) - .getValue(); + const recordGroup = getSnapshotValue( + snapshot, + recordGroupDefinitionFamilyState(destinationRecordGroupId), + ); - if (!column) return; + if (!recordGroup) return; - const destinationColumnRecordIds = snapshot - .getLoadable(recordIdsByColumnIdFamilyState(destinationColumnId)) - .getValue(); - const otherRecordsInDestinationColumn = - sourceColumnId === destinationColumnId - ? destinationColumnRecordIds.filter( + const destinationRecordByGroupIds = getSnapshotValue( + snapshot, + recordIndexRowIdsByGroupFamilyState(destinationRecordGroupId), + ); + const otherRecordIdsInDestinationColumn = + sourceRecordGroupId === destinationRecordGroupId + ? destinationRecordByGroupIds.filter( (recordId) => recordId !== draggedRecordId, ) - : destinationColumnRecordIds; + : destinationRecordByGroupIds; const recordBeforeId = - otherRecordsInDestinationColumn[destinationIndexInColumn - 1]; + otherRecordIdsInDestinationColumn[destinationIndexInColumn - 1]; const recordBefore = recordBeforeId - ? snapshot - .getLoadable(recordStoreFamilyState(recordBeforeId)) - .getValue() + ? getSnapshotValue(snapshot, recordStoreFamilyState(recordBeforeId)) : null; const recordAfterId = - otherRecordsInDestinationColumn[destinationIndexInColumn]; + otherRecordIdsInDestinationColumn[destinationIndexInColumn]; const recordAfter = recordAfterId - ? snapshot - .getLoadable(recordStoreFamilyState(recordAfterId)) - .getValue() + ? getSnapshotValue(snapshot, recordStoreFamilyState(recordAfterId)) : null; const draggedRecordPosition = getDraggedRecordPosition( @@ -157,14 +166,13 @@ export const RecordBoard = () => { updateOneRecord({ idToUpdate: draggedRecordId, updateOneRecordInput: { - [selectFieldMetadataItem.name]: column.value, + [selectFieldMetadataItem.name]: recordGroup.value, position: draggedRecordPosition, }, }); }, [ - columnsFamilySelector, - recordIdsByColumnIdFamilyState, + recordIndexRowIdsByGroupFamilyState, selectFieldMetadataItem, updateOneRecord, ], @@ -182,32 +190,36 @@ export const RecordBoard = () => { onColumnsChange={() => {}} onFieldsChange={() => {}} > - - - - - - - - - {columnIds.map((columnId) => ( - - ))} - - - - - - - - + + + + + + + + + + {visibleRecordGroupIds.map((recordGroupId) => ( + + ))} + + + + + + + + + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardHeader.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardHeader.tsx index b284532386dd..e62c658115fa 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardHeader.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardHeader.tsx @@ -1,7 +1,6 @@ -import { useRecoilValue } from 'recoil'; - -import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; import { RecordBoardColumnHeaderWrapper } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderWrapper'; +import { visibleRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import styled from '@emotion/styled'; const StyledHeaderContainer = styled.div` @@ -24,14 +23,17 @@ const StyledHeaderContainer = styled.div` `; export const RecordBoardHeader = () => { - const { columnIdsState } = useRecordBoardStates(); - - const columnIds = useRecoilValue(columnIdsState); + const visibleRecordGroupIds = useRecoilComponentValueV2( + visibleRecordGroupIdsComponentSelector, + ); return ( - {columnIds.map((columnId) => ( - + {visibleRecordGroupIds.map((recordGroupId) => ( + ))} ); 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 deleted file mode 100644 index a26506254b67..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useRecordBoardStates.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { RecordBoardScopeInternalContext } from '@/object-record/record-board/scopes/scope-internal-context/RecordBoardScopeInternalContext'; -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'; -import { isRecordBoardFetchingRecordsComponentState } from '@/object-record/record-board/states/isRecordBoardFetchingRecordsComponentState'; -import { recordBoardColumnIdsComponentState } from '@/object-record/record-board/states/recordBoardColumnIdsComponentState'; -import { recordBoardFieldDefinitionsComponentState } from '@/object-record/record-board/states/recordBoardFieldDefinitionsComponentState'; -import { recordBoardFiltersComponentState } from '@/object-record/record-board/states/recordBoardFiltersComponentState'; -import { recordBoardKanbanFieldMetadataNameComponentState } from '@/object-record/record-board/states/recordBoardKanbanFieldMetadataNameComponentState'; -import { recordBoardObjectSingularNameComponentState } from '@/object-record/record-board/states/recordBoardObjectSingularNameComponentState'; -import { recordBoardRecordIdsByColumnIdComponentFamilyState } from '@/object-record/record-board/states/recordBoardRecordIdsByColumnIdComponentFamilyState'; -import { recordBoardShouldFetchMoreInColumnComponentFamilyState } from '@/object-record/record-board/states/recordBoardShouldFetchMoreInColumnComponentFamilyState'; -import { recordBoardSortsComponentState } from '@/object-record/record-board/states/recordBoardSortsComponentState'; -import { recordBoardAllRecordIdsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardAllRecordIdsComponentSelector'; -import { recordBoardColumnsComponentFamilySelector } from '@/object-record/record-board/states/selectors/recordBoardColumnsComponentFamilySelector'; -import { recordBoardSelectedRecordIdsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardSelectedRecordIdsComponentSelector'; -import { recordBoardShouldFetchMoreComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardShouldFetchMoreComponentFamilySelector'; -import { recordBoardVisibleFieldDefinitionsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardVisibleFieldDefinitionsComponentSelector'; -import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId'; -import { getScopeIdOrUndefinedFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdOrUndefinedFromComponentId'; -import { extractComponentFamilyState } from '@/ui/utilities/state/component-state/utils/extractComponentFamilyState'; -import { extractComponentReadOnlySelector } from '@/ui/utilities/state/component-state/utils/extractComponentReadOnlySelector'; -import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState'; - -export const useRecordBoardStates = (recordBoardId?: string) => { - const scopeId = useAvailableScopeIdOrThrow( - RecordBoardScopeInternalContext, - getScopeIdOrUndefinedFromComponentId(recordBoardId), - ); - - return { - scopeId, - objectSingularNameState: extractComponentState( - recordBoardObjectSingularNameComponentState, - scopeId, - ), - kanbanFieldMetadataNameState: extractComponentState( - recordBoardKanbanFieldMetadataNameComponentState, - scopeId, - ), - isFetchingRecordState: extractComponentState( - isRecordBoardFetchingRecordsComponentState, - scopeId, - ), - isFetchingRecordsByColumnState: extractComponentFamilyState( - isRecordBoardFetchingRecordsByColumnFamilyState, - scopeId, - ), - columnIdsState: extractComponentState( - recordBoardColumnIdsComponentState, - scopeId, - ), - columnsFamilySelector: extractComponentFamilyState( - recordBoardColumnsComponentFamilySelector, - scopeId, - ), - - filtersState: extractComponentState( - recordBoardFiltersComponentState, - scopeId, - ), - sortsState: extractComponentState(recordBoardSortsComponentState, scopeId), - fieldDefinitionsState: extractComponentState( - recordBoardFieldDefinitionsComponentState, - scopeId, - ), - visibleFieldDefinitionsState: extractComponentReadOnlySelector( - recordBoardVisibleFieldDefinitionsComponentSelector, - scopeId, - ), - - recordIdsByColumnIdFamilyState: extractComponentFamilyState( - recordBoardRecordIdsByColumnIdComponentFamilyState, - scopeId, - ), - isRecordBoardCardSelectedFamilyState: extractComponentFamilyState( - isRecordBoardCardSelectedComponentFamilyState, - scopeId, - ), - allRecordIdsSelector: extractComponentReadOnlySelector( - recordBoardAllRecordIdsComponentSelector, - scopeId, - ), - selectedRecordIdsSelector: extractComponentReadOnlySelector( - recordBoardSelectedRecordIdsComponentSelector, - scopeId, - ), - - isCompactModeActiveState: extractComponentState( - isRecordBoardCompactModeActiveComponentState, - scopeId, - ), - - shouldFetchMoreInColumnFamilyState: extractComponentFamilyState( - recordBoardShouldFetchMoreInColumnComponentFamilyState, - scopeId, - ), - shouldFetchMoreSelector: extractComponentReadOnlySelector( - recordBoardShouldFetchMoreComponentSelector, - 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 deleted file mode 100644 index 58e00b9900ba..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useSetRecordBoardColumns.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { useRecoilCallback } from 'recoil'; - -import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; -import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; -import { sortRecordGroupDefinitions } from '@/object-record/record-group/utils/sortRecordGroupDefinitions'; -import { recordIndexRecordGroupSortComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupSortComponentState'; -import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; -import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; - -export const useSetRecordBoardColumns = (recordBoardId?: string) => { - const { scopeId, columnIdsState, columnsFamilySelector } = - useRecordBoardStates(recordBoardId); - - const recordGroupSort = useRecoilComponentValueV2( - recordIndexRecordGroupSortComponentState, - recordBoardId, - ); - - const setColumns = useRecoilCallback( - ({ set, snapshot }) => - (columns: RecordGroupDefinition[]) => { - const currentColumnsIds = snapshot - .getLoadable(columnIdsState) - .getValue(); - - const sortedColumns = sortRecordGroupDefinitions( - columns, - recordGroupSort, - ); - - const columnIds = sortedColumns - .filter(({ isVisible }) => isVisible) - .map(({ id }) => id); - - if (!isDeeplyEqual(currentColumnsIds, columnIds)) { - set(columnIdsState, columnIds); - } - - columns.forEach((column) => { - const currentColumn = snapshot - .getLoadable(columnsFamilySelector(column.id)) - .getValue(); - - if (isDeeplyEqual(currentColumn, column)) { - return; - } - - set(columnsFamilySelector(column.id), column); - }); - }, - [columnIdsState, recordGroupSort, columnsFamilySelector], - ); - - return { - scopeId, - setColumns, - }; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useSetRecordBoardRecordIds.ts b/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useSetRecordBoardRecordIds.ts deleted file mode 100644 index e5b61e4e6d94..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useSetRecordBoardRecordIds.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { useRecoilCallback } from 'recoil'; - -import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; -import { ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { sortRecordsByPosition } from '@/object-record/utils/sortRecordsByPosition'; -import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; - -export const useSetRecordBoardRecordIds = (recordBoardId?: string) => { - const { - scopeId, - recordIdsByColumnIdFamilyState, - columnsFamilySelector, - columnIdsState, - kanbanFieldMetadataNameState, - } = useRecordBoardStates(recordBoardId); - - const setRecordIds = useRecoilCallback( - ({ set, snapshot }) => - (records: ObjectRecord[]) => { - const columnIds = snapshot.getLoadable(columnIdsState).getValue(); - - columnIds.forEach((columnId) => { - const column = snapshot - .getLoadable(columnsFamilySelector(columnId)) - .getValue(); - - const existingColumnRecordIds = snapshot - .getLoadable(recordIdsByColumnIdFamilyState(columnId)) - .getValue(); - - const kanbanFieldMetadataName = snapshot - .getLoadable(kanbanFieldMetadataNameState) - .getValue(); - - if (!kanbanFieldMetadataName) { - return; - } - - const columnRecordIds = records - .filter( - (record) => record[kanbanFieldMetadataName] === column?.value, - ) - .sort(sortRecordsByPosition) - .map((record) => record.id); - - if (!isDeeplyEqual(existingColumnRecordIds, columnRecordIds)) { - set(recordIdsByColumnIdFamilyState(columnId), columnRecordIds); - } - }); - }, - [ - columnIdsState, - columnsFamilySelector, - recordIdsByColumnIdFamilyState, - kanbanFieldMetadataNameState, - ], - ); - - return { - scopeId, - setRecordIds, - }; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useSetRecordIdsForColumn.ts b/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useSetRecordIdsForColumn.ts deleted file mode 100644 index 25138622df44..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useSetRecordIdsForColumn.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { useRecoilCallback } from 'recoil'; - -import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; -import { ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { sortRecordsByPosition } from '@/object-record/utils/sortRecordsByPosition'; -import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; - -export const useSetRecordIdsForColumn = (recordBoardId?: string) => { - const { - scopeId, - recordIdsByColumnIdFamilyState, - columnsFamilySelector, - kanbanFieldMetadataNameState, - } = useRecordBoardStates(recordBoardId); - - const setRecordIdsForColumn = useRecoilCallback( - ({ set, snapshot }) => - (columnId: string, records: ObjectRecord[]) => { - const column = snapshot - .getLoadable(columnsFamilySelector(columnId)) - .getValue(); - - const existingColumnRecordIds = snapshot - .getLoadable(recordIdsByColumnIdFamilyState(columnId)) - .getValue(); - - const kanbanFieldMetadataName = snapshot - .getLoadable(kanbanFieldMetadataNameState) - .getValue(); - - if (!kanbanFieldMetadataName) { - return; - } - - const columnRecordIds = records - .filter((record) => record[kanbanFieldMetadataName] === column?.value) - .sort(sortRecordsByPosition) - .map((record) => record.id); - - if (!isDeeplyEqual(existingColumnRecordIds, columnRecordIds)) { - set(recordIdsByColumnIdFamilyState(columnId), columnRecordIds); - } - }, - [ - columnsFamilySelector, - recordIdsByColumnIdFamilyState, - kanbanFieldMetadataNameState, - ], - ); - - return { - scopeId, - setRecordIdsForColumn, - }; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoard.ts b/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoard.ts deleted file mode 100644 index 755703d0e900..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoard.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { useSetRecoilState } from 'recoil'; - -import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; -import { useSetRecordBoardColumns } from '@/object-record/record-board/hooks/internal/useSetRecordBoardColumns'; -import { useSetRecordBoardRecordIds } from '@/object-record/record-board/hooks/internal/useSetRecordBoardRecordIds'; -import { useSetRecordIdsForColumn } from '@/object-record/record-board/hooks/internal/useSetRecordIdsForColumn'; - -export const useRecordBoard = (recordBoardId?: string) => { - const { - scopeId, - fieldDefinitionsState, - objectSingularNameState, - selectedRecordIdsSelector, - isCompactModeActiveState, - kanbanFieldMetadataNameState, - shouldFetchMoreSelector, - isFetchingRecordsByColumnState, - } = useRecordBoardStates(recordBoardId); - - const { setColumns } = useSetRecordBoardColumns(recordBoardId); - const { setRecordIds } = useSetRecordBoardRecordIds(recordBoardId); - const { setRecordIdsForColumn } = useSetRecordIdsForColumn(recordBoardId); - - const setFieldDefinitions = useSetRecoilState(fieldDefinitionsState); - const setObjectSingularName = useSetRecoilState(objectSingularNameState); - const setKanbanFieldMetadataName = useSetRecoilState( - kanbanFieldMetadataNameState, - ); - - return { - scopeId, - setColumns, - setRecordIds, - setFieldDefinitions, - setObjectSingularName, - setKanbanFieldMetadataName, - selectedRecordIdsSelector, - isCompactModeActiveState, - shouldFetchMoreSelector, - setRecordIdsForColumn, - isFetchingRecordsByColumnState, - }; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoardSelection.ts b/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoardSelection.ts index baba7e9f9a5a..8c20d3933acd 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoardSelection.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoardSelection.ts @@ -2,13 +2,25 @@ import { useRecoilCallback } from 'recoil'; import { getActionMenuDropdownIdFromActionMenuId } from '@/action-menu/utils/getActionMenuDropdownIdFromActionMenuId'; import { getActionMenuIdFromRecordIndexId } from '@/action-menu/utils/getActionMenuIdFromRecordIndexId'; -import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; +import { isRecordBoardCardSelectedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardSelectedComponentFamilyState'; +import { recordBoardSelectedRecordIdsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardSelectedRecordIdsComponentSelector'; import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState'; +import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState'; +import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; export const useRecordBoardSelection = (recordBoardId: string) => { - const { selectedRecordIdsSelector, isRecordBoardCardSelectedFamilyState } = - useRecordBoardStates(recordBoardId); + const isRecordBoardCardSelectedFamilyState = + useRecoilComponentCallbackStateV2( + isRecordBoardCardSelectedComponentFamilyState, + recordBoardId, + ); + + const recordBoardSelectedRecordIdsSelector = + useRecoilComponentCallbackStateV2( + recordBoardSelectedRecordIdsComponentSelector, + recordBoardId, + ); const isActionMenuDropdownOpenState = extractComponentState( isDropdownOpenComponentState, @@ -22,9 +34,10 @@ export const useRecordBoardSelection = (recordBoardId: string) => { () => { set(isActionMenuDropdownOpenState, false); - const recordIds = snapshot - .getLoadable(selectedRecordIdsSelector()) - .getValue(); + const recordIds = getSnapshotValue( + snapshot, + recordBoardSelectedRecordIdsSelector, + ); for (const recordId of recordIds) { set(isRecordBoardCardSelectedFamilyState(recordId), false); @@ -32,7 +45,7 @@ export const useRecordBoardSelection = (recordBoardId: string) => { }, [ isActionMenuDropdownOpenState, - selectedRecordIdsSelector, + recordBoardSelectedRecordIdsSelector, isRecordBoardCardSelectedFamilyState, ], ); diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/useSetRecordBoardRecordIds.ts b/packages/twenty-front/src/modules/object-record/record-board/hooks/useSetRecordBoardRecordIds.ts new file mode 100644 index 000000000000..0194a5bf366a --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/useSetRecordBoardRecordIds.ts @@ -0,0 +1,110 @@ +import { useRecoilCallback } from 'recoil'; + +import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; +import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState'; +import { visibleRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector'; +import { recordIndexAllRowIdsComponentState } from '@/object-record/record-index/states/recordIndexAllRowIdsComponentState'; +import { recordIndexRowIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRowIdsByGroupComponentFamilyState'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { sortRecordsByPosition } from '@/object-record/utils/sortRecordsByPosition'; +import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; +import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; +import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; +import { isDefined } from '~/utils/isDefined'; + +export const useSetRecordBoardRecordIds = (recordBoardId?: string) => { + const visibleRecordGroupIdsSelector = useRecoilComponentCallbackStateV2( + visibleRecordGroupIdsComponentSelector, + ); + + const recordGroupFieldMetadataState = useRecoilComponentCallbackStateV2( + recordGroupFieldMetadataComponentState, + recordBoardId, + ); + + const recordIndexAllRowIdsState = useRecoilComponentCallbackStateV2( + recordIndexAllRowIdsComponentState, + recordBoardId, + ); + + const recordIndexRowIdsByGroupFamilyState = useRecoilComponentCallbackStateV2( + recordIndexRowIdsByGroupComponentFamilyState, + recordBoardId, + ); + + const setRecordIds = useRecoilCallback( + ({ set, snapshot }) => + (records: ObjectRecord[]) => { + const existingAllRowIds = getSnapshotValue( + snapshot, + recordIndexAllRowIdsState, + ); + + const recordGroupIds = getSnapshotValue( + snapshot, + visibleRecordGroupIdsSelector, + ); + + for (const recordGroupId of recordGroupIds) { + const recordGroup = getSnapshotValue( + snapshot, + recordGroupDefinitionFamilyState(recordGroupId), + ); + + const existingRecordGroupRowIds = getSnapshotValue( + snapshot, + recordIndexRowIdsByGroupFamilyState(recordGroupId), + ); + + const recordGroupFieldMetadata = getSnapshotValue( + snapshot, + recordGroupFieldMetadataState, + ); + + if (!isDefined(recordGroupFieldMetadata)) { + return; + } + + const recordGroupRowIds = records + .filter( + (record) => + record[recordGroupFieldMetadata.name] === recordGroup?.value, + ) + .sort(sortRecordsByPosition) + .map((record) => record.id); + + if (!isDeeplyEqual(existingRecordGroupRowIds, recordGroupRowIds)) { + set( + recordIndexRowIdsByGroupFamilyState(recordGroupId), + recordGroupRowIds, + ); + } + } + + const allRowIds: string[] = []; + + for (const recordGroupId of recordGroupIds) { + const tableRowIdsByGroup = getSnapshotValue( + snapshot, + recordIndexRowIdsByGroupFamilyState(recordGroupId), + ); + + allRowIds.push(...tableRowIdsByGroup); + } + + if (!isDeeplyEqual(existingAllRowIds, allRowIds)) { + set(recordIndexAllRowIdsState, allRowIds); + } + }, + [ + visibleRecordGroupIdsSelector, + recordIndexRowIdsByGroupFamilyState, + recordGroupFieldMetadataState, + recordIndexAllRowIdsState, + ], + ); + + return { + setRecordIds, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/useSetRecordIdsForColumn.ts b/packages/twenty-front/src/modules/object-record/record-board/hooks/useSetRecordIdsForColumn.ts new file mode 100644 index 000000000000..3ba7c3ee78a4 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/useSetRecordIdsForColumn.ts @@ -0,0 +1,109 @@ +import { useRecoilCallback } from 'recoil'; + +import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; +import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState'; +import { recordGroupIdsComponentState } from '@/object-record/record-group/states/recordGroupIdsComponentState'; +import { recordIndexAllRowIdsComponentState } from '@/object-record/record-index/states/recordIndexAllRowIdsComponentState'; +import { recordIndexRowIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRowIdsByGroupComponentFamilyState'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { sortRecordsByPosition } from '@/object-record/utils/sortRecordsByPosition'; +import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; +import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; +import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; +import { isDefined } from '~/utils/isDefined'; + +export const useSetRecordIdsForColumn = (recordBoardId?: string) => { + const recordGroupIdsState = useRecoilComponentCallbackStateV2( + recordGroupIdsComponentState, + recordBoardId, + ); + + const recordGroupFieldMetadataState = useRecoilComponentCallbackStateV2( + recordGroupFieldMetadataComponentState, + recordBoardId, + ); + + const recordIndexAllRowIdsState = useRecoilComponentCallbackStateV2( + recordIndexAllRowIdsComponentState, + recordBoardId, + ); + + const recordIndexRowIdsByGroupFamilyState = useRecoilComponentCallbackStateV2( + recordIndexRowIdsByGroupComponentFamilyState, + recordBoardId, + ); + + const setRecordIdsForColumn = useRecoilCallback( + ({ set, snapshot }) => + (currentRecordGroupId: string, records: ObjectRecord[]) => { + const existingAllRowIds = getSnapshotValue( + snapshot, + recordIndexAllRowIdsState, + ); + + const recordGroupIds = getSnapshotValue(snapshot, recordGroupIdsState); + + const recordGroup = getSnapshotValue( + snapshot, + recordGroupDefinitionFamilyState(currentRecordGroupId), + ); + + const existingRecordGroupRowIds = getSnapshotValue( + snapshot, + recordIndexRowIdsByGroupFamilyState(currentRecordGroupId), + ); + + const recordGroupFieldMetadata = getSnapshotValue( + snapshot, + recordGroupFieldMetadataState, + ); + + if (!isDefined(recordGroupFieldMetadata)) { + return; + } + + const recordGroupRowIds = records + .filter( + (record) => + record[recordGroupFieldMetadata.name] === recordGroup?.value, + ) + .sort(sortRecordsByPosition) + .map((record) => record.id); + + if (!isDeeplyEqual(existingRecordGroupRowIds, recordGroupRowIds)) { + set( + recordIndexRowIdsByGroupFamilyState(currentRecordGroupId), + recordGroupRowIds, + ); + } + + const allRowIds: string[] = []; + + for (const recordGroupId of recordGroupIds) { + const tableRowIdsByGroup = + recordGroupId !== currentRecordGroupId + ? getSnapshotValue( + snapshot, + recordIndexRowIdsByGroupFamilyState(recordGroupId), + ) + : recordGroupRowIds; + + allRowIds.push(...tableRowIdsByGroup); + } + + if (!isDeeplyEqual(existingAllRowIds, allRowIds)) { + set(recordIndexAllRowIdsState, allRowIds); + } + }, + [ + recordGroupIdsState, + recordIndexRowIdsByGroupFamilyState, + recordGroupFieldMetadataState, + recordIndexAllRowIdsState, + ], + ); + + return { + setRecordIdsForColumn, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx index 92d897a614ae..a456c89a3d9f 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx @@ -3,9 +3,11 @@ import { recordIndexActionMenuDropdownPositionComponentState } from '@/action-me import { getActionMenuDropdownIdFromActionMenuId } from '@/action-menu/utils/getActionMenuDropdownIdFromActionMenuId'; import { getActionMenuIdFromRecordIndexId } from '@/action-menu/utils/getActionMenuIdFromRecordIndexId'; import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext'; -import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; import { RecordBoardCardContext } from '@/object-record/record-board/record-board-card/contexts/RecordBoardCardContext'; import { RecordBoardScopeInternalContext } from '@/object-record/record-board/scopes/scope-internal-context/RecordBoardScopeInternalContext'; +import { isRecordBoardCardSelectedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardSelectedComponentFamilyState'; +import { isRecordBoardCompactModeActiveComponentState } from '@/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState'; +import { recordBoardVisibleFieldDefinitionsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardVisibleFieldDefinitionsComponentSelector'; import { FieldContext, RecordUpdateHook, @@ -22,11 +24,13 @@ import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { TextInput } from '@/ui/input/components/TextInput'; import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId'; import { RecordBoardScrollWrapperContext } from '@/ui/utilities/scroll/contexts/ScrollWrapperContexts'; +import { useRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyStateV2'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState'; import styled from '@emotion/styled'; import { ReactNode, useContext, useState } from 'react'; import { InView, useInView } from 'react-intersection-observer'; -import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; import { AnimatedEaseInOut, AvatarChipVariant, @@ -157,28 +161,31 @@ export const RecordBoardCard = ({ onCreateSuccess?: () => void; position?: 'first' | 'last'; }) => { + const { recordId } = useContext(RecordBoardCardContext); + const [newLabelValue, setNewLabelValue] = useState(''); + const { handleBlur, handleInputEnter } = useAddNewCard(); - const { recordId } = useContext(RecordBoardCardContext); + const { updateOneRecord, objectMetadataItem } = useContext(RecordBoardContext); - const { - isCompactModeActiveState, - isRecordBoardCardSelectedFamilyState, - visibleFieldDefinitionsState, - } = useRecordBoardStates(); - const isCompactModeActive = useRecoilValue(isCompactModeActiveState); - const [isCardExpanded, setIsCardExpanded] = useState(false); - - const [isCurrentCardSelected, setIsCurrentCardSelected] = useRecoilState( - isRecordBoardCardSelectedFamilyState(recordId), + const visibleFieldDefinitions = useRecoilComponentValueV2( + recordBoardVisibleFieldDefinitionsComponentSelector, ); - const visibleFieldDefinitions = useRecoilValue( - visibleFieldDefinitionsState(), + const isCompactModeActive = useRecoilComponentValueV2( + isRecordBoardCompactModeActiveComponentState, ); + const [isCardExpanded, setIsCardExpanded] = useState(false); + + const [isCurrentCardSelected, setIsCurrentCardSelected] = + useRecoilComponentFamilyStateV2( + isRecordBoardCardSelectedComponentFamilyState, + recordId, + ); + const record = useRecoilValue(recordStoreFamilyState(recordId)); const recordBoardId = useAvailableScopeIdOrThrow( 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 26072d4a7e06..1d850a8507fe 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 @@ -1,10 +1,12 @@ import styled from '@emotion/styled'; import { Droppable } from '@hello-pangea/dnd'; -import { useRecoilValue } from 'recoil'; -import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; import { RecordBoardColumnCardsContainer } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnCardsContainer'; import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext'; +import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; +import { recordIndexRowIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRowIdsByGroupComponentFamilyState'; +import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; +import { useRecoilValue } from 'recoil'; const StyledColumn = styled.div` background-color: ${({ theme }) => theme.background.primary}; @@ -25,27 +27,26 @@ type RecordBoardColumnProps = { export const RecordBoardColumn = ({ recordBoardColumnId, }: RecordBoardColumnProps) => { - const { columnsFamilySelector, recordIdsByColumnIdFamilyState } = - useRecordBoardStates(); - const columnDefinition = useRecoilValue( - columnsFamilySelector(recordBoardColumnId), + const recordGroupDefinition = useRecoilValue( + recordGroupDefinitionFamilyState(recordBoardColumnId), ); - const recordIds = useRecoilValue( - recordIdsByColumnIdFamilyState(recordBoardColumnId), + const recordRowIdsByGroup = useRecoilComponentFamilyValueV2( + recordIndexRowIdsByGroupComponentFamilyState, + recordBoardColumnId, ); - if (!columnDefinition) { + if (!recordGroupDefinition) { return null; } return ( @@ -53,7 +54,7 @@ export const RecordBoardColumn = ({ )} diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardsContainer.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardsContainer.tsx index d3e540902b55..8cdc5c32ffac 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardsContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardsContainer.tsx @@ -5,7 +5,6 @@ import { useRecoilValue } from 'recoil'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext'; -import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; import { RecordBoardColumnCardContainerSkeletonLoader } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnCardContainerSkeletonLoader'; import { RecordBoardColumnCardsMemo } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnCardsMemo'; import { RecordBoardColumnFetchMoreLoader } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnFetchMoreLoader'; @@ -15,7 +14,10 @@ import { RecordBoardColumnNewRecordButton } from '@/object-record/record-board/r import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext'; import { useIsOpportunitiesCompanyFieldDisabled } from '@/object-record/record-board/record-board-column/hooks/useIsOpportunitiesCompanyFieldDisabled'; import { getNumberOfCardsPerColumnForSkeletonLoading } from '@/object-record/record-board/record-board-column/utils/getNumberOfCardsPerColumnForSkeletonLoading'; +import { isRecordBoardCompactModeActiveComponentState } from '@/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState'; +import { recordBoardVisibleFieldDefinitionsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardVisibleFieldDefinitionsComponentSelector'; import { isRecordIndexBoardColumnLoadingFamilyState } from '@/object-record/states/isRecordBoardColumnLoadingFamilyState'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; const StyledColumnCardsContainer = styled.div` display: flex; @@ -56,16 +58,16 @@ export const RecordBoardColumnCardsContainer = ({ isRecordIndexBoardColumnLoadingFamilyState(columnId), ); - const { isCompactModeActiveState, visibleFieldDefinitionsState } = - useRecordBoardStates(); - - const visibleFieldDefinitions = useRecoilValue( - visibleFieldDefinitionsState(), + const visibleFieldDefinitions = useRecoilComponentValueV2( + recordBoardVisibleFieldDefinitionsComponentSelector, ); const numberOfFields = visibleFieldDefinitions.length; - const isCompactModeActive = useRecoilValue(isCompactModeActiveState); + const isCompactModeActive = useRecoilComponentValueV2( + isRecordBoardCompactModeActiveComponentState, + ); + const { isOpportunitiesCompanyFieldDisabled } = useIsOpportunitiesCompanyFieldDisabled(); diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnDropdownMenu.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnDropdownMenu.tsx index ed6045033fec..bfbfb7e9d0e1 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnDropdownMenu.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnDropdownMenu.tsx @@ -5,7 +5,6 @@ import { useRecordGroupActions } from '@/object-record/record-group/hooks/useRec import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; -import { ViewType } from '@/views/types/ViewType'; import { MenuItem } from 'twenty-ui'; const StyledMenuContainer = styled.div` @@ -26,9 +25,7 @@ export const RecordBoardColumnDropdownMenu = ({ }: RecordBoardColumnDropdownMenuProps) => { const boardColumnMenuRef = useRef(null); - const recordGroupActions = useRecordGroupActions({ - viewType: ViewType.Kanban, - }); + const recordGroupActions = useRecordGroupActions(); const closeMenu = useCallback(() => { onClose(); diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnFetchMoreLoader.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnFetchMoreLoader.tsx index 7a610c3dda95..1143aaf5e953 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnFetchMoreLoader.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnFetchMoreLoader.tsx @@ -1,11 +1,13 @@ +import styled from '@emotion/styled'; import { useContext, useEffect } from 'react'; import { useInView } from 'react-intersection-observer'; -import styled from '@emotion/styled'; -import { useRecoilValue, useSetRecoilState } from 'recoil'; +import { useRecoilValue } from 'recoil'; import { GRAY_SCALE } from 'twenty-ui'; -import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext'; +import { isRecordBoardFetchingRecordsByColumnFamilyState } from '@/object-record/record-board/states/isRecordBoardFetchingRecordsByColumnFamilyState'; +import { recordBoardShouldFetchMoreInColumnComponentFamilyState } from '@/object-record/record-board/states/recordBoardShouldFetchMoreInColumnComponentFamilyState'; +import { useSetRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentFamilyStateV2'; const StyledText = styled.div` align-items: center; @@ -19,15 +21,14 @@ const StyledText = styled.div` export const RecordBoardColumnFetchMoreLoader = () => { const { columnDefinition } = useContext(RecordBoardColumnContext); - const { shouldFetchMoreInColumnFamilyState, isFetchingRecordsByColumnState } = - useRecordBoardStates(); const isFetchingRecord = useRecoilValue( - isFetchingRecordsByColumnState({ columnId: columnDefinition.id }), + isRecordBoardFetchingRecordsByColumnFamilyState(columnDefinition.id), ); - const setShouldFetchMore = useSetRecoilState( - shouldFetchMoreInColumnFamilyState(columnDefinition.id), + const setShouldFetchMore = useSetRecoilComponentFamilyStateV2( + recordBoardShouldFetchMoreInColumnComponentFamilyState, + columnDefinition.id, ); const { ref, inView } = useInView(); diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderWrapper.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderWrapper.tsx index 1386b494d6ef..68de7adb566d 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderWrapper.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderWrapper.tsx @@ -1,8 +1,10 @@ import { isDefined } from 'twenty-ui'; -import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; import { RecordBoardColumnHeader } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeader'; import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext'; +import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; +import { recordIndexRowIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRowIdsByGroupComponentFamilyState'; +import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; import { useRecoilValue } from 'recoil'; type RecordBoardColumnHeaderWrapperProps = { @@ -12,14 +14,16 @@ type RecordBoardColumnHeaderWrapperProps = { export const RecordBoardColumnHeaderWrapper = ({ columnId, }: RecordBoardColumnHeaderWrapperProps) => { - const { columnsFamilySelector, recordIdsByColumnIdFamilyState } = - useRecordBoardStates(); - - const columnDefinition = useRecoilValue(columnsFamilySelector(columnId)); + const recordGroupDefinition = useRecoilValue( + recordGroupDefinitionFamilyState(columnId), + ); - const recordIds = useRecoilValue(recordIdsByColumnIdFamilyState(columnId)); + const recordRowIdsByGroup = useRecoilComponentFamilyValueV2( + recordIndexRowIdsByGroupComponentFamilyState, + columnId, + ); - if (!isDefined(columnDefinition)) { + if (!isDefined(recordGroupDefinition)) { return null; } @@ -27,9 +31,9 @@ export const RecordBoardColumnHeaderWrapper = ({ diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useColumnNewCardActions.ts b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useColumnNewCardActions.ts index cb087c0813a8..3926ceddbe12 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useColumnNewCardActions.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useColumnNewCardActions.ts @@ -1,12 +1,12 @@ -import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; import { useAddNewCard } from '@/object-record/record-board/record-board-column/hooks/useAddNewCard'; -import { useRecoilValue } from 'recoil'; +import { recordBoardVisibleFieldDefinitionsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardVisibleFieldDefinitionsComponentSelector'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; export const useColumnNewCardActions = (columnId: string) => { - const { visibleFieldDefinitionsState } = useRecordBoardStates(); - const visibleFieldDefinitions = useRecoilValue( - visibleFieldDefinitionsState(), + const visibleFieldDefinitions = useRecoilComponentValueV2( + recordBoardVisibleFieldDefinitionsComponentSelector, ); + const labelIdentifierField = visibleFieldDefinitions.find( (field) => field.isLabelIdentifier, ); diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext.ts b/packages/twenty-front/src/modules/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext.ts new file mode 100644 index 000000000000..1d83aab21078 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext.ts @@ -0,0 +1,4 @@ +import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext'; + +export const RecordBoardComponentInstanceContext = + createComponentInstanceContext(); diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/isRecordBoardCardSelectedComponentFamilyState.ts b/packages/twenty-front/src/modules/object-record/record-board/states/isRecordBoardCardSelectedComponentFamilyState.ts index 8cb7c99f4959..3b446676de7e 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/states/isRecordBoardCardSelectedComponentFamilyState.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/states/isRecordBoardCardSelectedComponentFamilyState.ts @@ -1,7 +1,9 @@ -import { createComponentFamilyState } from '@/ui/utilities/state/component-state/utils/createComponentFamilyState'; +import { RecordBoardComponentInstanceContext } from '@/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext'; +import { createComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentFamilyStateV2'; export const isRecordBoardCardSelectedComponentFamilyState = - createComponentFamilyState({ + createComponentFamilyStateV2({ key: 'isRecordBoardCardSelectedComponentFamilyState', defaultValue: false, + componentInstanceContext: RecordBoardComponentInstanceContext, }); diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState.ts b/packages/twenty-front/src/modules/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState.ts index 68741ee81a90..bde53d616ef1 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState.ts @@ -1,7 +1,9 @@ -import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; +import { RecordBoardComponentInstanceContext } from '@/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; export const isRecordBoardCompactModeActiveComponentState = - createComponentState({ + createComponentStateV2({ key: 'isRecordBoardCompactModeActiveComponentState', defaultValue: false, + componentInstanceContext: RecordBoardComponentInstanceContext, }); diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/isRecordBoardFetchingRecordsByColumnFamilyState.ts b/packages/twenty-front/src/modules/object-record/record-board/states/isRecordBoardFetchingRecordsByColumnFamilyState.ts index dfaaffc0ec23..8ee4affb3cc5 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/states/isRecordBoardFetchingRecordsByColumnFamilyState.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/states/isRecordBoardFetchingRecordsByColumnFamilyState.ts @@ -1,7 +1,10 @@ -import { createComponentFamilyState } from '@/ui/utilities/state/component-state/utils/createComponentFamilyState'; +import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; +import { atomFamily } from 'recoil'; -export const isRecordBoardFetchingRecordsByColumnFamilyState = - createComponentFamilyState({ - key: 'isRecordBoardFetchingRecordsByColumnFamilyState', - defaultValue: false, - }); +export const isRecordBoardFetchingRecordsByColumnFamilyState = atomFamily< + boolean, + RecordGroupDefinition['id'] +>({ + key: 'isRecordBoardFetchingRecordsByColumnFamilyState', + default: false, +}); diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/isRecordBoardFetchingRecordsComponentState.ts b/packages/twenty-front/src/modules/object-record/record-board/states/isRecordBoardFetchingRecordsComponentState.ts deleted file mode 100644 index c76a8777e328..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-board/states/isRecordBoardFetchingRecordsComponentState.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; - -export const isRecordBoardFetchingRecordsComponentState = - createComponentState({ - key: 'isRecordBoardFetchingRecordsComponentState', - defaultValue: false, - }); diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardColumnIdsComponentState.ts b/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardColumnIdsComponentState.ts deleted file mode 100644 index 3ae094376c22..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardColumnIdsComponentState.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; - -export const recordBoardColumnIdsComponentState = createComponentState< - string[] ->({ - key: 'recordBoardColumnIdsComponentState', - defaultValue: [], -}); 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 deleted file mode 100644 index 1530820d803f..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardColumnsComponentFamilyState.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; -import { createComponentFamilyState } from '@/ui/utilities/state/component-state/utils/createComponentFamilyState'; - -export const recordBoardColumnsComponentFamilyState = - createComponentFamilyState({ - key: 'recordBoardColumnsComponentFamilyState', - defaultValue: undefined, - }); diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardFieldDefinitionsComponentState.ts b/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardFieldDefinitionsComponentState.ts index e8fb862be662..36696c000453 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardFieldDefinitionsComponentState.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardFieldDefinitionsComponentState.ts @@ -1,10 +1,12 @@ +import { RecordBoardComponentInstanceContext } from '@/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext'; import { RecordBoardFieldDefinition } from '@/object-record/record-board/types/RecordBoardFieldDefinition'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; -import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; -export const recordBoardFieldDefinitionsComponentState = createComponentState< +export const recordBoardFieldDefinitionsComponentState = createComponentStateV2< RecordBoardFieldDefinition[] >({ key: 'recordBoardFieldDefinitionsComponentState', defaultValue: [], + componentInstanceContext: RecordBoardComponentInstanceContext, }); diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardFiltersComponentState.ts b/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardFiltersComponentState.ts deleted file mode 100644 index 7d493b349e5b..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardFiltersComponentState.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; -import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; - -export const recordBoardFiltersComponentState = createComponentState({ - key: 'recordBoardFiltersComponentState', - defaultValue: [], -}); diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardKanbanFieldMetadataNameComponentState.ts b/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardKanbanFieldMetadataNameComponentState.ts deleted file mode 100644 index 26490c9298b3..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardKanbanFieldMetadataNameComponentState.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; - -export const recordBoardKanbanFieldMetadataNameComponentState = - createComponentState({ - key: 'recordBoardKanbanFieldMetadataNameComponentState', - defaultValue: undefined, - }); diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardRecordIdsByColumnIdComponentFamilyState.ts b/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardRecordIdsByColumnIdComponentFamilyState.ts deleted file mode 100644 index 9cb2f0df334a..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardRecordIdsByColumnIdComponentFamilyState.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { createComponentFamilyState } from '@/ui/utilities/state/component-state/utils/createComponentFamilyState'; - -export const recordBoardRecordIdsByColumnIdComponentFamilyState = - createComponentFamilyState({ - key: 'recordBoardRecordIdsByColumnIdComponentFamilyState', - defaultValue: [], - }); diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardShouldFetchMoreInColumnComponentFamilyState.ts b/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardShouldFetchMoreInColumnComponentFamilyState.ts index 01dd190990ef..2dec1d6b87b2 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardShouldFetchMoreInColumnComponentFamilyState.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardShouldFetchMoreInColumnComponentFamilyState.ts @@ -1,7 +1,9 @@ -import { createComponentFamilyState } from '@/ui/utilities/state/component-state/utils/createComponentFamilyState'; +import { RecordBoardComponentInstanceContext } from '@/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext'; +import { createComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentFamilyStateV2'; export const recordBoardShouldFetchMoreInColumnComponentFamilyState = - createComponentFamilyState({ - key: 'onRecordBoardFetchMoreIrecordBoardShouldFetchMoreInColumnComponentFamilyStatesVisibleComponentFamilyState', + createComponentFamilyStateV2({ + key: 'recordBoardShouldFetchMoreInColumnComponentFamilyState', defaultValue: false, + componentInstanceContext: RecordBoardComponentInstanceContext, }); diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardSortsComponentState.ts b/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardSortsComponentState.ts deleted file mode 100644 index d2aa0923f335..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardSortsComponentState.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Sort } from '@/object-record/object-sort-dropdown/types/Sort'; -import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; - -export const recordBoardSortsComponentState = createComponentState({ - key: 'recordBoardSortsComponentState', - defaultValue: [], -}); diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/selectors/recordBoardAllRecordIdsComponentSelector.ts b/packages/twenty-front/src/modules/object-record/record-board/states/selectors/recordBoardAllRecordIdsComponentSelector.ts deleted file mode 100644 index 154da9313fa2..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-board/states/selectors/recordBoardAllRecordIdsComponentSelector.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { recordBoardColumnIdsComponentState } from '@/object-record/record-board/states/recordBoardColumnIdsComponentState'; -import { recordBoardRecordIdsByColumnIdComponentFamilyState } from '@/object-record/record-board/states/recordBoardRecordIdsByColumnIdComponentFamilyState'; -import { createComponentReadOnlySelector } from '@/ui/utilities/state/component-state/utils/createComponentReadOnlySelector'; - -export const recordBoardAllRecordIdsComponentSelector = - createComponentReadOnlySelector({ - key: 'recordBoardAllRecordIdsComponentSelector', - get: - ({ scopeId }) => - ({ get }) => { - const columnIds = get(recordBoardColumnIdsComponentState({ scopeId })); - - const recordIdsByColumn = columnIds.map((columnId) => - get( - recordBoardRecordIdsByColumnIdComponentFamilyState({ - scopeId, - familyKey: columnId, - }), - ), - ); - - const recordIds = recordIdsByColumn.flat(); - - return recordIds; - }, - }); 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 deleted file mode 100644 index fefff8451060..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-board/states/selectors/recordBoardColumnsComponentFamilySelector.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { recordBoardColumnsComponentFamilyState } from '@/object-record/record-board/states/recordBoardColumnsComponentFamilyState'; -import { createComponentFamilySelector } from '@/ui/utilities/state/component-state/utils/createComponentFamilySelector'; -import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; - -export const recordBoardColumnsComponentFamilySelector = - createComponentFamilySelector({ - key: 'recordBoardColumnsComponentFamilySelector', - get: - ({ - scopeId, - familyKey: columnId, - }: { - scopeId: string; - familyKey: string; - }) => - ({ get }) => { - return get( - recordBoardColumnsComponentFamilyState({ - scopeId, - familyKey: columnId, - }), - ); - }, - set: - ({ - scopeId, - familyKey: columnId, - }: { - scopeId: string; - familyKey: string; - }) => - ({ set }, newColumn) => { - set( - recordBoardColumnsComponentFamilyState({ - scopeId, - familyKey: columnId, - }), - newColumn, - ); - }, - }); diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/selectors/recordBoardSelectedRecordIdsComponentSelector.ts b/packages/twenty-front/src/modules/object-record/record-board/states/selectors/recordBoardSelectedRecordIdsComponentSelector.ts index ce2b1e4cac4d..849d45735982 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/states/selectors/recordBoardSelectedRecordIdsComponentSelector.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/states/selectors/recordBoardSelectedRecordIdsComponentSelector.ts @@ -1,32 +1,24 @@ +import { RecordBoardComponentInstanceContext } from '@/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext'; import { isRecordBoardCardSelectedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardSelectedComponentFamilyState'; -import { recordBoardColumnIdsComponentState } from '@/object-record/record-board/states/recordBoardColumnIdsComponentState'; -import { recordBoardRecordIdsByColumnIdComponentFamilyState } from '@/object-record/record-board/states/recordBoardRecordIdsByColumnIdComponentFamilyState'; -import { createComponentReadOnlySelector } from '@/ui/utilities/state/component-state/utils/createComponentReadOnlySelector'; +import { recordIndexAllRowIdsComponentState } from '@/object-record/record-index/states/recordIndexAllRowIdsComponentState'; +import { createComponentSelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentSelectorV2'; export const recordBoardSelectedRecordIdsComponentSelector = - createComponentReadOnlySelector({ + createComponentSelectorV2({ key: 'recordBoardSelectedRecordIdsSelector', + componentInstanceContext: RecordBoardComponentInstanceContext, get: - ({ scopeId }) => + ({ instanceId }) => ({ get }) => { - const columnIds = get(recordBoardColumnIdsComponentState({ scopeId })); - - const recordIdsByColumn = columnIds.map((columnId) => - get( - recordBoardRecordIdsByColumnIdComponentFamilyState({ - scopeId, - familyKey: columnId, - }), - ), + const allRowIds = get( + recordIndexAllRowIdsComponentState.atomFamily({ instanceId }), ); - const recordIds = recordIdsByColumn.flat(); - - return recordIds.filter( + return allRowIds.filter( (recordId) => get( - isRecordBoardCardSelectedComponentFamilyState({ - scopeId, + isRecordBoardCardSelectedComponentFamilyState.atomFamily({ + instanceId, familyKey: recordId, }), ) === true, diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/selectors/recordBoardShouldFetchMoreComponentFamilySelector.ts b/packages/twenty-front/src/modules/object-record/record-board/states/selectors/recordBoardShouldFetchMoreComponentFamilySelector.ts deleted file mode 100644 index 225ffafac6a9..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-board/states/selectors/recordBoardShouldFetchMoreComponentFamilySelector.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { recordBoardColumnIdsComponentState } from '@/object-record/record-board/states/recordBoardColumnIdsComponentState'; -import { recordBoardShouldFetchMoreInColumnComponentFamilyState } from '@/object-record/record-board/states/recordBoardShouldFetchMoreInColumnComponentFamilyState'; -import { createComponentReadOnlySelector } from '@/ui/utilities/state/component-state/utils/createComponentReadOnlySelector'; - -export const recordBoardShouldFetchMoreComponentSelector = - createComponentReadOnlySelector({ - key: 'recordBoardShouldFetchMoreComponentSelector', - get: - ({ scopeId }: { scopeId: string }) => - ({ get }) => { - const columnIds = get( - recordBoardColumnIdsComponentState({ - scopeId, - }), - ); - - const shouldFetchMoreInColumns = columnIds.map((columnId) => { - return get( - recordBoardShouldFetchMoreInColumnComponentFamilyState({ - scopeId, - familyKey: columnId, - }), - ); - }); - - return shouldFetchMoreInColumns.some(Boolean); - }, - }); diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/selectors/recordBoardVisibleFieldDefinitionsComponentSelector.ts b/packages/twenty-front/src/modules/object-record/record-board/states/selectors/recordBoardVisibleFieldDefinitionsComponentSelector.ts index 4b2732eb36d8..62ae69407bf0 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/states/selectors/recordBoardVisibleFieldDefinitionsComponentSelector.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/states/selectors/recordBoardVisibleFieldDefinitionsComponentSelector.ts @@ -1,13 +1,17 @@ +import { RecordBoardComponentInstanceContext } from '@/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext'; import { recordBoardFieldDefinitionsComponentState } from '@/object-record/record-board/states/recordBoardFieldDefinitionsComponentState'; -import { createComponentReadOnlySelector } from '@/ui/utilities/state/component-state/utils/createComponentReadOnlySelector'; +import { createComponentSelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentSelectorV2'; export const recordBoardVisibleFieldDefinitionsComponentSelector = - createComponentReadOnlySelector({ + createComponentSelectorV2({ key: 'recordBoardVisibleFieldDefinitionsComponentSelector', get: - ({ scopeId }) => + ({ instanceId }) => ({ get }) => - get(recordBoardFieldDefinitionsComponentState({ scopeId })) + get( + recordBoardFieldDefinitionsComponentState.atomFamily({ instanceId }), + ) .filter((field) => field.isVisible) .sort((a, b) => a.position - b.position), + componentInstanceContext: RecordBoardComponentInstanceContext, }); diff --git a/packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupMenuItemDraggable.tsx b/packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupMenuItemDraggable.tsx index 70bd645af243..3a5d668ad005 100644 --- a/packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupMenuItemDraggable.tsx +++ b/packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupMenuItemDraggable.tsx @@ -1,31 +1,45 @@ import { IconEye, IconEyeOff, MenuItemDraggable, Tag } from 'twenty-ui'; +import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; import { RecordGroupDefinition, RecordGroupDefinitionType, } from '@/object-record/record-group/types/RecordGroupDefinition'; +import { useRecoilValue } from 'recoil'; import { isDefined } from '~/utils/isDefined'; type RecordGroupMenuItemDraggableProps = { - recordGroup: RecordGroupDefinition; + recordGroupId: string; showDragGrip?: boolean; isDraggable?: boolean; - onVisibilityChange: (viewGroup: RecordGroupDefinition) => void; + onVisibilityChange: (recordGroup: RecordGroupDefinition) => void; }; export const RecordGroupMenuItemDraggable = ({ - recordGroup, + recordGroupId, showDragGrip, isDraggable, onVisibilityChange, }: RecordGroupMenuItemDraggableProps) => { + const recordGroup = useRecoilValue( + recordGroupDefinitionFamilyState(recordGroupId), + ); + + if (!isDefined(recordGroup)) { + return null; + } + const isNoValue = recordGroup.type === RecordGroupDefinitionType.NoValue; const getIconButtons = (recordGroup: RecordGroupDefinition) => { const iconButtons = [ { Icon: recordGroup.isVisible ? IconEyeOff : IconEye, - onClick: () => onVisibilityChange(recordGroup), + onClick: () => + onVisibilityChange({ + ...recordGroup, + isVisible: !recordGroup.isVisible, + }), }, ].filter(isDefined); diff --git a/packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupsVisibilityDropdownSection.tsx b/packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupsVisibilityDropdownSection.tsx index e43afd1eeff1..e655a402d017 100644 --- a/packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupsVisibilityDropdownSection.tsx +++ b/packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupsVisibilityDropdownSection.tsx @@ -13,17 +13,17 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop import { StyledDropdownMenuSubheader } from '@/ui/layout/dropdown/components/StyledDropdownMenuSubheader'; type RecordGroupsVisibilityDropdownSectionProps = { - recordGroups: RecordGroupDefinition[]; + recordGroupIds: string[]; isDraggable: boolean; onDragEnd?: OnDragEndResponder; - onVisibilityChange: (viewGroup: RecordGroupDefinition) => void; + onVisibilityChange: (recordGroup: RecordGroupDefinition) => void; title: string; showSubheader?: boolean; showDragGrip: boolean; }; export const RecordGroupsVisibilityDropdownSection = ({ - recordGroups, + recordGroupIds, isDraggable, onDragEnd, onVisibilityChange, @@ -43,12 +43,13 @@ export const RecordGroupsVisibilityDropdownSection = ({ {title} )} - {!!recordGroups.length && ( + {recordGroupIds.length > 0 && ( <> {!isDraggable ? ( - recordGroups.map((recordGroup) => ( + recordGroupIds.map((recordGroupId) => ( - {recordGroups.map((recordGroup, index) => ( + {recordGroupIds.map((recordGroupId, index) => ( { +export const useCurrentRecordGroupDefinition = () => { const context = useContext(RecordGroupContext); - const hasRecordGroups = useRecoilComponentValueV2( - hasRecordGroupDefinitionsComponentSelector, - recordTableId, + const recordGroupDefinition = useRecoilValue( + recordGroupDefinitionFamilyState(context?.recordGroupId), ); - const recordGroupDefinitions = useRecoilComponentValueV2( - recordGroupDefinitionsComponentState, - recordTableId, - ); - - const recordGroupDefinition = useMemo(() => { - if (!hasRecordGroups) { - return undefined; - } - - if (!context) { - throw new Error( - 'useCurrentRecordGroupDefinition must be used within a RecordGroupContextProvider.', - ); - } - - return recordGroupDefinitions.find( - ({ id }) => id === context.recordGroupId, - ); - }, [context, hasRecordGroups, recordGroupDefinitions]); - return recordGroupDefinition; }; 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 index b99af28b6957..b1bc9829d768 100644 --- 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 @@ -2,24 +2,18 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata 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 { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState'; import { RecordGroupAction } from '@/object-record/record-group/types/RecordGroupActions'; import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; -import { ViewType } from '@/views/types/ViewType'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useCallback, useContext, useMemo } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { useSetRecoilState } from 'recoil'; import { IconEyeOff, IconSettings, isDefined } from 'twenty-ui'; -type UseRecordGroupActionsParams = { - viewType: ViewType; -}; - -export const useRecordGroupActions = ({ - viewType, -}: UseRecordGroupActionsParams) => { +export const useRecordGroupActions = () => { const navigate = useNavigate(); const location = useLocation(); @@ -35,14 +29,13 @@ export const useRecordGroupActions = ({ objectNameSingular, }); - const { viewGroupFieldMetadataItem } = useRecordGroups({ - objectNameSingular, - }); + const recordGroupFieldMetadata = useRecoilComponentValueV2( + recordGroupFieldMetadataComponentState, + ); const { handleVisibilityChange: handleRecordGroupVisibilityChange } = useRecordGroupVisibility({ viewBarId: recordIndexId, - viewType, }); const setNavigationMemorizedUrl = useSetRecoilState( @@ -52,11 +45,11 @@ export const useRecordGroupActions = ({ const navigateToSelectSettings = useCallback(() => { setNavigationMemorizedUrl(location.pathname + location.search); - if (!isDefined(viewGroupFieldMetadataItem)) { - throw new Error('viewGroupFieldMetadataItem is not a non-empty string'); + if (!isDefined(recordGroupFieldMetadata)) { + throw new Error('recordGroupFieldMetadata is not a non-empty string'); } - const settingsPath = `/settings/objects/${getObjectSlug(objectMetadataItem)}/${getFieldSlug(viewGroupFieldMetadataItem)}`; + const settingsPath = `/settings/objects/${getObjectSlug(objectMetadataItem)}/${getFieldSlug(recordGroupFieldMetadata)}`; navigate(settingsPath); }, [ @@ -65,7 +58,7 @@ export const useRecordGroupActions = ({ location.search, navigate, objectMetadataItem, - viewGroupFieldMetadataItem, + recordGroupFieldMetadata, ]); const recordGroupActions: RecordGroupAction[] = useMemo( 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 index b0c738a0fbc5..9b1c038e1a38 100644 --- 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 @@ -1,59 +1,89 @@ 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 { useSetRecordGroup } from '@/object-record/record-group/hooks/useSetRecordGroup'; +import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; +import { visibleRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector'; +import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; +import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; +import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; import { useSaveCurrentViewGroups } from '@/views/hooks/useSaveCurrentViewGroups'; import { mapRecordGroupDefinitionsToViewGroups } from '@/views/utils/mapRecordGroupDefinitionsToViewGroups'; +import { useRecoilCallback } from 'recoil'; import { moveArrayItem } from '~/utils/array/moveArrayItem'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; +import { isDefined } from '~/utils/isDefined'; type UseRecordGroupHandlersParams = { - objectNameSingular: string; viewBarId: string; }; export const useRecordGroupReorder = ({ - objectNameSingular, viewBarId, }: UseRecordGroupHandlersParams) => { - const setRecordGroupDefinitions = useSetRecoilComponentStateV2( - recordGroupDefinitionsComponentState, - ); + const setRecordGroup = useSetRecordGroup(viewBarId); - const { visibleRecordGroups } = useRecordGroups({ - objectNameSingular: objectNameSingular, - }); + const visibleRecordGroupIdsSelector = useRecoilComponentCallbackStateV2( + visibleRecordGroupIdsComponentSelector, + ); const { saveViewGroups } = useSaveCurrentViewGroups(viewBarId); - const handleOrderChange: OnDragEndResponder = useCallback( - (result) => { - if (!result.destination) { - return; - } + const handleOrderChange: OnDragEndResponder = useRecoilCallback( + ({ snapshot }) => + (result) => { + if (!result.destination) { + return; + } + + const visibleRecordGroupIds = getSnapshotValue( + snapshot, + visibleRecordGroupIdsSelector, + ); + + const reorderedVisibleRecordGroupIds = moveArrayItem( + visibleRecordGroupIds, + { + fromIndex: result.source.index - 1, + toIndex: result.destination.index - 1, + }, + ); + + if ( + isDeeplyEqual(visibleRecordGroupIds, reorderedVisibleRecordGroupIds) + ) { + return; + } - const reorderedVisibleBoardGroups = moveArrayItem(visibleRecordGroups, { - fromIndex: result.source.index - 1, - toIndex: result.destination.index - 1, - }); + const updatedRecordGroups = reorderedVisibleRecordGroupIds.reduce< + RecordGroupDefinition[] + >((acc, recordGroupId, index) => { + const recordGroupDefinition = getSnapshotValue( + snapshot, + recordGroupDefinitionFamilyState(recordGroupId), + ); - if (isDeeplyEqual(visibleRecordGroups, reorderedVisibleBoardGroups)) - return; + if (!isDefined(recordGroupDefinition)) { + return acc; + } - const updatedGroups = [...reorderedVisibleBoardGroups].map( - (group, index) => ({ ...group, position: index }), - ); + return [ + ...acc, + { + ...recordGroupDefinition, + position: index, + }, + ]; + }, []); - setRecordGroupDefinitions(updatedGroups); - saveViewGroups(mapRecordGroupDefinitionsToViewGroups(updatedGroups)); - }, - [saveViewGroups, setRecordGroupDefinitions, visibleRecordGroups], + setRecordGroup(updatedRecordGroups); + saveViewGroups( + mapRecordGroupDefinitionsToViewGroups(updatedRecordGroups), + ); + }, + [saveViewGroups, setRecordGroup, visibleRecordGroupIdsSelector], ); 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 index 5f877bff3494..aa26edec35b9 100644 --- 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 @@ -1,125 +1,113 @@ -import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; -import { recordGroupDefinitionsComponentState } from '@/object-record/record-group/states/recordGroupDefinitionsComponentState'; +import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; +import { recordGroupIdsComponentState } from '@/object-record/record-group/states/recordGroupIdsComponentState'; import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; import { recordIndexRecordGroupHideComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupHideComponentState'; -import { tableRowIdsByGroupComponentFamilyState } from '@/object-record/record-table/states/tableRowIdsByGroupComponentFamilyState'; +import { recordIndexRowIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRowIdsByGroupComponentFamilyState'; import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; import { useSaveCurrentViewGroups } from '@/views/hooks/useSaveCurrentViewGroups'; -import { ViewType } from '@/views/types/ViewType'; import { mapRecordGroupDefinitionsToViewGroups } from '@/views/utils/mapRecordGroupDefinitionsToViewGroups'; +import { recordGroupDefinitionToViewGroup } from '@/views/utils/recordGroupDefinitionToViewGroup'; import { useRecoilCallback } from 'recoil'; +import { isDefined } from '~/utils/isDefined'; type UseRecordGroupVisibilityParams = { viewBarId: string; - viewType: ViewType; }; export const useRecordGroupVisibility = ({ viewBarId, - viewType, }: UseRecordGroupVisibilityParams) => { - const recordGroupDefinitionsState = useRecoilComponentCallbackStateV2( - recordGroupDefinitionsComponentState, + const recordIndexRecordGroupIdsState = useRecoilComponentCallbackStateV2( + recordGroupIdsComponentState, ); - const tableRowIdsByGroupFamilyState = useRecoilComponentCallbackStateV2( - tableRowIdsByGroupComponentFamilyState, + const recordIndexRowIdsByGroupFamilyState = useRecoilComponentCallbackStateV2( + recordIndexRowIdsByGroupComponentFamilyState, viewBarId, ); - const { recordIdsByColumnIdFamilyState } = useRecordBoardStates(viewBarId); - const objectOptionsDropdownRecordGroupHideState = useRecoilComponentCallbackStateV2(recordIndexRecordGroupHideComponentState); - const { saveViewGroups } = useSaveCurrentViewGroups(viewBarId); + const { saveViewGroup, saveViewGroups } = useSaveCurrentViewGroups(viewBarId); const handleVisibilityChange = useRecoilCallback( - ({ snapshot, set }) => - async (updatedRecordGroupDefinition: RecordGroupDefinition) => { - const recordGroupDefinitions = getSnapshotValue( - snapshot, - recordGroupDefinitionsState, + ({ set }) => + async (updatedRecordGroup: RecordGroupDefinition) => { + set( + recordGroupDefinitionFamilyState(updatedRecordGroup.id), + updatedRecordGroup, ); - const updatedRecordGroupDefinitions = recordGroupDefinitions.map( - (groupDefinition) => - groupDefinition.id === updatedRecordGroupDefinition.id - ? { - ...groupDefinition, - isVisible: !groupDefinition.isVisible, - } - : groupDefinition, - ); - - set(recordGroupDefinitionsState, updatedRecordGroupDefinitions); - - saveViewGroups( - mapRecordGroupDefinitionsToViewGroups(updatedRecordGroupDefinitions), - ); + saveViewGroup(recordGroupDefinitionToViewGroup(updatedRecordGroup)); // If visibility is manually toggled, we should reset the hideEmptyRecordGroup state set(objectOptionsDropdownRecordGroupHideState, false); }, - [ - objectOptionsDropdownRecordGroupHideState, - recordGroupDefinitionsState, - saveViewGroups, - ], + [saveViewGroup, objectOptionsDropdownRecordGroupHideState], ); const handleHideEmptyRecordGroupChange = useRecoilCallback( ({ snapshot, set }) => async () => { - const recordGroupDefinitions = getSnapshotValue( + const updatedRecordGroupDefinitions: RecordGroupDefinition[] = []; + const recordGroupIds = getSnapshotValue( snapshot, - recordGroupDefinitionsState, + recordIndexRecordGroupIdsState, ); const currentHideState = getSnapshotValue( snapshot, objectOptionsDropdownRecordGroupHideState, ); + const newHideState = !currentHideState; - set(objectOptionsDropdownRecordGroupHideState, !currentHideState); - - const updatedRecordGroupDefinitions = recordGroupDefinitions.map( - (recordGroup) => { - // TODO: Maybe we can improve that and only use one state for both table and board - const recordGroupRowIds = - viewType === ViewType.Table - ? getSnapshotValue( - snapshot, - tableRowIdsByGroupFamilyState(recordGroup.id), - ) - : getSnapshotValue( - snapshot, - recordIdsByColumnIdFamilyState(recordGroup.id), - ); - - if (recordGroupRowIds.length > 0) { - return recordGroup; - } - - return { - ...recordGroup, - isVisible: currentHideState, - }; - }, - ); + set(objectOptionsDropdownRecordGroupHideState, newHideState); + + for (const recordGroupId of recordGroupIds) { + const recordGroup = getSnapshotValue( + snapshot, + recordGroupDefinitionFamilyState(recordGroupId), + ); + + if (!isDefined(recordGroup)) { + throw new Error( + `Record group with id ${recordGroupId} not found in snapshot`, + ); + } + + const recordGroupRowIds = getSnapshotValue( + snapshot, + recordIndexRowIdsByGroupFamilyState(recordGroupId), + ); + + if (recordGroupRowIds.length > 0) { + continue; + } + + const updatedRecordGroup = { + ...recordGroup, + isVisible: !newHideState, + }; + + set( + recordGroupDefinitionFamilyState(recordGroupId), + updatedRecordGroup, + ); + + updatedRecordGroupDefinitions.push(updatedRecordGroup); + } saveViewGroups( mapRecordGroupDefinitionsToViewGroups(updatedRecordGroupDefinitions), ); }, [ - recordGroupDefinitionsState, + recordIndexRecordGroupIdsState, objectOptionsDropdownRecordGroupHideState, saveViewGroups, - viewType, - tableRowIdsByGroupFamilyState, - recordIdsByColumnIdFamilyState, + recordIndexRowIdsByGroupFamilyState, ], ); 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 deleted file mode 100644 index 8f638dbbe486..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroups.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { useMemo } from 'react'; - -import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { recordGroupDefinitionsComponentState } from '@/object-record/record-group/states/recordGroupDefinitionsComponentState'; -import { sortRecordGroupDefinitions } from '@/object-record/record-group/utils/sortRecordGroupDefinitions'; -import { recordIndexRecordGroupSortComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupSortComponentState'; -import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; - -type UseRecordGroupsParams = { - objectNameSingular: string; -}; - -export const useRecordGroups = ({ - objectNameSingular, -}: UseRecordGroupsParams) => { - const recordGroupDefinitions = useRecoilComponentValueV2( - recordGroupDefinitionsComponentState, - ); - - const recordGroupSort = useRecoilComponentValueV2( - recordIndexRecordGroupSortComponentState, - ); - - 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( - () => sortRecordGroupDefinitions(recordGroupDefinitions, recordGroupSort), - [recordGroupDefinitions, recordGroupSort], - ); - - 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/hooks/useSetRecordGroup.ts b/packages/twenty-front/src/modules/object-record/record-group/hooks/useSetRecordGroup.ts new file mode 100644 index 000000000000..d84e393a08c7 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-group/hooks/useSetRecordGroup.ts @@ -0,0 +1,83 @@ +import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; +import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState'; +import { recordGroupIdsComponentState } from '@/object-record/record-group/states/recordGroupIdsComponentState'; +import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; +import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; +import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; +import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; +import { useContext } from 'react'; +import { useRecoilCallback } from 'recoil'; +import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; +import { isDefined } from '~/utils/isDefined'; + +export const useSetRecordGroup = (viewId?: string) => { + const { objectMetadataItem } = useContext(RecordIndexRootPropsContext); + + const recordIndexRecordGroupIdsState = useRecoilComponentCallbackStateV2( + recordGroupIdsComponentState, + viewId, + ); + + const recordGroupFieldMetadataState = useRecoilComponentCallbackStateV2( + recordGroupFieldMetadataComponentState, + viewId, + ); + + return useRecoilCallback( + ({ snapshot, set }) => + (recordGroups: RecordGroupDefinition[]) => { + if (recordGroups.length === 0) { + return; + } + + const currentRecordGroupId = getSnapshotValue( + snapshot, + recordIndexRecordGroupIdsState, + ); + const fieldMetadataId = recordGroups[0].fieldMetadataId; + const fieldMetadata = objectMetadataItem.fields.find( + (field) => field.id === fieldMetadataId, + ); + const currentFieldMetadata = getSnapshotValue( + snapshot, + recordGroupFieldMetadataState, + ); + + // Set the field metadata linked to the record groups + if ( + isDefined(fieldMetadata) && + !isDeeplyEqual(fieldMetadata, currentFieldMetadata) + ) { + set(recordGroupFieldMetadataState, fieldMetadata); + } + + // Set the record groups by id + recordGroups.forEach((recordGroup) => { + const existingRecordGroup = getSnapshotValue( + snapshot, + recordGroupDefinitionFamilyState(recordGroup.id), + ); + + if (isDeeplyEqual(existingRecordGroup, recordGroup)) { + return; + } + + set(recordGroupDefinitionFamilyState(recordGroup.id), recordGroup); + }); + + const recordGroupIds = recordGroups.map(({ id }) => id); + + if (isDeeplyEqual(currentRecordGroupId, recordGroupIds)) { + return; + } + + // Set the record group ids + set(recordIndexRecordGroupIdsState, recordGroupIds); + }, + [ + objectMetadataItem.fields, + recordGroupFieldMetadataState, + recordIndexRecordGroupIdsState, + ], + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-group/states/recordGroupDefinitionFamilyState.ts b/packages/twenty-front/src/modules/object-record/record-group/states/recordGroupDefinitionFamilyState.ts new file mode 100644 index 000000000000..80cf70f68590 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-group/states/recordGroupDefinitionFamilyState.ts @@ -0,0 +1,10 @@ +import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; +import { atomFamily } from 'recoil'; + +export const recordGroupDefinitionFamilyState = atomFamily< + RecordGroupDefinition | undefined, + RecordGroupDefinition['id'] +>({ + key: 'recordGroupDefinitionFamilyState', + default: undefined, +}); diff --git a/packages/twenty-front/src/modules/object-record/record-group/states/recordGroupFieldMetadataComponentState.ts b/packages/twenty-front/src/modules/object-record/record-group/states/recordGroupFieldMetadataComponentState.ts new file mode 100644 index 000000000000..144b47a043ac --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-group/states/recordGroupFieldMetadataComponentState.ts @@ -0,0 +1,11 @@ +import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; +import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; + +export const recordGroupFieldMetadataComponentState = createComponentStateV2< + FieldMetadataItem | undefined +>({ + key: 'recordGroupFieldMetadataComponentState', + defaultValue: undefined, + componentInstanceContext: ViewComponentInstanceContext, +}); 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/recordGroupIdsComponentState.ts similarity index 72% rename from packages/twenty-front/src/modules/object-record/record-group/states/recordGroupDefinitionsComponentState.ts rename to packages/twenty-front/src/modules/object-record/record-group/states/recordGroupIdsComponentState.ts index 56ec80fcc243..6665fb6e44e3 100644 --- a/packages/twenty-front/src/modules/object-record/record-group/states/recordGroupDefinitionsComponentState.ts +++ b/packages/twenty-front/src/modules/object-record/record-group/states/recordGroupIdsComponentState.ts @@ -2,10 +2,10 @@ import { RecordGroupDefinition } from '@/object-record/record-group/types/Record import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; -export const recordGroupDefinitionsComponentState = createComponentStateV2< - RecordGroupDefinition[] +export const recordGroupIdsComponentState = createComponentStateV2< + RecordGroupDefinition['id'][] >({ - key: 'recordGroupDefinitionsComponentState', + key: 'recordGroupIdsComponentState', defaultValue: [], componentInstanceContext: ViewComponentInstanceContext, }); diff --git a/packages/twenty-front/src/modules/object-record/record-group/states/hasRecordGroupDefinitionsComponentSelector.ts b/packages/twenty-front/src/modules/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector.ts similarity index 52% rename from packages/twenty-front/src/modules/object-record/record-group/states/hasRecordGroupDefinitionsComponentSelector.ts rename to packages/twenty-front/src/modules/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector.ts index bccab902cc86..7603240105c2 100644 --- a/packages/twenty-front/src/modules/object-record/record-group/states/hasRecordGroupDefinitionsComponentSelector.ts +++ b/packages/twenty-front/src/modules/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector.ts @@ -1,21 +1,21 @@ -import { recordGroupDefinitionsComponentState } from '@/object-record/record-group/states/recordGroupDefinitionsComponentState'; +import { recordGroupIdsComponentState } from '@/object-record/record-group/states/recordGroupIdsComponentState'; import { createComponentSelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentSelectorV2'; import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; -export const hasRecordGroupDefinitionsComponentSelector = +export const hasRecordGroupsComponentSelector = createComponentSelectorV2({ - key: 'hasRecordGroupDefinitionsComponentSelector', + key: 'hasRecordGroupsComponentSelector', componentInstanceContext: ViewComponentInstanceContext, get: ({ instanceId }) => ({ get }) => { - const recordGroupDefinitions = get( - recordGroupDefinitionsComponentState.atomFamily({ + const recordGroupIds = get( + recordGroupIdsComponentState.atomFamily({ instanceId, }), ); - return recordGroupDefinitions.length > 0; + return recordGroupIds.length > 0; }, }); diff --git a/packages/twenty-front/src/modules/object-record/record-group/states/selectors/hiddenRecordGroupIdsComponentSelector.ts b/packages/twenty-front/src/modules/object-record/record-group/states/selectors/hiddenRecordGroupIdsComponentSelector.ts new file mode 100644 index 000000000000..4c57886cbff8 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-group/states/selectors/hiddenRecordGroupIdsComponentSelector.ts @@ -0,0 +1,34 @@ +import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; +import { recordGroupIdsComponentState } from '@/object-record/record-group/states/recordGroupIdsComponentState'; + +import { createComponentSelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentSelectorV2'; +import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; +import { isDefined } from '~/utils/isDefined'; + +export const hiddenRecordGroupIdsComponentSelector = createComponentSelectorV2< + string[] +>({ + key: 'hiddenRecordGroupIdsComponentSelector', + componentInstanceContext: ViewComponentInstanceContext, + get: + ({ instanceId }) => + ({ get }) => { + const recordGroupIds = get( + recordGroupIdsComponentState.atomFamily({ + instanceId, + }), + ); + + return recordGroupIds.filter((recordGroupId) => { + const recordGroupDefinition = get( + recordGroupDefinitionFamilyState(recordGroupId), + ); + + if (!isDefined(recordGroupDefinition)) { + return false; + } + + return !recordGroupDefinition.isVisible; + }); + }, +}); diff --git a/packages/twenty-front/src/modules/object-record/record-group/states/selectors/recordGroupDefinitionsComponentSelector.ts b/packages/twenty-front/src/modules/object-record/record-group/states/selectors/recordGroupDefinitionsComponentSelector.ts new file mode 100644 index 000000000000..ce0e0432eabe --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-group/states/selectors/recordGroupDefinitionsComponentSelector.ts @@ -0,0 +1,37 @@ +import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; +import { recordGroupIdsComponentState } from '@/object-record/record-group/states/recordGroupIdsComponentState'; +import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; + +import { createComponentSelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentSelectorV2'; +import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; +import { isDefined } from '~/utils/isDefined'; + +export const recordGroupDefinitionsComponentSelector = + createComponentSelectorV2({ + key: 'recordGroupDefinitionsComponentSelector', + componentInstanceContext: ViewComponentInstanceContext, + get: + ({ instanceId }) => + ({ get }) => { + const recordGroupIds = get( + recordGroupIdsComponentState.atomFamily({ + instanceId, + }), + ); + + return recordGroupIds.reduce( + (acc, recordGroupId) => { + const recordGroupDefinition = get( + recordGroupDefinitionFamilyState(recordGroupId), + ); + + if (!isDefined(recordGroupDefinition)) { + return acc; + } + + return [...acc, recordGroupDefinition]; + }, + [], + ); + }, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector.ts b/packages/twenty-front/src/modules/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector.ts new file mode 100644 index 000000000000..4c2332ebbdde --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector.ts @@ -0,0 +1,63 @@ +import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; +import { recordGroupIdsComponentState } from '@/object-record/record-group/states/recordGroupIdsComponentState'; +import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; +import { RecordGroupSort } from '@/object-record/record-group/types/RecordGroupSort'; +import { sortedInsert } from '@/object-record/record-group/utils/sortedInsert'; +import { recordIndexRecordGroupSortComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupSortComponentState'; + +import { createComponentSelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentSelectorV2'; +import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; +import { isDefined } from '~/utils/isDefined'; + +export const visibleRecordGroupIdsComponentSelector = createComponentSelectorV2< + string[] +>({ + key: 'visibleRecordGroupIdsComponentSelector', + componentInstanceContext: ViewComponentInstanceContext, + get: + ({ instanceId }) => + ({ get }) => { + const recordGroupSort = get( + recordIndexRecordGroupSortComponentState.atomFamily({ + instanceId, + }), + ); + const recordGroupIds = get( + recordGroupIdsComponentState.atomFamily({ + instanceId, + }), + ); + + const result: RecordGroupDefinition[] = []; + + const comparator = ( + a: RecordGroupDefinition, + b: RecordGroupDefinition, + ) => { + switch (recordGroupSort) { + case RecordGroupSort.Alphabetical: + return a.title.localeCompare(b.title); + case RecordGroupSort.ReverseAlphabetical: + return b.title.localeCompare(a.title); + case RecordGroupSort.Manual: + default: + return a.position - b.position; + } + }; + + for (const recordGroupId of recordGroupIds) { + const recordGroupDefinition = get( + recordGroupDefinitionFamilyState(recordGroupId), + ); + + if ( + isDefined(recordGroupDefinition) && + recordGroupDefinition.isVisible + ) { + sortedInsert(result, recordGroupDefinition, comparator); + } + } + + return result.map(({ id }) => id); + }, +}); diff --git a/packages/twenty-front/src/modules/object-record/record-group/utils/sortRecordGroupDefinitions.ts b/packages/twenty-front/src/modules/object-record/record-group/utils/sortRecordGroupDefinitions.ts index 45c3a68b6073..afe2aa41a879 100644 --- a/packages/twenty-front/src/modules/object-record/record-group/utils/sortRecordGroupDefinitions.ts +++ b/packages/twenty-front/src/modules/object-record/record-group/utils/sortRecordGroupDefinitions.ts @@ -5,7 +5,7 @@ export const sortRecordGroupDefinitions = ( recordGroupDefinitions: RecordGroupDefinition[], recordGroupSort: RecordGroupSort, ) => { - const visibleGroups = recordGroupDefinitions.filter( + const visibleRecordGroups = recordGroupDefinitions.filter( (boardGroup) => boardGroup.isVisible, ); @@ -17,15 +17,15 @@ export const sortRecordGroupDefinitions = ( switch (recordGroupSort) { case RecordGroupSort.Alphabetical: - return visibleGroups.sort((a, b) => + return visibleRecordGroups.sort((a, b) => compareAlphabetical(a.title.toLowerCase(), b.title.toLowerCase()), ); case RecordGroupSort.ReverseAlphabetical: - return visibleGroups.sort((a, b) => + return visibleRecordGroups.sort((a, b) => compareAlphabetical(a.title.toLowerCase(), b.title.toLowerCase(), true), ); case RecordGroupSort.Manual: default: - return visibleGroups.sort((a, b) => a.position - b.position); + return visibleRecordGroups.sort((a, b) => a.position - b.position); } }; diff --git a/packages/twenty-front/src/modules/object-record/record-group/utils/sortedInsert.ts b/packages/twenty-front/src/modules/object-record/record-group/utils/sortedInsert.ts new file mode 100644 index 000000000000..5a7dd1027e66 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-group/utils/sortedInsert.ts @@ -0,0 +1,20 @@ +export const sortedInsert = ( + array: T[], + item: T, + comparator: (a: T, b: T) => number, +) => { + let low = 0; + let high = array.length; + + while (low < high) { + const mid = Math.floor((low + high) / 2); + + if (comparator(item, array[mid]) < 0) { + high = mid; + } else { + low = mid + 1; + } + } + + array.splice(low, 0, item); +}; 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 6f6a2e2bede4..bde4108f5eb0 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 @@ -5,7 +5,7 @@ import { isRecordBoardFetchingRecordsByColumnFamilyState } from '@/object-record import { recordBoardShouldFetchMoreInColumnComponentFamilyState } from '@/object-record/record-board/states/recordBoardShouldFetchMoreInColumnComponentFamilyState'; import { useLoadRecordIndexBoardColumn } from '@/object-record/record-index/hooks/useLoadRecordIndexBoardColumn'; import { isRecordIndexBoardColumnLoadingFamilyState } from '@/object-record/states/isRecordBoardColumnLoadingFamilyState'; -import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId'; +import { useRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyStateV2'; export const RecordIndexBoardColumnLoaderEffect = ({ objectNameSingular, @@ -18,20 +18,14 @@ export const RecordIndexBoardColumnLoaderEffect = ({ boardFieldMetadataId: string | null; columnId: string; }) => { - const [shouldFetchMore, setShouldFetchMore] = useRecoilState( - recordBoardShouldFetchMoreInColumnComponentFamilyState({ - scopeId: getScopeIdFromComponentId(recordBoardId), - familyKey: columnId, - }), + const [shouldFetchMore, setShouldFetchMore] = useRecoilComponentFamilyStateV2( + recordBoardShouldFetchMoreInColumnComponentFamilyState, + columnId, + recordBoardId, ); const [loadingRecordsForThisColumn, setLoadingRecordsForThisColumn] = - useRecoilState( - isRecordBoardFetchingRecordsByColumnFamilyState({ - scopeId: getScopeIdFromComponentId(recordBoardId), - familyKey: { columnId }, - }), - ); + useRecoilState(isRecordBoardFetchingRecordsByColumnFamilyState(columnId)); const { fetchMoreRecords, loading, records, hasNextPage } = useLoadRecordIndexBoardColumn({ 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 194580587dc9..e8bd37d0534a 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 @@ -1,9 +1,10 @@ import { useRecoilValue } from 'recoil'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; +import { visibleRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector'; import { RecordIndexBoardColumnLoaderEffect } from '@/object-record/record-index/components/RecordIndexBoardColumnLoaderEffect'; import { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; type RecordIndexBoardDataLoaderProps = { objectNameSingular: string; @@ -18,6 +19,10 @@ export const RecordIndexBoardDataLoader = ({ objectNameSingular, }); + const visibleRecordGroupIds = useRecoilComponentValueV2( + visibleRecordGroupIdsComponentSelector, + ); + const recordIndexKanbanFieldMetadataId = useRecoilValue( recordIndexKanbanFieldMetadataIdState, ); @@ -26,18 +31,14 @@ export const RecordIndexBoardDataLoader = ({ (field) => field.id === recordIndexKanbanFieldMetadataId, ); - const { columnIdsState } = useRecordBoardStates(recordBoardId); - - const columnIds = useRecoilValue(columnIdsState); - return ( <> - {columnIds.map((columnId, index) => ( + {visibleRecordGroupIds.map((recordGroupId, index) => ( ))} 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 02834d1e7f9e..81f64f3f6f8b 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,101 +1,52 @@ import { useEffect } from 'react'; -import { useRecoilValue, useSetRecoilState } from 'recoil'; +import { useRecoilValue } from 'recoil'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; -import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard'; -import { recordGroupDefinitionsComponentState } from '@/object-record/record-group/states/recordGroupDefinitionsComponentState'; +import { isRecordBoardCompactModeActiveComponentState } from '@/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState'; +import { recordBoardFieldDefinitionsComponentState } from '@/object-record/record-board/states/recordBoardFieldDefinitionsComponentState'; +import { recordBoardSelectedRecordIdsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardSelectedRecordIdsComponentSelector'; 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 { 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'; type RecordIndexBoardDataLoaderEffectProps = { - objectNameSingular: string; recordBoardId: string; }; export const RecordIndexBoardDataLoaderEffect = ({ - objectNameSingular, recordBoardId, }: RecordIndexBoardDataLoaderEffectProps) => { - const { objectMetadataItem } = useObjectMetadataItem({ - objectNameSingular, - }); - const recordIndexFieldDefinitions = useRecoilValue( recordIndexFieldDefinitionsState, ); - const recordGroupDefinitions = useRecoilComponentValueV2( - recordGroupDefinitionsComponentState, + const recordIndexIsCompactModeActive = useRecoilValue( + recordIndexIsCompactModeActiveState, ); - const recordIndexKanbanFieldMetadataId = useRecoilValue( - recordIndexKanbanFieldMetadataIdState, + const setRecordBoardFieldDefinitions = useSetRecoilComponentStateV2( + recordBoardFieldDefinitionsComponentState, + recordBoardId, ); - const recordIndexIsCompactModeActive = useRecoilValue( - recordIndexIsCompactModeActiveState, + const selectedRecordIds = useRecoilComponentValueV2( + recordBoardSelectedRecordIdsComponentSelector, + recordBoardId, ); - const { isCompactModeActiveState } = useRecordBoard(recordBoardId); - - const setIsCompactModeActive = useSetRecoilState(isCompactModeActiveState); + const setIsCompactModeActive = useSetRecoilComponentStateV2( + isRecordBoardCompactModeActiveComponentState, + recordBoardId, + ); useEffect(() => { setIsCompactModeActive(recordIndexIsCompactModeActive); }, [recordIndexIsCompactModeActive, setIsCompactModeActive]); - const { - setColumns, - setObjectSingularName, - selectedRecordIdsSelector, - setFieldDefinitions, - setKanbanFieldMetadataName, - } = useRecordBoard(recordBoardId); - - useEffect(() => { - setFieldDefinitions(recordIndexFieldDefinitions); - }, [recordIndexFieldDefinitions, setFieldDefinitions]); - - useEffect(() => { - setObjectSingularName(objectNameSingular); - }, [objectNameSingular, setObjectSingularName]); - - useEffect(() => { - setColumns(recordGroupDefinitions); - }, [recordGroupDefinitions, 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]); - - useEffect(() => { - if (isDefined(recordIndexKanbanFieldMetadataId)) { - const kanbanFieldMetadataName = objectMetadataItem?.fields.find( - (field) => - field.type === FieldMetadataType.Select && - field.id === recordIndexKanbanFieldMetadataId, - )?.name; - - if (isDefined(kanbanFieldMetadataName)) { - setKanbanFieldMetadataName(kanbanFieldMetadataName); - } - } - }, [ - objectMetadataItem, - recordIndexKanbanFieldMetadataId, - setKanbanFieldMetadataName, - ]); - - const selectedRecordIds = useRecoilValue(selectedRecordIdsSelector()); + setRecordBoardFieldDefinitions(recordIndexFieldDefinitions); + }, [recordIndexFieldDefinitions, setRecordBoardFieldDefinitions]); const setContextStoreTargetedRecords = useSetRecoilComponentStateV2( contextStoreTargetedRecordsRuleComponentState, 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 feda1b8b966d..0da7a717210a 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,11 +24,9 @@ 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 { useSetRecordGroup } from '@/object-record/record-group/hooks/useSetRecordGroup'; import { RecordIndexFiltersToContextStoreEffect } from '@/object-record/record-index/components/RecordIndexFiltersToContextStoreEffect'; import { recordIndexViewFilterGroupsState } from '@/object-record/record-index/states/recordIndexViewFilterGroupsState'; -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 { ViewField } from '@/views/types/ViewField'; @@ -38,7 +36,7 @@ import { mapViewFieldsToColumnDefinitions } from '@/views/utils/mapViewFieldsToC import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters'; import { mapViewGroupsToRecordGroupDefinitions } from '@/views/utils/mapViewGroupsToRecordGroupDefinitions'; import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts'; -import { useContext } from 'react'; +import { useCallback, useContext } from 'react'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; const StyledContainer = styled.div` @@ -68,9 +66,7 @@ export const RecordIndexContainer = () => { objectNameSingular, } = useContext(RecordIndexRootPropsContext); - const recordGroupDefinitionsCallbackState = useRecoilComponentCallbackStateV2( - recordGroupDefinitionsComponentState, - ); + const setRecordGroup = useSetRecordGroup(recordIndexId); const { columnDefinitions, filterDefinitions, sortDefinitions } = useColumnDefinitionsFromFieldMetadata(objectMetadataItem); @@ -96,8 +92,6 @@ export const RecordIndexContainer = () => { recordTableId: recordIndexId, }); - const { setColumns } = useRecordBoard(recordIndexId); - const onViewFieldsChange = useRecoilCallback( ({ set, snapshot }) => (viewFields: ViewField[]) => { @@ -124,30 +118,16 @@ 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(); + const onViewGroupsChange = useCallback( + (viewGroups: ViewGroup[]) => { + const newGroupDefinitions = mapViewGroupsToRecordGroupDefinitions({ + objectMetadataItem, + viewGroups, + }); - if ( - !isDeeplyEqual( - existingRecordIndexGroupDefinitions, - newGroupDefinitions, - ) - ) { - set(recordGroupDefinitionsCallbackState, newGroupDefinitions); - } - }, - [objectMetadataItem, recordGroupDefinitionsCallbackState, setColumns], + setRecordGroup(newGroupDefinitions); + }, + [objectMetadataItem, setRecordGroup], ); const setContextStoreTargetedRecordsRule = useSetRecoilComponentStateV2( @@ -229,10 +209,7 @@ export const RecordIndexContainer = () => { objectNameSingular={objectNameSingular} recordBoardId={recordIndexId} /> - + )} 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 65c8b130399f..ace9040aee3e 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 @@ -1,8 +1,9 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -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 { recordBoardVisibleFieldDefinitionsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardVisibleFieldDefinitionsComponentSelector'; +import { visibleRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector'; 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'; @@ -11,6 +12,7 @@ import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import styled from '@emotion/styled'; import { useCallback, useContext } from 'react'; import { useRecoilValue } from 'recoil'; @@ -32,6 +34,10 @@ export const RecordIndexPageKanbanAddButton = () => { ); const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular }); + const visibleRecordGroupIds = useRecoilComponentValueV2( + visibleRecordGroupIdsComponentSelector, + ); + const recordIndexKanbanFieldMetadataId = useRecoilValue( recordIndexKanbanFieldMetadataIdState, ); @@ -42,12 +48,11 @@ export const RecordIndexPageKanbanAddButton = () => { const isOpportunity = objectMetadataItem.nameSingular === CoreObjectNameSingular.Opportunity; - const { columnIdsState, visibleFieldDefinitionsState } = - useRecordBoardStates(recordIndexId); - const columnIds = useRecoilValue(columnIdsState); - const visibleFieldDefinitions = useRecoilValue( - visibleFieldDefinitionsState(), + const visibleFieldDefinitions = useRecoilComponentValueV2( + recordBoardVisibleFieldDefinitionsComponentSelector, + recordIndexId, ); + const labelIdentifierField = visibleFieldDefinitions.find( (field) => field.isLabelIdentifier, ); @@ -101,11 +106,10 @@ export const RecordIndexPageKanbanAddButton = () => { dropdownComponents={ - {columnIds.map((columnId) => ( + {visibleRecordGroupIds.map((recordGroupId) => ( ))} 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 effa5ce3a49c..c209a6b0ee3a 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,7 +1,9 @@ +import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; import { RecordGroupDefinitionType } from '@/object-record/record-group/types/RecordGroupDefinition'; -import { useRecordIndexPageKanbanAddMenuItem } from '@/object-record/record-index/hooks/useRecordIndexPageKanbanAddMenuItem'; import styled from '@emotion/styled'; +import { useRecoilValue } from 'recoil'; import { MenuItem, Tag } from 'twenty-ui'; +import { isDefined } from '~/utils/isDefined'; const StyledMenuItem = styled(MenuItem)` width: calc(100% - 2 * var(--horizontal-padding)); @@ -9,20 +11,18 @@ const StyledMenuItem = styled(MenuItem)` type RecordIndexPageKanbanAddMenuItemProps = { columnId: string; - recordIndexId: string; onItemClick: (columnDefinition: any) => void; }; export const RecordIndexPageKanbanAddMenuItem = ({ columnId, - recordIndexId, onItemClick, }: RecordIndexPageKanbanAddMenuItemProps) => { - const { columnDefinition } = useRecordIndexPageKanbanAddMenuItem( - recordIndexId, - columnId, + const recordGroupDefinition = useRecoilValue( + recordGroupDefinitionFamilyState(columnId), ); - if (!columnDefinition) { + + if (!isDefined(recordGroupDefinition)) { return null; } @@ -31,24 +31,24 @@ export const RecordIndexPageKanbanAddMenuItem = ({ text={ } - onClick={() => onItemClick(columnDefinition)} + onClick={() => onItemClick(recordGroupDefinition)} /> ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-index/export/hooks/__tests__/useExportFetchRecords.test.ts b/packages/twenty-front/src/modules/object-record/record-index/export/hooks/__tests__/useExportFetchRecords.test.ts index e1bc40ca0eef..94042f4e02b1 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/export/hooks/__tests__/useExportFetchRecords.test.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/export/hooks/__tests__/useExportFetchRecords.test.ts @@ -8,14 +8,12 @@ import { import { PERSON_FRAGMENT_WITH_DEPTH_ZERO_RELATIONS } from '@/object-record/hooks/__mocks__/personFragments'; import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard'; -import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard'; -import { recordBoardKanbanFieldMetadataNameComponentState } from '@/object-record/record-board/states/recordBoardKanbanFieldMetadataNameComponentState'; -import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState'; +import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState'; +import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; import { ViewType } from '@/views/types/ViewType'; import { MockedResponse } from '@apollo/client/testing'; import { expect } from '@storybook/test'; import gql from 'graphql-tag'; -import { useRecoilValue } from 'recoil'; import { getJestMetadataAndApolloMocksAndContextStoreWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndContextStoreWrapper'; import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; @@ -232,10 +230,12 @@ describe('useRecordData', () => { const callback = jest.fn(); const { result } = renderHook( () => { - const kanbanFieldNameState = extractComponentState( - recordBoardKanbanFieldMetadataNameComponentState, - recordIndexId, - ); + const [recordGroupFieldMetadata, setRecordGroupFieldMetadata] = + useRecoilComponentStateV2( + recordGroupFieldMetadataComponentState, + recordIndexId, + ); + return { tableData: useExportFetchRecords({ recordIndexId, @@ -246,8 +246,8 @@ describe('useRecordData', () => { delayMs: 0, viewType: ViewType.Kanban, }), - useRecordBoardHook: useRecordBoard(recordIndexId), - kanbanFieldName: useRecoilValue(kanbanFieldNameState), + kanbanFieldName: recordGroupFieldMetadata?.name, + setRecordGroupFieldMetadata, kanbanData: useObjectOptionsForBoard({ objectNameSingular: objectMetadataItem.nameSingular, recordBoardId: recordIndexId, @@ -269,9 +269,7 @@ describe('useRecordData', () => { ); await act(async () => { - result.current.useRecordBoardHook.setKanbanFieldMetadataName( - updatedAtFieldMetadataItem?.name, - ); + result.current.setRecordGroupFieldMetadata(updatedAtFieldMetadataItem); }); await act(async () => { @@ -322,10 +320,12 @@ describe('useRecordData', () => { const callback = jest.fn(); const { result } = renderHook( () => { - const kanbanFieldNameState = extractComponentState( - recordBoardKanbanFieldMetadataNameComponentState, - recordIndexId, - ); + const [recordGroupFieldMetadata, setRecordGroupFieldMetadata] = + useRecoilComponentStateV2( + recordGroupFieldMetadataComponentState, + recordIndexId, + ); + return { tableData: useExportFetchRecords({ recordIndexId, @@ -336,8 +336,9 @@ describe('useRecordData', () => { delayMs: 0, viewType: ViewType.Table, }), - setKanbanFieldName: useRecordBoard(recordIndexId), - kanbanFieldName: useRecoilValue(kanbanFieldNameState), + objectMetadataItem, + kanbanFieldName: recordGroupFieldMetadata?.name, + setRecordGroupFieldMetadata, kanbanData: useObjectOptionsForBoard({ objectNameSingular: objectMetadataItem.nameSingular, recordBoardId: recordIndexId, @@ -351,9 +352,14 @@ describe('useRecordData', () => { ); await act(async () => { - result.current.setKanbanFieldName.setKanbanFieldMetadataName( - result.current.kanbanData.hiddenBoardFields[0].metadata.fieldName, - ); + const fieldMetadataItem = + result.current.objectMetadataItem?.fields.find( + (fieldMetadata) => + fieldMetadata.id === + result.current.kanbanData.hiddenBoardFields[0].fieldMetadataId, + ); + + result.current.setRecordGroupFieldMetadata(fieldMetadataItem); }); await act(async () => { diff --git a/packages/twenty-front/src/modules/object-record/record-index/export/hooks/useExportFetchRecords.ts b/packages/twenty-front/src/modules/object-record/record-index/export/hooks/useExportFetchRecords.ts index bcc82de3073b..5237622268d9 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/export/hooks/useExportFetchRecords.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/export/hooks/useExportFetchRecords.ts @@ -1,5 +1,4 @@ import { useEffect, useState } from 'react'; -import { useRecoilValue } from 'recoil'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; @@ -13,7 +12,7 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRecords'; import { EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE } from '@/object-record/object-options-dropdown/constants/ExportTableDataDefaultPageSize'; import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard'; -import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; +import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState'; import { useFindManyParams } from '@/object-record/record-index/hooks/useLoadRecordIndexTable'; import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; @@ -68,10 +67,13 @@ export const useExportFetchRecords = ({ viewBarId: recordIndexId, }); - const { kanbanFieldMetadataNameState } = useRecordBoardStates(recordIndexId); - const kanbanFieldMetadataName = useRecoilValue(kanbanFieldMetadataNameState); + const recordGroupFieldMetadata = useRecoilComponentValueV2( + recordGroupFieldMetadataComponentState, + recordIndexId, + ); + const hiddenKanbanFieldColumn = hiddenBoardFields.find( - (column) => column.metadata.fieldName === kanbanFieldMetadataName, + (column) => column.metadata.fieldName === recordGroupFieldMetadata?.name, ); const columns = useRecoilComponentValueV2( visibleTableColumnsComponentSelector, diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoard.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoard.ts index 7b241909de0c..fee8cc97aa9a 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoard.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoard.ts @@ -1,12 +1,13 @@ import { useEffect } from 'react'; -import { useRecoilValue, useSetRecoilState } from 'recoil'; +import { useRecoilValue } from 'recoil'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy'; -import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard'; +import { useSetRecordBoardRecordIds } from '@/object-record/record-board/hooks/useSetRecordBoardRecordIds'; +import { isRecordBoardCompactModeActiveComponentState } from '@/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState'; +import { recordBoardFieldDefinitionsComponentState } from '@/object-record/record-board/states/recordBoardFieldDefinitionsComponentState'; import { computeViewRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter'; -import { recordGroupDefinitionsComponentState } from '@/object-record/record-group/states/recordGroupDefinitionsComponentState'; import { useRecordBoardRecordGqlFields } from '@/object-record/record-index/hooks/useRecordBoardRecordGqlFields'; import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState'; import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState'; @@ -14,7 +15,7 @@ import { recordIndexIsCompactModeActiveState } from '@/object-record/record-inde import { recordIndexSortsState } from '@/object-record/record-index/states/recordIndexSortsState'; import { recordIndexViewFilterGroupsState } from '@/object-record/record-index/states/recordIndexViewFilterGroupsState'; import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore'; -import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { useSetRecordCountInCurrentView } from '@/views/hooks/useSetRecordCountInCurrentView'; type UseLoadRecordIndexBoardProps = { @@ -31,33 +32,28 @@ export const useLoadRecordIndexBoard = ({ const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular, }); - const { - setRecordIds: setRecordIdsInBoard, - setFieldDefinitions, - setColumns, - isCompactModeActiveState, - } = useRecordBoard(recordBoardId); + + const setRecordBoardFieldDefinitions = useSetRecoilComponentStateV2( + recordBoardFieldDefinitionsComponentState, + recordBoardId, + ); + + const { setRecordIds: setRecordIdsInBoard } = + useSetRecordBoardRecordIds(recordBoardId); + const { upsertRecords: upsertRecordsInStore } = useUpsertRecordsInStore(); const recordIndexFieldDefinitions = useRecoilValue( recordIndexFieldDefinitionsState, ); useEffect(() => { - setFieldDefinitions(recordIndexFieldDefinitions); - }, [recordIndexFieldDefinitions, setFieldDefinitions]); + setRecordBoardFieldDefinitions(recordIndexFieldDefinitions); + }, [recordIndexFieldDefinitions, setRecordBoardFieldDefinitions]); const recordIndexViewFilterGroups = useRecoilValue( recordIndexViewFilterGroupsState, ); - const recordGroupDefinitions = useRecoilComponentValueV2( - recordGroupDefinitionsComponentState, - ); - - useEffect(() => { - setColumns(recordGroupDefinitions); - }, [recordGroupDefinitions, setColumns]); - const recordIndexFilters = useRecoilValue(recordIndexFiltersState); const recordIndexSorts = useRecoilValue(recordIndexSortsState); const requestFilters = computeViewRecordGqlOperationFilter( @@ -92,7 +88,10 @@ export const useLoadRecordIndexBoard = ({ const { setRecordCountInCurrentView } = useSetRecordCountInCurrentView(viewBarId); - const setIsCompactModeActive = useSetRecoilState(isCompactModeActiveState); + const setIsCompactModeActive = useSetRecoilComponentStateV2( + isRecordBoardCompactModeActiveComponentState, + recordBoardId, + ); useEffect(() => { setRecordIdsInBoard(records); 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 99a5220335b6..2738118054fe 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 @@ -4,9 +4,9 @@ import { useRecoilValue } from 'recoil'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy'; -import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; -import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard'; +import { useSetRecordIdsForColumn } from '@/object-record/record-board/hooks/useSetRecordIdsForColumn'; import { computeViewRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter'; +import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; import { useRecordBoardRecordGqlFields } from '@/object-record/record-index/hooks/useRecordBoardRecordGqlFields'; import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState'; import { recordIndexSortsState } from '@/object-record/record-index/states/recordIndexSortsState'; @@ -30,16 +30,18 @@ export const useLoadRecordIndexBoardColumn = ({ const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular, }); - const { setRecordIdsForColumn } = useRecordBoard(recordBoardId); - const { columnsFamilySelector } = useRecordBoardStates(recordBoardId); + const { setRecordIdsForColumn } = useSetRecordIdsForColumn(recordBoardId); const { upsertRecords: upsertRecordsInStore } = useUpsertRecordsInStore(); + const recordGroupDefinition = useRecoilValue( + recordGroupDefinitionFamilyState(columnId), + ); + const recordIndexViewFilterGroups = useRecoilValue( recordIndexViewFilterGroupsState, ); const recordIndexFilters = useRecoilValue(recordIndexFiltersState); const recordIndexSorts = useRecoilValue(recordIndexSortsState); - const columnDefinition = useRecoilValue(columnsFamilySelector(columnId)); const requestFilters = computeViewRecordGqlOperationFilter( recordIndexFilters, @@ -60,9 +62,9 @@ export const useLoadRecordIndexBoardColumn = ({ const filter = { ...requestFilters, [recordIndexKanbanFieldMetadataItem?.name ?? '']: isDefined( - columnDefinition?.value, + recordGroupDefinition?.value, ) - ? { in: [columnDefinition?.value] } + ? { in: [recordGroupDefinition?.value] } : { is: 'NULL' }, }; diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts index 8162bb7dfc2e..d705ef14bc97 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts @@ -27,8 +27,7 @@ export const useFindManyParams = ( objectNameSingular, }); - const currentRecordGroupDefinition = - useCurrentRecordGroupDefinition(recordTableId); + const currentRecordGroupDefinition = useCurrentRecordGroupDefinition(); const tableViewFilterGroups = useRecoilComponentValueV2( tableViewFilterGroupsComponentState, diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordBoardRecordGqlFields.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordBoardRecordGqlFields.ts index aef238c2be0e..4386c2922128 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordBoardRecordGqlFields.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordBoardRecordGqlFields.ts @@ -1,9 +1,9 @@ -import { useRecoilValue } from 'recoil'; - import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { getObjectMetadataIdentifierFields } from '@/object-metadata/utils/getObjectMetadataIdentifierFields'; import { hasPositionField } from '@/object-metadata/utils/hasPositionField'; -import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; +import { recordBoardVisibleFieldDefinitionsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardVisibleFieldDefinitionsComponentSelector'; +import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { isDefined } from '~/utils/isDefined'; export const useRecordBoardRecordGqlFields = ({ @@ -13,15 +13,17 @@ export const useRecordBoardRecordGqlFields = ({ recordBoardId: string; objectMetadataItem: ObjectMetadataItem; }) => { - const { kanbanFieldMetadataNameState, visibleFieldDefinitionsState } = - useRecordBoardStates(recordBoardId); + const visibleFieldDefinitions = useRecoilComponentValueV2( + recordBoardVisibleFieldDefinitionsComponentSelector, + recordBoardId, + ); const { imageIdentifierFieldMetadataItem, labelIdentifierFieldMetadataItem } = getObjectMetadataIdentifierFields({ objectMetadataItem }); - const kanbanFieldMetadataName = useRecoilValue(kanbanFieldMetadataNameState); - const visibleFieldDefinitions = useRecoilValue( - visibleFieldDefinitionsState(), + const recordGroupFieldMetadata = useRecoilComponentValueV2( + recordGroupFieldMetadataComponentState, + recordBoardId, ); const identifierQueryFields: Record = {}; @@ -59,8 +61,8 @@ export const useRecordBoardRecordGqlFields = ({ }, }; - if (isDefined(kanbanFieldMetadataName)) { - recordGqlFields[kanbanFieldMetadataName] = true; + if (isDefined(recordGroupFieldMetadata?.name)) { + recordGqlFields[recordGroupFieldMetadata.name] = true; } return recordGqlFields; diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexPageKanbanAddMenuItem.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexPageKanbanAddMenuItem.ts deleted file mode 100644 index 8e5604cb0fe8..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexPageKanbanAddMenuItem.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; -import { useRecoilValue } from 'recoil'; - -export const useRecordIndexPageKanbanAddMenuItem = ( - recordIndexId: string, - columnId: string, -) => { - const { columnsFamilySelector } = useRecordBoardStates(recordIndexId); - const columnDefinition = useRecoilValue(columnsFamilySelector(columnId)); - - return { columnDefinition }; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexAllRowIdsComponentState.ts b/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexAllRowIdsComponentState.ts new file mode 100644 index 000000000000..7e163fd08ea3 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexAllRowIdsComponentState.ts @@ -0,0 +1,10 @@ +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; +import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; + +export const recordIndexAllRowIdsComponentState = createComponentStateV2< + string[] +>({ + key: 'recordIndexAllRowIdsComponentState', + defaultValue: [], + componentInstanceContext: ViewComponentInstanceContext, +}); diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/tableRowIdsByGroupComponentFamilyState.ts b/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexRowIdsByGroupComponentFamilyState.ts similarity index 50% rename from packages/twenty-front/src/modules/object-record/record-table/states/tableRowIdsByGroupComponentFamilyState.ts rename to packages/twenty-front/src/modules/object-record/record-index/states/recordIndexRowIdsByGroupComponentFamilyState.ts index 395f7f185bb7..fe35021c6eb5 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/states/tableRowIdsByGroupComponentFamilyState.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexRowIdsByGroupComponentFamilyState.ts @@ -1,10 +1,10 @@ import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; -import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; import { createComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentFamilyStateV2'; +import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; -export const tableRowIdsByGroupComponentFamilyState = +export const recordIndexRowIdsByGroupComponentFamilyState = createComponentFamilyStateV2({ - key: 'tableRowIdsByGroupComponentFamilyState', + key: 'recordIndexRowIdsByGroupComponentFamilyState', defaultValue: [], - componentInstanceContext: RecordTableComponentInstanceContext, + componentInstanceContext: ViewComponentInstanceContext, }); diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx index ac2fd976d25d..834761638081 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx @@ -1,7 +1,8 @@ import styled from '@emotion/styled'; import { isNonEmptyString, isNull } from '@sniptt/guards'; -import { hasRecordGroupDefinitionsComponentSelector } from '@/object-record/record-group/states/hasRecordGroupDefinitionsComponentSelector'; +import { hasRecordGroupsComponentSelector } from '@/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector'; +import { recordIndexAllRowIdsComponentState } from '@/object-record/record-index/states/recordIndexAllRowIdsComponentState'; import { RecordTableComponentInstance } from '@/object-record/record-table/components/RecordTableComponentInstance'; import { RecordTableContextProvider } from '@/object-record/record-table/components/RecordTableContextProvider'; import { RecordTableStickyEffect } from '@/object-record/record-table/components/RecordTableStickyEffect'; @@ -16,7 +17,6 @@ import { RecordTableRecordGroupsBody } from '@/object-record/record-table/record import { RecordTableHeader } from '@/object-record/record-table/record-table-header/components/RecordTableHeader'; import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState'; import { recordTablePendingRecordIdComponentState } from '@/object-record/record-table/states/recordTablePendingRecordIdComponentState'; -import { tableAllRowIdsComponentState } from '@/object-record/record-table/states/tableAllRowIdsComponentState'; import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect'; import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; @@ -53,8 +53,8 @@ export const RecordTable = ({ recordTableId, ); - const tableRowIds = useRecoilComponentValueV2( - tableAllRowIdsComponentState, + const allRowIds = useRecoilComponentValueV2( + recordIndexAllRowIdsComponentState, recordTableId, ); @@ -64,13 +64,13 @@ export const RecordTable = ({ ); const hasRecordGroups = useRecoilComponentValueV2( - hasRecordGroupDefinitionsComponentSelector, + hasRecordGroupsComponentSelector, recordTableId, ); const recordTableIsEmpty = !isRecordTableInitialLoading && - tableRowIds.length === 0 && + allRowIds.length === 0 && isNull(pendingRecordId); const { resetTableRowSelection, setRowSelected } = useRecordTable({ @@ -109,9 +109,7 @@ export const RecordTable = ({ {!hasRecordGroups ? ( ) : ( - + )} diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableNoRecordGroupRows.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableNoRecordGroupRows.tsx index f36fa08ae48f..7febddcdfa36 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableNoRecordGroupRows.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableNoRecordGroupRows.tsx @@ -1,14 +1,16 @@ +import { recordIndexAllRowIdsComponentState } from '@/object-record/record-index/states/recordIndexAllRowIdsComponentState'; import { RecordTableBodyFetchMoreLoader } from '@/object-record/record-table/record-table-body/components/RecordTableBodyFetchMoreLoader'; import { RecordTableRow } from '@/object-record/record-table/record-table-row/components/RecordTableRow'; -import { tableAllRowIdsComponentState } from '@/object-record/record-table/states/tableAllRowIdsComponentState'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; export const RecordTableNoRecordGroupRows = () => { - const rowIds = useRecoilComponentValueV2(tableAllRowIdsComponentState); + const allRowIds = useRecoilComponentValueV2( + recordIndexAllRowIdsComponentState, + ); return ( <> - {rowIds.map((recordId, rowIndex) => { + {allRowIds.map((recordId, rowIndex) => { return ( { const recordGroupId = useCurrentRecordGroupId(); - const allRowIds = useRecoilComponentValueV2(tableAllRowIdsComponentState); + const allRowIds = useRecoilComponentValueV2( + recordIndexAllRowIdsComponentState, + ); const recordGroupRowIds = useRecoilComponentFamilyValueV2( - tableRowIdsByGroupComponentFamilyState, + recordIndexRowIdsByGroupComponentFamilyState, recordGroupId, ); diff --git a/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyHandler.tsx b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyHandler.tsx index 7b28090002e3..debedb73f53f 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyHandler.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyHandler.tsx @@ -1,9 +1,9 @@ import { isNull } from '@sniptt/guards'; +import { recordIndexAllRowIdsComponentState } from '@/object-record/record-index/states/recordIndexAllRowIdsComponentState'; import { RecordTableEmptyState } from '@/object-record/record-table/empty-state/components/RecordTableEmptyState'; import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState'; import { recordTablePendingRecordIdComponentState } from '@/object-record/record-table/states/recordTablePendingRecordIdComponentState'; -import { tableAllRowIdsComponentState } from '@/object-record/record-table/states/tableAllRowIdsComponentState'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; type RecordTableEmptyHandlerProps = { @@ -20,8 +20,8 @@ export const RecordTableEmptyHandler = ({ recordTableId, ); - const tableRowIds = useRecoilComponentValueV2( - tableAllRowIdsComponentState, + const allRowIds = useRecoilComponentValueV2( + recordIndexAllRowIdsComponentState, recordTableId, ); @@ -32,7 +32,7 @@ export const RecordTableEmptyHandler = ({ const recordTableIsEmpty = !isRecordTableInitialLoading && - tableRowIds.length === 0 && + allRowIds.length === 0 && isNull(pendingRecordId); if (recordTableIsEmpty) { diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useResetTableRowSelection.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useResetTableRowSelection.ts index 82b51a2e164b..409c30a461a4 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useResetTableRowSelection.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useResetTableRowSelection.ts @@ -2,10 +2,10 @@ import { useRecoilCallback } from 'recoil'; import { getActionMenuDropdownIdFromActionMenuId } from '@/action-menu/utils/getActionMenuDropdownIdFromActionMenuId'; import { getActionMenuIdFromRecordIndexId } from '@/action-menu/utils/getActionMenuIdFromRecordIndexId'; +import { recordIndexAllRowIdsComponentState } from '@/object-record/record-index/states/recordIndexAllRowIdsComponentState'; import { hasUserSelectedAllRowsComponentState } from '@/object-record/record-table/record-table-row/states/hasUserSelectedAllRowsFamilyState'; import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState'; import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; -import { tableAllRowIdsComponentState } from '@/object-record/record-table/states/tableAllRowIdsComponentState'; import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState'; import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; @@ -18,8 +18,8 @@ export const useResetTableRowSelection = (recordTableId?: string) => { recordTableId, ); - const tableAllRowIdsState = useRecoilComponentCallbackStateV2( - tableAllRowIdsComponentState, + const recordIndexAllRowIdsState = useRecoilComponentCallbackStateV2( + recordIndexAllRowIdsComponentState, recordTableIdFromContext, ); @@ -43,9 +43,9 @@ export const useResetTableRowSelection = (recordTableId?: string) => { return useRecoilCallback( ({ set, snapshot }) => () => { - const tableRowIds = getSnapshotValue(snapshot, tableAllRowIdsState); + const allRowIds = getSnapshotValue(snapshot, recordIndexAllRowIdsState); - for (const rowId of tableRowIds) { + for (const rowId of allRowIds) { set(isRowSelectedFamilyState(rowId), false); } @@ -54,7 +54,7 @@ export const useResetTableRowSelection = (recordTableId?: string) => { set(isActionMenuDropdownOpenState, false); }, [ - tableAllRowIdsState, + recordIndexAllRowIdsState, hasUserSelectedAllRowsState, isActionMenuDropdownOpenState, isRowSelectedFamilyState, diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSelectAllRows.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSelectAllRows.ts index 24d54fb1ba22..f715435c076e 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSelectAllRows.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSelectAllRows.ts @@ -1,8 +1,8 @@ import { useRecoilCallback } from 'recoil'; +import { recordIndexAllRowIdsComponentState } from '@/object-record/record-index/states/recordIndexAllRowIdsComponentState'; import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState'; import { allRowsSelectedStatusComponentSelector } from '@/object-record/record-table/states/selectors/allRowsSelectedStatusComponentSelector'; -import { tableAllRowIdsComponentState } from '@/object-record/record-table/states/tableAllRowIdsComponentState'; import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; @@ -15,8 +15,8 @@ export const useSelectAllRows = (recordTableId?: string) => { isRowSelectedComponentFamilyState, recordTableId, ); - const tableAllRowIdsState = useRecoilComponentCallbackStateV2( - tableAllRowIdsComponentState, + const recordIndexAllRowIdsState = useRecoilComponentCallbackStateV2( + recordIndexAllRowIdsComponentState, recordTableId, ); @@ -28,24 +28,24 @@ export const useSelectAllRows = (recordTableId?: string) => { allRowsSelectedStatusSelector, ); - const tableRowIds = getSnapshotValue(snapshot, tableAllRowIdsState); + const allRowIds = getSnapshotValue(snapshot, recordIndexAllRowIdsState); if ( allRowsSelectedStatus === 'none' || allRowsSelectedStatus === 'some' ) { - for (const rowId of tableRowIds) { + for (const rowId of allRowIds) { set(isRowSelectedFamilyState(rowId), true); } } else { - for (const rowId of tableRowIds) { + for (const rowId of allRowIds) { set(isRowSelectedFamilyState(rowId), false); } } }, [ allRowsSelectedStatusSelector, - tableAllRowIdsState, + recordIndexAllRowIdsState, isRowSelectedFamilyState, ], ); diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSetRecordTableData.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSetRecordTableData.ts index 7a35ef87a6ac..dcfa9e6f5483 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSetRecordTableData.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSetRecordTableData.ts @@ -1,11 +1,11 @@ import { useRecoilCallback } from 'recoil'; -import { recordGroupDefinitionsComponentState } from '@/object-record/record-group/states/recordGroupDefinitionsComponentState'; +import { recordGroupIdsComponentState } from '@/object-record/record-group/states/recordGroupIdsComponentState'; +import { recordIndexAllRowIdsComponentState } from '@/object-record/record-index/states/recordIndexAllRowIdsComponentState'; +import { recordIndexRowIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRowIdsByGroupComponentFamilyState'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { hasUserSelectedAllRowsComponentState } from '@/object-record/record-table/record-table-row/states/hasUserSelectedAllRowsFamilyState'; import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState'; -import { tableAllRowIdsComponentState } from '@/object-record/record-table/states/tableAllRowIdsComponentState'; -import { tableRowIdsByGroupComponentFamilyState } from '@/object-record/record-table/states/tableRowIdsByGroupComponentFamilyState'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; @@ -21,12 +21,12 @@ export const useSetRecordTableData = ({ recordTableId, onEntityCountChange, }: useSetRecordTableDataProps) => { - const tableRowIdsByGroupFamilyState = useRecoilComponentCallbackStateV2( - tableRowIdsByGroupComponentFamilyState, + const recordIndexRowIdsByGroupFamilyState = useRecoilComponentCallbackStateV2( + recordIndexRowIdsByGroupComponentFamilyState, recordTableId, ); - const tableAllRowIdsState = useRecoilComponentCallbackStateV2( - tableAllRowIdsComponentState, + const recordIndexAllRowIdsState = useRecoilComponentCallbackStateV2( + recordIndexAllRowIdsComponentState, recordTableId, ); const isRowSelectedFamilyState = useRecoilComponentCallbackStateV2( @@ -37,8 +37,8 @@ export const useSetRecordTableData = ({ hasUserSelectedAllRowsComponentState, recordTableId, ); - const recordGroupDefinitionsState = useRecoilComponentCallbackStateV2( - recordGroupDefinitionsComponentState, + const recordIndexRecordGroupIdsState = useRecoilComponentCallbackStateV2( + recordGroupIdsComponentState, recordTableId, ); @@ -46,11 +46,11 @@ export const useSetRecordTableData = ({ ({ set, snapshot }) => ({ records, - recordGroupId, + currentRecordGroupId, totalCount, }: { records: T[]; - recordGroupId?: string; + currentRecordGroupId?: string; totalCount?: number; }) => { for (const record of records) { @@ -66,9 +66,9 @@ export const useSetRecordTableData = ({ const currentRowIds = getSnapshotValue( snapshot, - recordGroupId - ? tableRowIdsByGroupFamilyState(recordGroupId) - : tableAllRowIdsState, + currentRecordGroupId + ? recordIndexRowIdsByGroupFamilyState(currentRecordGroupId) + : recordIndexAllRowIdsState, ); const hasUserSelectedAllRows = getSnapshotValue( @@ -76,9 +76,9 @@ export const useSetRecordTableData = ({ hasUserSelectedAllRowsState, ); - const recordGroupDefinitions = getSnapshotValue( + const recordGroupIds = getSnapshotValue( snapshot, - recordGroupDefinitionsState, + recordIndexRecordGroupIdsState, ); const recordIds = records.map((record) => record.id); @@ -90,39 +90,42 @@ export const useSetRecordTableData = ({ } } - if (isDefined(recordGroupId)) { + if (isDefined(currentRecordGroupId)) { // TODO: Hack to store all ids in the same order as the record group definitions // Should be replaced by something more efficient const allRowIds: string[] = []; - set(tableRowIdsByGroupFamilyState(recordGroupId), recordIds); + set( + recordIndexRowIdsByGroupFamilyState(currentRecordGroupId), + recordIds, + ); - for (const recordGroupDefinition of recordGroupDefinitions) { + for (const recordGroupId of recordGroupIds) { const tableRowIdsByGroup = - recordGroupDefinition.id !== recordGroupId + recordGroupId !== currentRecordGroupId ? getSnapshotValue( snapshot, - tableRowIdsByGroupFamilyState(recordGroupDefinition.id), + recordIndexRowIdsByGroupFamilyState(recordGroupId), ) : recordIds; allRowIds.push(...tableRowIdsByGroup); } - set(tableAllRowIdsState, allRowIds); + set(recordIndexAllRowIdsState, allRowIds); } else { - set(tableAllRowIdsState, recordIds); + set(recordIndexAllRowIdsState, recordIds); } onEntityCountChange(totalCount); } }, [ - tableRowIdsByGroupFamilyState, - tableAllRowIdsState, - recordGroupDefinitionsState, + recordIndexRowIdsByGroupFamilyState, + recordIndexAllRowIdsState, + hasUserSelectedAllRowsState, + recordIndexRecordGroupIdsState, onEntityCountChange, isRowSelectedFamilyState, - hasUserSelectedAllRowsState, ], ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableMoveFocus.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableMoveFocus.ts index 7694d5f757af..bfbe8e0050a4 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableMoveFocus.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableMoveFocus.ts @@ -3,9 +3,9 @@ import { useRecoilCallback } from 'recoil'; import { MoveFocusDirection } from '@/object-record/record-table/types/MoveFocusDirection'; import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; +import { recordIndexAllRowIdsComponentState } from '@/object-record/record-index/states/recordIndexAllRowIdsComponentState'; import { numberOfTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/numberOfTableColumnsComponentSelector'; import { softFocusPositionComponentState } from '@/object-record/record-table/states/softFocusPositionComponentState'; -import { tableAllRowIdsComponentState } from '@/object-record/record-table/states/tableAllRowIdsComponentState'; import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; import { useSetSoftFocusPosition } from './internal/useSetSoftFocusPosition'; @@ -17,8 +17,8 @@ export const useRecordTableMoveFocus = (recordTableId?: string) => { recordTableId, ); - const tableAllRowIdsState = useRecoilComponentCallbackStateV2( - tableAllRowIdsComponentState, + const recordIndexAllRowIdsState = useRecoilComponentCallbackStateV2( + recordIndexAllRowIdsComponentState, recordTableId, ); @@ -47,7 +47,7 @@ export const useRecordTableMoveFocus = (recordTableId?: string) => { const moveDown = useRecoilCallback( ({ snapshot }) => () => { - const allRowIds = getSnapshotValue(snapshot, tableAllRowIdsState); + const allRowIds = getSnapshotValue(snapshot, recordIndexAllRowIdsState); const softFocusPosition = getSnapshotValue( snapshot, softFocusPositionState, @@ -64,7 +64,7 @@ export const useRecordTableMoveFocus = (recordTableId?: string) => { row: newRowIndex, }); }, - [tableAllRowIdsState, setSoftFocusPosition, softFocusPositionState], + [recordIndexAllRowIdsState, setSoftFocusPosition, softFocusPositionState], ); const numberOfTableColumnsSelector = useRecoilComponentCallbackStateV2( @@ -75,7 +75,7 @@ export const useRecordTableMoveFocus = (recordTableId?: string) => { const moveRight = useRecoilCallback( ({ snapshot }) => () => { - const allRowIds = getSnapshotValue(snapshot, tableAllRowIdsState); + const allRowIds = getSnapshotValue(snapshot, recordIndexAllRowIdsState); const softFocusPosition = getSnapshotValue( snapshot, softFocusPositionState, @@ -116,7 +116,7 @@ export const useRecordTableMoveFocus = (recordTableId?: string) => { } }, [ - tableAllRowIdsState, + recordIndexAllRowIdsState, softFocusPositionState, numberOfTableColumnsSelector, setSoftFocusPosition, diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyDragDropContext.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyDragDropContext.tsx index 0bcc4ac78cdc..cf3546e1d271 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyDragDropContext.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyDragDropContext.tsx @@ -3,10 +3,10 @@ import { ReactNode, useContext } from 'react'; import { useSetRecoilState } from 'recoil'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; +import { recordIndexAllRowIdsComponentState } from '@/object-record/record-index/states/recordIndexAllRowIdsComponentState'; import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; import { useComputeNewRowPosition } from '@/object-record/record-table/hooks/useComputeNewRowPosition'; import { isRemoveSortingModalOpenState } from '@/object-record/record-table/states/isRemoveSortingModalOpenState'; -import { tableAllRowIdsComponentState } from '@/object-record/record-table/states/tableAllRowIdsComponentState'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; import { isDefined } from '~/utils/isDefined'; @@ -22,8 +22,8 @@ export const RecordTableBodyDragDropContext = ({ objectNameSingular, }); - const tableAllRowIds = useRecoilComponentValueV2( - tableAllRowIdsComponentState, + const allRowIds = useRecoilComponentValueV2( + recordIndexAllRowIdsComponentState, ); const { currentViewWithCombinedFiltersAndSorts } = @@ -43,7 +43,7 @@ export const RecordTableBodyDragDropContext = ({ return; } - const computeResult = computeNewRowPosition(result, tableAllRowIds); + const computeResult = computeNewRowPosition(result, allRowIds); if (!isDefined(computeResult)) { return; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBody.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBody.tsx index f2a765d6d47f..19b13b6de290 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBody.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBody.tsx @@ -1,22 +1,22 @@ +import { recordIndexAllRowIdsComponentState } from '@/object-record/record-index/states/recordIndexAllRowIdsComponentState'; import { RecordTableNoRecordGroupRows } from '@/object-record/record-table/components/RecordTableNoRecordGroupRows'; import { RecordTableBodyDragDropContext } from '@/object-record/record-table/record-table-body/components/RecordTableBodyDragDropContext'; import { RecordTableBodyDroppable } from '@/object-record/record-table/record-table-body/components/RecordTableBodyDroppable'; import { RecordTableBodyLoading } from '@/object-record/record-table/record-table-body/components/RecordTableBodyLoading'; import { RecordTablePendingRow } from '@/object-record/record-table/record-table-row/components/RecordTablePendingRow'; import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState'; -import { tableAllRowIdsComponentState } from '@/object-record/record-table/states/tableAllRowIdsComponentState'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; export const RecordTableNoRecordGroupBody = () => { - const tableAllRowIds = useRecoilComponentValueV2( - tableAllRowIdsComponentState, + const allRowIds = useRecoilComponentValueV2( + recordIndexAllRowIdsComponentState, ); const isRecordTableInitialLoading = useRecoilComponentValueV2( isRecordTableInitialLoadingComponentState, ); - if (isRecordTableInitialLoading && tableAllRowIds.length === 0) { + if (isRecordTableInitialLoading && allRowIds.length === 0) { return ; } diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffect.tsx index 155a0dcc6a4a..d768a67005dd 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffect.tsx @@ -56,7 +56,7 @@ export const RecordTableRecordGroupBodyEffect = () => { if (!loading) { setRecordTableData({ records, - recordGroupId, + currentRecordGroupId: recordGroupId, totalCount, }); } diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffects.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffects.tsx index 9efde27e353e..ee45df94b8a3 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffects.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffects.tsx @@ -1,18 +1,15 @@ import { RecordGroupContext } from '@/object-record/record-group/states/context/RecordGroupContext'; -import { recordGroupDefinitionsComponentState } from '@/object-record/record-group/states/recordGroupDefinitionsComponentState'; +import { recordGroupIdsComponentState } from '@/object-record/record-group/states/recordGroupIdsComponentState'; import { RecordTableRecordGroupBodyEffect } from '@/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffect'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; export const RecordTableRecordGroupBodyEffects = () => { - const recordGroupDefinitions = useRecoilComponentValueV2( - recordGroupDefinitionsComponentState, + const recordGroupIds = useRecoilComponentValueV2( + recordGroupIdsComponentState, ); - return recordGroupDefinitions.map((recordGroupDefinition) => ( - + return recordGroupIds.map((recordGroupId) => ( + )); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupsBody.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupsBody.tsx index d1f6ccb2fe7d..5f1b2dc77162 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupsBody.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupsBody.tsx @@ -1,32 +1,28 @@ -import { useRecordGroups } from '@/object-record/record-group/hooks/useRecordGroups'; import { RecordGroupContext } from '@/object-record/record-group/states/context/RecordGroupContext'; +import { visibleRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector'; +import { recordIndexAllRowIdsComponentState } from '@/object-record/record-index/states/recordIndexAllRowIdsComponentState'; import { RecordTableRecordGroupRows } from '@/object-record/record-table/components/RecordTableRecordGroupRows'; import { RecordTableBodyDragDropContext } from '@/object-record/record-table/record-table-body/components/RecordTableBodyDragDropContext'; import { RecordTableBodyDroppable } from '@/object-record/record-table/record-table-body/components/RecordTableBodyDroppable'; import { RecordTableBodyLoading } from '@/object-record/record-table/record-table-body/components/RecordTableBodyLoading'; import { RecordTablePendingRow } from '@/object-record/record-table/record-table-row/components/RecordTablePendingRow'; import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState'; -import { tableAllRowIdsComponentState } from '@/object-record/record-table/states/tableAllRowIdsComponentState'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; -type RecordTableRecordGroupsBodyProps = { - objectNameSingular: string; -}; - -export const RecordTableRecordGroupsBody = ({ - objectNameSingular, -}: RecordTableRecordGroupsBodyProps) => { - const tableAllRowIds = useRecoilComponentValueV2( - tableAllRowIdsComponentState, +export const RecordTableRecordGroupsBody = () => { + const allRowIds = useRecoilComponentValueV2( + recordIndexAllRowIdsComponentState, ); const isRecordTableInitialLoading = useRecoilComponentValueV2( isRecordTableInitialLoadingComponentState, ); - const { visibleRecordGroups } = useRecordGroups({ objectNameSingular }); + const visibleRecordGroupIds = useRecoilComponentValueV2( + visibleRecordGroupIdsComponentSelector, + ); - if (isRecordTableInitialLoading && tableAllRowIds.length === 0) { + if (isRecordTableInitialLoading && allRowIds.length === 0) { return ; } @@ -34,10 +30,10 @@ export const RecordTableRecordGroupsBody = ({ - {visibleRecordGroups.map((recordGroupDefinition) => ( + {visibleRecordGroupIds.map((recordGroupId) => ( diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/selectors/allRowsSelectedStatusComponentSelector.ts b/packages/twenty-front/src/modules/object-record/record-table/states/selectors/allRowsSelectedStatusComponentSelector.ts index 470b8d9dcb8e..c9d1db2d32ea 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/states/selectors/allRowsSelectedStatusComponentSelector.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/states/selectors/allRowsSelectedStatusComponentSelector.ts @@ -1,7 +1,7 @@ import { selectedRowIdsComponentSelector } from '@/object-record/record-table/states/selectors/selectedRowIdsComponentSelector'; +import { recordIndexAllRowIdsComponentState } from '@/object-record/record-index/states/recordIndexAllRowIdsComponentState'; import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; -import { tableAllRowIdsComponentState } from '@/object-record/record-table/states/tableAllRowIdsComponentState'; import { createComponentSelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentSelectorV2'; import { AllRowsSelectedStatus } from '../../types/AllRowSelectedStatus'; @@ -12,8 +12,9 @@ export const allRowsSelectedStatusComponentSelector = get: ({ instanceId }) => ({ get }) => { - const tableRowIds = get( - tableAllRowIdsComponentState.atomFamily({ + const allRowIds = get( + // TODO: Working because instanceId is the same, but we're not in the same context, should be changed ! + recordIndexAllRowIdsComponentState.atomFamily({ instanceId, }), ); @@ -29,7 +30,7 @@ export const allRowsSelectedStatusComponentSelector = const allRowsSelectedStatus = numberOfSelectedRows === 0 ? 'none' - : selectedRowIds.length === tableRowIds.length + : selectedRowIds.length === allRowIds.length ? 'all' : 'some'; diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/selectors/selectedRowIdsComponentSelector.ts b/packages/twenty-front/src/modules/object-record/record-table/states/selectors/selectedRowIdsComponentSelector.ts index ced5cb600fc4..734056d3fe7c 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/states/selectors/selectedRowIdsComponentSelector.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/states/selectors/selectedRowIdsComponentSelector.ts @@ -1,6 +1,6 @@ +import { recordIndexAllRowIdsComponentState } from '@/object-record/record-index/states/recordIndexAllRowIdsComponentState'; import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState'; import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; -import { tableAllRowIdsComponentState } from '@/object-record/record-table/states/tableAllRowIdsComponentState'; import { createComponentSelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentSelectorV2'; export const selectedRowIdsComponentSelector = createComponentSelectorV2< @@ -11,13 +11,14 @@ export const selectedRowIdsComponentSelector = createComponentSelectorV2< get: ({ instanceId }) => ({ get }) => { - const rowIds = get( - tableAllRowIdsComponentState.atomFamily({ + const allRowIds = get( + // TODO: Working because instanceId is the same, but we're not in the same context, should be changed ! + recordIndexAllRowIdsComponentState.atomFamily({ instanceId, }), ); - return rowIds.filter( + return allRowIds.filter( (rowId) => get( isRowSelectedComponentFamilyState.atomFamily({ diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/selectors/unselectedRowIdsComponentSelector.ts b/packages/twenty-front/src/modules/object-record/record-table/states/selectors/unselectedRowIdsComponentSelector.ts index 1579e41b06c6..00fcba743dc1 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/states/selectors/unselectedRowIdsComponentSelector.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/states/selectors/unselectedRowIdsComponentSelector.ts @@ -1,6 +1,6 @@ +import { recordIndexAllRowIdsComponentState } from '@/object-record/record-index/states/recordIndexAllRowIdsComponentState'; import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState'; import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; -import { tableAllRowIdsComponentState } from '@/object-record/record-table/states/tableAllRowIdsComponentState'; import { createComponentSelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentSelectorV2'; export const unselectedRowIdsComponentSelector = createComponentSelectorV2< @@ -12,7 +12,8 @@ export const unselectedRowIdsComponentSelector = createComponentSelectorV2< ({ instanceId }) => ({ get }) => { const rowIds = get( - tableAllRowIdsComponentState.atomFamily({ + // TODO: Working because instanceId is the same, but we're not in the same context, should be changed ! + recordIndexAllRowIdsComponentState.atomFamily({ instanceId, }), ); diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/tableAllRowIdsComponentState.ts b/packages/twenty-front/src/modules/object-record/record-table/states/tableAllRowIdsComponentState.ts deleted file mode 100644 index e6f8ef4b24af..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-table/states/tableAllRowIdsComponentState.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; -import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; - -export const tableAllRowIdsComponentState = createComponentStateV2({ - key: 'tableAllRowIdsComponentState', - defaultValue: [], - componentInstanceContext: RecordTableComponentInstanceContext, -}); diff --git a/packages/twenty-front/src/modules/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyStateV2.ts b/packages/twenty-front/src/modules/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyStateV2.ts new file mode 100644 index 000000000000..1286621e10ba --- /dev/null +++ b/packages/twenty-front/src/modules/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyStateV2.ts @@ -0,0 +1,30 @@ +import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; +import { ComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/types/ComponentFamilyStateV2'; +import { globalComponentInstanceContextMap } from '@/ui/utilities/state/component-state/utils/globalComponentInstanceContextMap'; +import { SerializableParam, useRecoilState } from 'recoil'; + +export const useRecoilComponentFamilyStateV2 = < + StateType, + FamilyKey extends SerializableParam, +>( + componentState: ComponentFamilyStateV2, + familyKey: FamilyKey, + instanceIdFromProps?: string, +) => { + const componentInstanceContext = globalComponentInstanceContextMap.get( + componentState.key, + ); + + if (!componentInstanceContext) { + throw new Error( + `Instance context for key "${componentState.key}" is not defined`, + ); + } + + const instanceId = useAvailableComponentInstanceIdOrThrow( + componentInstanceContext, + instanceIdFromProps, + ); + + return useRecoilState(componentState.atomFamily({ instanceId, familyKey })); +}; diff --git a/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewGroups.ts b/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewGroups.ts index 384b2628414f..246c03f03f0c 100644 --- a/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewGroups.ts +++ b/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewGroups.ts @@ -20,6 +20,56 @@ export const useSaveCurrentViewGroups = (viewBarComponentId?: string) => { viewBarComponentId, ); + const saveViewGroup = useRecoilCallback( + ({ snapshot }) => + async (viewGroupToSave: ViewGroup) => { + const currentViewId = snapshot + .getLoadable(currentViewIdCallbackState) + .getValue(); + + if (!currentViewId) { + return; + } + + const view = await getViewFromCache(currentViewId); + + if (isUndefinedOrNull(view)) { + return; + } + + const currentViewGroups = view.viewGroups; + + const existingField = currentViewGroups.find( + (currentViewGroup) => + currentViewGroup.fieldValue === viewGroupToSave.fieldValue, + ); + + if (isUndefinedOrNull(existingField)) { + return; + } + + if ( + isDeeplyEqual( + { + position: existingField.position, + isVisible: existingField.isVisible, + }, + { + position: viewGroupToSave.position, + isVisible: viewGroupToSave.isVisible, + }, + ) + ) { + return; + } + + await updateViewGroupRecords([ + { ...viewGroupToSave, id: existingField.id }, + ]); + }, + [currentViewIdCallbackState, getViewFromCache, updateViewGroupRecords], + ); + const saveViewGroups = useRecoilCallback( ({ snapshot }) => async (viewGroupsToSave: ViewGroup[]) => { @@ -91,6 +141,7 @@ export const useSaveCurrentViewGroups = (viewBarComponentId?: string) => { ); return { + saveViewGroup, saveViewGroups, }; }; diff --git a/packages/twenty-front/src/modules/views/utils/mapRecordGroupDefinitionsToViewGroups.ts b/packages/twenty-front/src/modules/views/utils/mapRecordGroupDefinitionsToViewGroups.ts index b9251945195c..0d1647b3e16e 100644 --- a/packages/twenty-front/src/modules/views/utils/mapRecordGroupDefinitionsToViewGroups.ts +++ b/packages/twenty-front/src/modules/views/utils/mapRecordGroupDefinitionsToViewGroups.ts @@ -1,17 +1,9 @@ import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; import { ViewGroup } from '@/views/types/ViewGroup'; +import { recordGroupDefinitionToViewGroup } from '@/views/utils/recordGroupDefinitionToViewGroup'; 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 ?? '', - }), - ); + return groupDefinitions.map(recordGroupDefinitionToViewGroup); }; diff --git a/packages/twenty-front/src/modules/views/utils/mapViewGroupsToRecordGroupDefinitions.ts b/packages/twenty-front/src/modules/views/utils/mapViewGroupsToRecordGroupDefinitions.ts index 7c7aba97abcf..73f4782479be 100644 --- a/packages/twenty-front/src/modules/views/utils/mapViewGroupsToRecordGroupDefinitions.ts +++ b/packages/twenty-front/src/modules/views/utils/mapViewGroupsToRecordGroupDefinitions.ts @@ -1,3 +1,4 @@ +import { v4 } from 'uuid'; import { isDefined } from '~/utils/isDefined'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; @@ -42,16 +43,7 @@ export const mapViewGroupsToRecordGroupDefinitions = ({ ); if (!selectedOption) { - return { - id: 'no-value', - title: 'No Value', - type: RecordGroupDefinitionType.NoValue, - value: null, - position: viewGroup.position, - isVisible: viewGroup.isVisible, - fieldMetadataId: selectFieldMetadataItem.id, - color: 'transparent', - } satisfies RecordGroupDefinition; + return null; } return { @@ -65,8 +57,32 @@ export const mapViewGroupsToRecordGroupDefinitions = ({ isVisible: viewGroup.isVisible, } as RecordGroupDefinition; }) - .filter(isDefined) - .sort((a, b) => a.position - b.position); + .filter(isDefined); - return recordGroupDefinitionsFromViewGroups; + if (selectFieldMetadataItem.isNullable === true) { + const viewGroup = viewGroups.find( + (viewGroup) => viewGroup.fieldValue === '', + ); + + const noValueColumn = { + id: viewGroup?.id ?? v4(), + title: 'No Value', + type: RecordGroupDefinitionType.NoValue, + value: null, + position: + viewGroup?.position ?? + recordGroupDefinitionsFromViewGroups + .map((option) => option.position) + .reduce((a, b) => Math.max(a, b), 0) + 1, + isVisible: viewGroup?.isVisible ?? true, + fieldMetadataId: selectFieldMetadataItem.id, + color: 'transparent', + } satisfies RecordGroupDefinition; + + return [...recordGroupDefinitionsFromViewGroups, noValueColumn]; + } + + return recordGroupDefinitionsFromViewGroups.sort( + (a, b) => a.position - b.position, + ); }; diff --git a/packages/twenty-front/src/modules/views/utils/recordGroupDefinitionToViewGroup.ts b/packages/twenty-front/src/modules/views/utils/recordGroupDefinitionToViewGroup.ts new file mode 100644 index 000000000000..74ae28f5f553 --- /dev/null +++ b/packages/twenty-front/src/modules/views/utils/recordGroupDefinitionToViewGroup.ts @@ -0,0 +1,15 @@ +import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; +import { ViewGroup } from '@/views/types/ViewGroup'; + +export const recordGroupDefinitionToViewGroup = ( + recordGroup: RecordGroupDefinition, +): ViewGroup => { + return { + __typename: 'ViewGroup', + id: recordGroup.id, + fieldMetadataId: recordGroup.fieldMetadataId, + position: recordGroup.position, + isVisible: recordGroup.isVisible ?? true, + fieldValue: recordGroup.value ?? '', + }; +};