Get ready for 1000+ members (#14313)

The goal of this PR is to test if Twenty can support a large number of
members, a question which was raised by a large company that is
considering moving away from Salesforce.

I was expecting the currentWorkspaceMembersState to cause a lot more
issue. It would be very hard to get rid of it in the context of actors,
I think that would require a big refactoring. I thought we'd have to do
it but it turns out the perf are pretty good. One thing we need to
improve is the pagination on the roles page, we'll wait for @Bonapara to
update that
This commit is contained in:
Félix Malfait
2025-09-05 12:37:22 +02:00
committed by GitHub
parent 094d670590
commit d5f88e566c
246 changed files with 5467 additions and 703 deletions
@@ -132,7 +132,7 @@ export const AdvancedFilterSubFieldSelectMenu = ({
>
{compositeFieldTypeIsFilterableByAnySubField && (
<SelectableListItem
itemId={'-1'}
itemId="-1"
key={`select-filter-${-1}`}
onEnter={() => {
handleSelectFilter(fieldMetadataItemUsedInDropdown);
@@ -187,7 +187,7 @@ export const ObjectOptionsDropdownLayoutContent = () => {
{currentView?.type === ViewType.Kanban && (
<>
<SelectableListItem
itemId={'Group'}
itemId="Group"
onEnter={() => {
isDefined(recordGroupFieldMetadata)
? onContentChange('recordGroups')
@@ -209,7 +209,7 @@ export const ObjectOptionsDropdownLayoutContent = () => {
</SelectableListItem>
<SelectableListItem
itemId={'Compact view'}
itemId="Compact view"
onEnter={() => {
setAndPersistIsCompactModeActive(
!isCompactModeActive,
@@ -32,8 +32,8 @@ export const RecordBoardColumnHeaderAggregateDropdownButton = ({
<>
<StyledTag
text={value ? value.toString() : '-'}
color={'transparent'}
weight={'regular'}
color="transparent"
weight="regular"
/>
{!isDropdownOpen && (
<AppTooltip
@@ -4,6 +4,7 @@ import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/c
import { type VariablePickerComponent } from '@/object-record/record-field/ui/form-types/types/VariablePickerComponent';
import { type FieldEmailsValue } from '@/object-record/record-field/ui/types/FieldMetadata';
import { InputLabel } from '@/ui/input/components/InputLabel';
import { useLingui } from '@lingui/react/macro';
type FormEmailsFieldInputProps = {
label?: string;
@@ -20,6 +21,8 @@ export const FormEmailsFieldInput = ({
readonly,
VariablePicker,
}: FormEmailsFieldInputProps) => {
const { t } = useLingui();
const handleChange = (email: string) => {
onChange({
primaryEmail: email,
@@ -32,10 +35,10 @@ export const FormEmailsFieldInput = ({
{label ? <InputLabel>{label}</InputLabel> : null}
<FormNestedFieldInputContainer>
<FormTextFieldInput
label="Primary Email"
label={t`Primary Email`}
defaultValue={defaultValue?.primaryEmail}
onChange={handleChange}
placeholder={'Primary Email'}
placeholder={t`Primary Email`}
readonly={readonly}
VariablePicker={VariablePicker}
/>
@@ -16,6 +16,7 @@ import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
import { css, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { useCallback, useId } from 'react';
import { isDefined, isValidUuid } from 'twenty-shared/utils';
@@ -77,6 +78,8 @@ export const FormSingleRecordPicker = ({
testId,
VariablePicker,
}: FormSingleRecordPickerProps) => {
const { t } = useLingui();
const theme = useTheme();
const draftValue: FormSingleRecordPickerValue = isStandaloneVariableString(
defaultValue,
@@ -177,7 +180,7 @@ export const FormSingleRecordPicker = ({
<Dropdown
dropdownId={dropdownId}
dropdownPlacement="bottom-start"
clickableComponentWidth={'100%'}
clickableComponentWidth="100%"
onClose={handleCloseRelationPickerDropdown}
onOpen={handleOpenDropdown}
dropdownOffset={{ y: parseInt(theme.spacing(1), 10) }}
@@ -207,7 +210,7 @@ export const FormSingleRecordPicker = ({
focusId={dropdownId}
componentInstanceId={dropdownId}
EmptyIcon={IconForbid}
emptyLabel={'No ' + objectNameSingular}
emptyLabel={t`No ${objectNameSingular}`}
onCancel={() => closeDropdown(dropdownId)}
onRecordSelected={handleRecordSelected}
objectNameSingular={objectNameSingular}
@@ -13,11 +13,13 @@ import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { useLingui } from '@lingui/react/macro';
import { useContext } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { IconForbid } from 'twenty-ui/display';
export const RelationToOneFieldInput = () => {
const { t } = useLingui();
const { fieldDefinition, recordId } = useRelationField<ObjectRecord>();
const { onSubmit, onCancel } = useContext(FieldInputEventContext);
@@ -73,13 +75,14 @@ export const RelationToOneFieldInput = () => {
if (isLoading) {
return <></>;
}
const fieldLabel = fieldDefinition.label;
return (
<SingleRecordPicker
focusId={instanceId}
componentInstanceId={instanceId}
EmptyIcon={IconForbid}
emptyLabel={'No ' + fieldDefinition.label}
emptyLabel={t`No ${fieldLabel}`}
onCancel={onCancel}
onCreate={handleCreateNew}
onRecordSelected={handleRecordSelected}
@@ -24,6 +24,7 @@ import { FieldMetadataType } from 'twenty-shared/types';
import { getCanvasElementForDropdownTesting } from 'twenty-ui/testing';
import { RelationToOneFieldInput } from '../RelationToOneFieldInput';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { getFieldInputEventContextProviderWithJestMocks } from './utils/getFieldInputEventContextProviderWithJestMocks';
const RelationWorkspaceSetterEffect = () => {
@@ -140,6 +141,7 @@ const meta: Meta = {
clearMocksDecorator,
ObjectMetadataItemsDecorator,
SnackBarDecorator,
I18nFrontDecorator,
],
parameters: {
clearMocks: true,
@@ -50,7 +50,7 @@ export const RecordIndexBoardDataLoader = ({
objectNameSingular={objectNameSingular}
kanbanFieldMetadataItem={recordIndexKanbanFieldMetadataItem}
recordBoardId={recordBoardId}
columnId={'no-value'}
columnId="no-value"
/>
)}
</>
@@ -3,8 +3,11 @@ import { useRemoveRecordSort } from '@/object-record/record-sort/hooks/useRemove
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useLingui } from '@lingui/react/macro';
export const RecordIndexRemoveSortingModal = () => {
const { t } = useLingui();
const currentRecordSorts = useRecoilComponentValue(
currentRecordSortsComponentState,
);
@@ -24,10 +27,10 @@ export const RecordIndexRemoveSortingModal = () => {
return (
<ConfirmationModal
modalId={RECORD_INDEX_REMOVE_SORTING_MODAL_ID}
title={'Remove sorting?'}
subtitle={'This is required to enable manual row reordering.'}
title={t`Remove sorting?`}
subtitle={t`This is required to enable manual row reordering.`}
onConfirmClick={handleRemoveClick}
confirmButtonText={'Remove Sorting'}
confirmButtonText={t`Remove Sorting`}
/>
);
};
@@ -1,5 +1,8 @@
import { MenuItem } from 'twenty-ui/navigation';
import { useLingui } from '@lingui/react/macro';
export const RecordPickerNoRecordFoundMenuItem = () => {
return <MenuItem disabled text={'No records found'} accent="placeholder" />;
const { t } = useLingui();
return <MenuItem disabled text={t`No records found`} accent="placeholder" />;
};
@@ -103,8 +103,8 @@ export const SingleRecordPickerMenuItems = ({
>
{emptyLabel && (
<SelectableListItem
key={'select-none'}
itemId={'select-none'}
key="select-none"
itemId="select-none"
onEnter={() => {
setSelectedRecordId(undefined);
onRecordSelected();
@@ -12,6 +12,7 @@ import { SingleRecordPicker } from '@/object-record/record-picker/single-record-
import { type SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord';
import { IconUserCircle } from 'twenty-ui/display';
import { ComponentDecorator } from 'twenty-ui/testing';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
const records = allMockPersonRecords.map<SingleRecordPickerRecord>(
(person) => ({
@@ -30,6 +31,7 @@ const meta: Meta<typeof SingleRecordPicker> = {
ComponentDecorator,
ObjectMetadataItemsDecorator,
SnackBarDecorator,
I18nFrontDecorator,
],
args: {
objectNameSingular: CoreObjectNameSingular.WorkspaceMember,
@@ -165,7 +165,7 @@ const meta: Meta = {
triggerEvent: 'CLICK',
}}
>
<RecordTableComponentInstance recordTableId={'recordTableId'}>
<RecordTableComponentInstance recordTableId="recordTableId">
<RecordTableBodyContextProvider
value={{
onOpenTableCell: () => {},
@@ -60,7 +60,7 @@ export const RecordTableEmptyStateDisplay = (
<Button
Icon={props.ButtonIcon}
title={props.buttonTitle}
variant={'secondary'}
variant="secondary"
onClick={props.onClick}
disabled={props.buttonIsDisabled}
/>
@@ -7,10 +7,13 @@ import { RecordTableEmptyStateDisplay } from '@/object-record/record-table/empty
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { useLingui } from '@lingui/react/macro';
import { isDefined } from 'twenty-shared/utils';
import { IconFilterOff } from 'twenty-ui/display';
export const RecordTableEmptyStateSoftDelete = () => {
const { t } = useLingui();
const { objectMetadataItem, objectNameSingular, recordTableId } =
useRecordTableContextOrThrow();
@@ -43,9 +46,9 @@ export const RecordTableEmptyStateSoftDelete = () => {
return (
<RecordTableEmptyStateDisplay
buttonTitle={'Remove Deleted filter'}
subTitle={'No deleted records matching the filter criteria were found.'}
title={`No Deleted ${objectLabelSingular} found`}
buttonTitle={t`Remove Deleted filter`}
subTitle={t`No deleted records matching the filter criteria were found.`}
title={t`No Deleted ${objectLabelSingular} found`}
ButtonIcon={IconFilterOff}
animatedPlaceholderType="noDeletedRecord"
onClick={handleButtonClick}
@@ -3,6 +3,7 @@ import { RecordTableColumnAggregateFooterDropdownContext } from '@/object-record
import { useViewFieldAggregateOperation } from '@/object-record/record-table/record-table-footer/hooks/useViewFieldAggregateOperation';
import { type ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { useLingui } from '@lingui/react/macro';
import { type ReactNode, useContext } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { IconCheck } from 'twenty-ui/display';
@@ -15,6 +16,8 @@ export const RecordTableColumnAggregateFooterAggregateOperationMenuItems = ({
aggregateOperations: ExtendedAggregateOperations[];
children?: ReactNode;
}) => {
const { t } = useLingui();
const {
updateViewFieldAggregateOperation,
currentViewFieldAggregateOperation,
@@ -45,13 +48,13 @@ export const RecordTableColumnAggregateFooterAggregateOperationMenuItems = ({
))}
{children}
<MenuItem
key={'none'}
key="none"
onClick={async () => {
await updateViewFieldAggregateOperation(null);
resetContent();
closeDropdown(dropdownId);
}}
text={'None'}
text={t`None`}
RightIcon={
!isDefined(currentViewFieldAggregateOperation) ? IconCheck : undefined
}
@@ -91,7 +91,7 @@ export const RecordTableColumnAggregateFooterMenuContent = () => {
/>
) : null}
<MenuItem
key={'none'}
key="none"
onClick={async () => {
await updateViewFieldAggregateOperation(null);
resetContent();
@@ -99,7 +99,7 @@ export const RecordTableColumnAggregateFooterValueCell = ({
dropdownId={dropdownId}
/>
{!hasAggregateOperationForViewField && (
<StyledIcon fontWeight={'light'} size={theme.icon.size.sm} />
<StyledIcon fontWeight="light" size={theme.icon.size.sm} />
)}
</>
) : (