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:
+1
-1
@@ -132,7 +132,7 @@ export const AdvancedFilterSubFieldSelectMenu = ({
|
||||
>
|
||||
{compositeFieldTypeIsFilterableByAnySubField && (
|
||||
<SelectableListItem
|
||||
itemId={'-1'}
|
||||
itemId="-1"
|
||||
key={`select-filter-${-1}`}
|
||||
onEnter={() => {
|
||||
handleSelectFilter(fieldMetadataItemUsedInDropdown);
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -32,8 +32,8 @@ export const RecordBoardColumnHeaderAggregateDropdownButton = ({
|
||||
<>
|
||||
<StyledTag
|
||||
text={value ? value.toString() : '-'}
|
||||
color={'transparent'}
|
||||
weight={'regular'}
|
||||
color="transparent"
|
||||
weight="regular"
|
||||
/>
|
||||
{!isDropdownOpen && (
|
||||
<AppTooltip
|
||||
|
||||
+5
-2
@@ -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}
|
||||
/>
|
||||
|
||||
+5
-2
@@ -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}
|
||||
|
||||
+4
-1
@@ -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}
|
||||
|
||||
+2
@@ -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,
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ export const RecordIndexBoardDataLoader = ({
|
||||
objectNameSingular={objectNameSingular}
|
||||
kanbanFieldMetadataItem={recordIndexKanbanFieldMetadataItem}
|
||||
recordBoardId={recordBoardId}
|
||||
columnId={'no-value'}
|
||||
columnId="no-value"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
+6
-3
@@ -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`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+4
-1
@@ -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" />;
|
||||
};
|
||||
|
||||
+2
-2
@@ -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();
|
||||
|
||||
+2
@@ -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,
|
||||
|
||||
+1
-1
@@ -165,7 +165,7 @@ const meta: Meta = {
|
||||
triggerEvent: 'CLICK',
|
||||
}}
|
||||
>
|
||||
<RecordTableComponentInstance recordTableId={'recordTableId'}>
|
||||
<RecordTableComponentInstance recordTableId="recordTableId">
|
||||
<RecordTableBodyContextProvider
|
||||
value={{
|
||||
onOpenTableCell: () => {},
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@ export const RecordTableEmptyStateDisplay = (
|
||||
<Button
|
||||
Icon={props.ButtonIcon}
|
||||
title={props.buttonTitle}
|
||||
variant={'secondary'}
|
||||
variant="secondary"
|
||||
onClick={props.onClick}
|
||||
disabled={props.buttonIsDisabled}
|
||||
/>
|
||||
|
||||
+6
-3
@@ -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}
|
||||
|
||||
+5
-2
@@ -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
|
||||
}
|
||||
|
||||
+1
-1
@@ -91,7 +91,7 @@ export const RecordTableColumnAggregateFooterMenuContent = () => {
|
||||
/>
|
||||
) : null}
|
||||
<MenuItem
|
||||
key={'none'}
|
||||
key="none"
|
||||
onClick={async () => {
|
||||
await updateViewFieldAggregateOperation(null);
|
||||
resetContent();
|
||||
|
||||
+1
-1
@@ -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} />
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user