feat: added workspace member filter for actor fields (#16628)

Closes #16619

This PR adds support for filtering ACTOR fields by `Workspace Member`
subfield, enabling users to filter records by who created them with a
`Me` option.

I replicated the same UX and code structure as the Relation field filter
for consistency.

Each filter selection triggers a server-side GraphQL call. But there is
client-side filtering in `isRecordMatchingFilter.ts` which instantly
filters already-loaded records while the server is fetching new results.
I replicated this behaviour from how `FULL_NAME` and other composite
fields handle filtering.


UX decision that were taken by me (Let me know if changes are needed) : 
- The filter shows comma separated selected workspace member names until
3 members are selected. After that it shows [no of selected members]
workspace members.

<img width="643" height="283" alt="Screenshot 2025-12-17 at 8 01 23 PM"
src="https://github.com/user-attachments/assets/18d524f4-979b-4d92-ad64-aa7b63be7897"
/>
<img width="774" height="309" alt="Screenshot 2025-12-17 at 8 01 33 PM"
src="https://github.com/user-attachments/assets/779f374f-1501-48f9-afa2-a78628e067b1"
/>
<img width="737" height="397" alt="Screenshot 2025-12-17 at 8 01 40 PM"
src="https://github.com/user-attachments/assets/4e8b963e-8a0f-467d-91ae-48bca4e1d842"
/>
<img width="697" height="334" alt="Screenshot 2025-12-17 at 8 01 53 PM"
src="https://github.com/user-attachments/assets/c9a0e4bb-48b4-486b-a4f9-d7c49ff3852c"
/>
<img width="684" height="343" alt="Screenshot 2025-12-17 at 8 02 04 PM"
src="https://github.com/user-attachments/assets/d5c45eba-06a3-40fc-b9d0-d585dc8bc136"
/>

---------

Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
Abhishek Kumar
2026-02-19 22:26:40 +05:30
committed by GitHub
parent 2a670520b9
commit aaa6511007
21 changed files with 413 additions and 33 deletions
@@ -1,9 +1,12 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { AdvancedFilterContext } from '@/object-record/advanced-filter/states/context/AdvancedFilterContext';
import { subFieldNameUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState';
import { FormCountryMultiSelectInput } from '@/object-record/record-field/ui/form-types/components/FormCountryMultiSelectInput';
import { FormMultiSelectFieldInput } from '@/object-record/record-field/ui/form-types/components/FormMultiSelectFieldInput';
import { FormNumberFieldInput } from '@/object-record/record-field/ui/form-types/components/FormNumberFieldInput';
import { FormRelationToOneFieldInput } from '@/object-record/record-field/ui/form-types/components/FormRelationToOneFieldInput';
import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput';
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { CURRENCIES } from '@/settings/data-model/constants/Currencies';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
@@ -22,9 +25,11 @@ const ACTOR_SOURCE_OPTIONS: SelectOption[] = Object.values(
export const AdvancedFilterCommandMenuValueFormCompositeFieldInput = ({
recordFilter,
onChange,
onClear,
}: {
recordFilter: RecordFilter;
onChange: (newValue: JsonValue) => void;
onClear: () => void;
}) => {
const { VariablePicker } = useContext(AdvancedFilterContext);
@@ -96,6 +101,15 @@ export const AdvancedFilterCommandMenuValueFormCompositeFieldInput = ({
readonly={readonly}
VariablePicker={VariablePicker}
/>
) : recordFilter.subFieldName === 'workspaceMemberId' ? (
<FormRelationToOneFieldInput
objectNameSingular={CoreObjectNameSingular.WorkspaceMember}
defaultValue={recordFilter.value}
onChange={onChange}
onClear={onClear}
VariablePicker={VariablePicker}
readonly={readonly}
/>
) : (
<FormTextFieldInput
defaultValue={recordFilter.value}
@@ -19,9 +19,9 @@ import {
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { stringifyRelativeDateFilter } from '@/views/view-filter-value/utils/stringifyRelativeDateFilter';
import { WORKFLOW_TIMEZONE } from '@/workflow/constants/WorkflowTimeZone';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { isObject, isString } from '@sniptt/guards';
import { useContext } from 'react';
import { FieldMetadataType } from 'twenty-shared/types';
@@ -77,6 +77,10 @@ export const AdvancedFilterCommandMenuValueFormInput = ({
}
};
const handleClear = () => {
applyObjectFilterDropdownFilterValue('');
};
const handleRelativeDateFilterChange = (newValue: RelativeDateFilter) => {
applyObjectFilterDropdownFilterValue(stringifyRelativeDateFilter(newValue));
};
@@ -144,6 +148,7 @@ export const AdvancedFilterCommandMenuValueFormInput = ({
<AdvancedFilterCommandMenuValueFormCompositeFieldInput
recordFilter={recordFilter}
onChange={handleChange}
onClear={handleClear}
/>
);
}
@@ -1,3 +1,4 @@
import { ObjectFilterDropdownActorSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownActorSelect';
import { ObjectFilterDropdownOptionSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownOptionSelect';
import { ObjectFilterDropdownRatingInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput';
import { ObjectFilterDropdownRecordSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect';
@@ -14,6 +15,7 @@ import { ObjectFilterDropdownDateTimeInput } from '@/object-record/object-filter
import { ObjectFilterDropdownTextInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownTextInput';
import { subFieldNameUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState';
import { isFilterOnActorSourceSubField } from '@/object-record/object-filter-dropdown/utils/isFilterOnActorSourceSubField';
import { isFilterOnActorWorkspaceMemberSubField } from '@/object-record/object-filter-dropdown/utils/isFilterOnActorWorkspaceMemberSubField';
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
@@ -40,6 +42,9 @@ export const AdvancedFilterDropdownFilterInput = ({
subFieldNameUsedInDropdown,
);
const isActorWorkspaceMemberCompositeFilter =
isFilterOnActorWorkspaceMemberSubField(subFieldNameUsedInDropdown);
return (
<>
{filterType === 'ADDRESS' &&
@@ -64,6 +69,14 @@ export const AdvancedFilterDropdownFilterInput = ({
{filterType === 'ACTOR' &&
(isActorSourceCompositeFilter ? (
<ObjectFilterDropdownSourceSelect dropdownId={filterDropdownId} />
) : isActorWorkspaceMemberCompositeFilter ? (
<DropdownContent
widthInPixels={GenericDropdownContentWidth.ExtraLarge}
>
<ObjectFilterDropdownSearchInput />
<DropdownMenuSeparator />
<ObjectFilterDropdownActorSelect dropdownId={filterDropdownId} />
</DropdownContent>
) : (
<ObjectFilterDropdownTextInput filterDropdownId={filterDropdownId} />
))}
@@ -0,0 +1,173 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { ObjectFilterDropdownRecordPinnedItems } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordPinnedItems';
import { CURRENT_WORKSPACE_MEMBER_SELECTABLE_ITEM_ID } from '@/object-record/object-filter-dropdown/constants/CurrentWorkspaceMemberSelectableItemId';
import { useApplyObjectFilterDropdownFilterValue } from '@/object-record/object-filter-dropdown/hooks/useApplyObjectFilterDropdownFilterValue';
import { useObjectFilterDropdownFilterValue } from '@/object-record/object-filter-dropdown/hooks/useObjectFilterDropdownFilterValue';
import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState';
import { MultipleSelectDropdown } from '@/object-record/select/components/MultipleSelectDropdown';
import { useRecordsForSelect } from '@/object-record/select/hooks/useRecordsForSelect';
import { type SelectableItem } from '@/object-record/select/types/SelectableItem';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { type RelationFilterValue } from '@/views/view-filter-value/types/RelationFilterValue';
import { t } from '@lingui/core/macro';
import {
arrayOfUuidOrVariableSchema,
jsonRelationFilterValueSchema,
} from 'twenty-shared/utils';
import { IconUserCircle } from 'twenty-ui/display';
export const EMPTY_ACTOR_FILTER_VALUE: string = JSON.stringify({
isCurrentWorkspaceMemberSelected: false,
selectedRecordIds: [],
} satisfies RelationFilterValue);
export const MAX_WORKSPACE_MEMBERS_TO_DISPLAY = 3;
type ObjectFilterDropdownActorSelectProps = {
dropdownId: string;
};
export const ObjectFilterDropdownActorSelect = ({
dropdownId,
}: ObjectFilterDropdownActorSelectProps) => {
const { objectFilterDropdownFilterValue } =
useObjectFilterDropdownFilterValue();
const { applyObjectFilterDropdownFilterValue } =
useApplyObjectFilterDropdownFilterValue();
const objectFilterDropdownSearchInput = useRecoilComponentValue(
objectFilterDropdownSearchInputComponentState,
);
const { isCurrentWorkspaceMemberSelected, selectedRecordIds } =
jsonRelationFilterValueSchema
.catch({
isCurrentWorkspaceMemberSelected: false,
selectedRecordIds: arrayOfUuidOrVariableSchema.parse(
objectFilterDropdownFilterValue,
),
})
.parse(objectFilterDropdownFilterValue);
const { loading, filteredSelectedRecords, recordsToSelect, selectedRecords } =
useRecordsForSelect({
searchFilterText: objectFilterDropdownSearchInput,
selectedIds: selectedRecordIds,
objectNameSingular: CoreObjectNameSingular.WorkspaceMember,
limit: 10,
allowRequestsToTwentyIcons: false,
});
const currentWorkspaceMemberSelectableItem: SelectableItem = {
id: CURRENT_WORKSPACE_MEMBER_SELECTABLE_ITEM_ID,
name: 'Me',
isSelected: isCurrentWorkspaceMemberSelected ?? false,
AvatarIcon: IconUserCircle,
};
const pinnedSelectableItems: SelectableItem[] = [
currentWorkspaceMemberSelectableItem,
];
const filteredPinnedSelectableItems = pinnedSelectableItems.filter((item) =>
item.name
.toLowerCase()
.includes(objectFilterDropdownSearchInput.toLowerCase()),
);
const handleMultipleRecordSelectChange = (
itemToSelect: SelectableItem,
isNewSelectedValue: boolean,
) => {
if (loading) {
return;
}
const isItemCurrentWorkspaceMember =
itemToSelect.id === CURRENT_WORKSPACE_MEMBER_SELECTABLE_ITEM_ID;
const selectedRecordIdsWithAddedRecord = [
...selectedRecordIds,
itemToSelect.id,
];
const selectedRecordIdsWithRemovedRecord = selectedRecordIds.filter(
(id) => id !== itemToSelect.id,
);
const newSelectedRecordIds = isItemCurrentWorkspaceMember
? selectedRecordIds
: isNewSelectedValue
? selectedRecordIdsWithAddedRecord
: selectedRecordIdsWithRemovedRecord;
const newIsCurrentWorkspaceMemberSelected = isItemCurrentWorkspaceMember
? isNewSelectedValue
: isCurrentWorkspaceMemberSelected;
const selectedRecordNames = [
...recordsToSelect,
...selectedRecords,
...filteredSelectedRecords,
]
.filter(
(record, index, self) =>
self.findIndex((r) => r.id === record.id) === index,
)
.filter((record) => newSelectedRecordIds.includes(record.id))
.map((record) => record.name);
const selectedPinnedItemNames = newIsCurrentWorkspaceMemberSelected
? [currentWorkspaceMemberSelectableItem.name]
: [];
const selectedItemNames = [
...selectedPinnedItemNames,
...selectedRecordNames,
];
const numberOfSelectedItems = selectedItemNames.length;
const filterDisplayValue =
numberOfSelectedItems > MAX_WORKSPACE_MEMBERS_TO_DISPLAY
? t`${numberOfSelectedItems} workspace members`
: selectedItemNames.join(', ');
const newFilterValue =
newSelectedRecordIds.length > 0 || newIsCurrentWorkspaceMemberSelected
? JSON.stringify({
isCurrentWorkspaceMemberSelected:
newIsCurrentWorkspaceMemberSelected,
selectedRecordIds: newSelectedRecordIds,
} satisfies RelationFilterValue)
: '';
applyObjectFilterDropdownFilterValue(newFilterValue, filterDisplayValue);
};
return (
<>
{filteredPinnedSelectableItems.length > 0 && (
<>
<ObjectFilterDropdownRecordPinnedItems
selectableItems={filteredPinnedSelectableItems}
onChange={handleMultipleRecordSelectChange}
/>
<DropdownMenuSeparator />
</>
)}
<MultipleSelectDropdown
selectableListId="object-filter-actor-select-id"
focusId={dropdownId}
itemsToSelect={recordsToSelect}
filteredSelectedItems={filteredSelectedRecords}
selectedItems={selectedRecords}
onChange={handleMultipleRecordSelectChange}
searchFilter={objectFilterDropdownSearchInput}
loadingItems={loading}
/>
</>
);
};
@@ -14,6 +14,11 @@ export const getSubMenuOptions = (subMenu: FilterableFieldType | null) => {
icon: 'IconId',
type: 'ACTOR',
},
{
name: 'Workspace Member',
icon: 'IconUserCircle',
type: 'WORKSPACE_MEMBER',
},
];
default:
return [];
@@ -0,0 +1,7 @@
import { type FieldActorValue } from '@/object-record/record-field/ui/types/FieldMetadata';
export const isFilterOnActorWorkspaceMemberSubField = (
subFieldName?: string | null | undefined,
) => {
return subFieldName === ('workspaceMemberId' satisfies keyof FieldActorValue);
};
@@ -59,6 +59,7 @@ type FormFieldInputProps = {
field: Pick<FieldDefinition<FieldMetadata>, 'label' | 'metadata' | 'type'>;
defaultValue: JsonValue;
onChange: (value: JsonValue) => void;
onClear?: () => void;
VariablePicker?: VariablePickerComponent;
readonly?: boolean;
placeholder?: string;
@@ -71,6 +72,7 @@ export const FormFieldInput = ({
field,
defaultValue,
onChange,
onClear,
VariablePicker,
readonly,
placeholder,
@@ -234,6 +236,7 @@ export const FormFieldInput = ({
defaultValue={
defaultValue as FieldRelationValue<FieldRelationToOneValue> | string
}
onClear={onClear}
onChange={onChange}
VariablePicker={VariablePicker}
readonly={readonly}
@@ -1,5 +1,5 @@
import { t } from '@lingui/core/macro';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { FormFieldInputContainer } from '@/object-record/record-field/ui/form-types/components/FormFieldInputContainer';
import { FormFieldInputInnerContainer } from '@/object-record/record-field/ui/form-types/components/FormFieldInputInnerContainer';
@@ -13,6 +13,7 @@ export type FormRelationToOneFieldInputProps = {
objectNameSingular?: string;
defaultValue?: FieldRelationValue<FieldRelationToOneValue> | string;
onChange: (value: JsonValue) => void;
onClear?: () => void;
readonly?: boolean;
VariablePicker?: VariablePickerComponent;
};
@@ -27,6 +28,7 @@ export const FormRelationToOneFieldInput = ({
label,
objectNameSingular,
onChange,
onClear,
defaultValue,
readonly,
VariablePicker,
@@ -41,6 +43,7 @@ export const FormRelationToOneFieldInput = ({
: defaultValue
}
onChange={onChange}
onClear={onClear}
objectNameSingulars={[objectNameSingular]}
disabled={readonly}
VariablePicker={VariablePicker}
@@ -63,6 +63,7 @@ export type FormSingleRecordPickerProps = {
label?: string;
defaultValue?: RecordId | Variable;
onChange: (value: RecordId | Variable | null) => void;
onClear?: () => void;
objectNameSingulars: string[];
disabled?: boolean;
testId?: string;
@@ -74,6 +75,7 @@ export const FormSingleRecordPicker = ({
defaultValue,
objectNameSingulars,
onChange,
onClear,
disabled,
testId,
VariablePicker,
@@ -127,7 +129,7 @@ export const FormSingleRecordPicker = ({
selectedMorphItem: RecordPickerPickableMorphItem | null | undefined,
) => {
if (!isNonEmptyString(selectedMorphItem?.recordId)) {
onChange(null);
onClear?.();
return;
}
@@ -143,7 +145,7 @@ export const FormSingleRecordPicker = ({
const handleUnlinkVariable = (event?: React.MouseEvent<HTMLDivElement>) => {
// Prevents the dropdown to open when clicking on the chip
event?.stopPropagation();
onChange(null);
onClear?.();
};
const setRecordPickerSelectedId = useSetRecoilComponentState(
@@ -7,6 +7,7 @@ export const ICON_NAME_BY_SUB_FIELD: Partial<
amountMicros: 'IconNumber95Small',
name: 'IconTextSize',
source: 'IconTransferIn',
workspaceMemberId: 'IconUserCircle',
primaryEmail: 'IconMail',
additionalEmails: 'IconMailPlus',
primaryLinkLabel: 'IconTextSize',
@@ -1,4 +1,5 @@
import { isFilterOnActorSourceSubField } from '@/object-record/object-filter-dropdown/utils/isFilterOnActorSourceSubField';
import { isFilterOnActorWorkspaceMemberSubField } from '@/object-record/object-filter-dropdown/utils/isFilterOnActorWorkspaceMemberSubField';
import { type CompositeFieldSubFieldName } from '@/settings/data-model/types/CompositeFieldSubFieldName';
import {
FieldMetadataType,
@@ -201,7 +202,10 @@ export const getRecordFilterOperands = ({
case 'SELECT':
return FILTER_OPERANDS_MAP.SELECT;
case 'ACTOR': {
if (isFilterOnActorSourceSubField(subFieldName)) {
if (
isFilterOnActorSourceSubField(subFieldName) ||
isFilterOnActorWorkspaceMemberSubField(subFieldName)
) {
return [
RecordFilterOperand.IS,
RecordFilterOperand.IS_NOT,
@@ -370,6 +370,13 @@ export const isRecordMatchingFilter = ({
case FieldMetadataType.ACTOR: {
const actorFilter = filterValue as ActorFilter;
if (isDefined(actorFilter.workspaceMemberId)) {
return isMatchingUUIDFilter({
uuidFilter: actorFilter.workspaceMemberId,
value: record[filterKey].workspaceMemberId,
});
}
return (
actorFilter.name === undefined ||
isMatchingStringFilter({
@@ -40,7 +40,7 @@ export const COMPOSITE_FIELD_SUB_FIELD_LABELS: {
[FieldMetadataType.ACTOR]: {
source: 'Source',
name: 'Name',
workspaceMemberId: 'Workspace Member ID',
workspaceMemberId: 'Workspace Member',
context: 'Context',
},
[FieldMetadataType.RICH_TEXT_V2]: {
@@ -464,7 +464,7 @@ export const SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS = {
COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.ACTOR]
.workspaceMemberId,
isImportable: true,
isFilterable: false,
isFilterable: true,
isIncludedInUniqueConstraint: false,
},
{
@@ -1,8 +1,13 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { FormCountryMultiSelectInput } from '@/object-record/record-field/ui/form-types/components/FormCountryMultiSelectInput';
import { FormMultiSelectFieldInput } from '@/object-record/record-field/ui/form-types/components/FormMultiSelectFieldInput';
import { FormNumberFieldInput } from '@/object-record/record-field/ui/form-types/components/FormNumberFieldInput';
import { FormSingleRecordPicker } from '@/object-record/record-field/ui/form-types/components/FormSingleRecordPicker';
import { FormRelationToOneFieldInput } from '@/object-record/record-field/ui/form-types/components/FormRelationToOneFieldInput';
import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput';
import {
type FieldRelationToOneValue,
type FieldRelationValue,
} from '@/object-record/record-field/ui/types/FieldMetadata';
import { CURRENCIES } from '@/settings/data-model/constants/Currencies';
import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/filters/states/context/WorkflowStepFilterContext';
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
@@ -21,9 +26,11 @@ const ACTOR_SOURCE_OPTIONS: SelectOption[] = Object.values(
export const WorkflowStepFilterValueCompositeInput = ({
stepFilter,
onChange,
onClear,
}: {
stepFilter: StepFilter;
onChange: (newValue: JsonValue) => void;
onClear: () => void;
}) => {
const { readonly } = useContext(WorkflowStepFilterContext);
const { type: filterType, compositeFieldSubFieldName: subFieldName } =
@@ -90,12 +97,17 @@ export const WorkflowStepFilterValueCompositeInput = ({
VariablePicker={WorkflowVariablePicker}
/>
) : subFieldName === 'workspaceMemberId' ? (
<FormSingleRecordPicker
objectNameSingulars={['workspaceMember']}
defaultValue={stepFilter.value}
<FormRelationToOneFieldInput
objectNameSingular={CoreObjectNameSingular.WorkspaceMember}
defaultValue={
stepFilter.value as
| FieldRelationValue<FieldRelationToOneValue>
| string
}
onChange={onChange}
disabled={readonly}
onClear={onClear}
VariablePicker={WorkflowVariablePicker}
readonly={readonly}
/>
) : (
<FormTextFieldInput
@@ -93,6 +93,15 @@ export const WorkflowStepFilterValueInput = ({
});
};
const handleClearValue = () => {
upsertStepFilterSettings({
stepFilterToUpsert: {
...stepFilter,
value: '',
},
});
};
const handleRelativeDateFilterChange = (
newRelativeDateFilter: RelativeDateFilter,
) => {
@@ -182,6 +191,7 @@ export const WorkflowStepFilterValueInput = ({
<WorkflowStepFilterValueCompositeInput
stepFilter={stepFilter}
onChange={handleValueChange}
onClear={handleClearValue}
/>
);
}
@@ -269,6 +279,7 @@ export const WorkflowStepFilterValueInput = ({
field={field}
defaultValue={stepFilter.value}
onChange={handleValueChange}
onClear={handleClearValue}
readonly={readonly}
VariablePicker={WorkflowVariablePicker}
placeholder={t`Enter value`}
@@ -1,6 +1,11 @@
import { isDefined } from 'twenty-shared/utils';
import { COMPANY_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/company-data-seeds.constant';
import { PERSON_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/person-data-seeds.constant';
import { WORKSPACE_MEMBER_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/workspace-member-data-seeds.constant';
import {
WORKSPACE_MEMBER_DATA_SEED_IDS,
WORKSPACE_MEMBER_DATA_SEEDS,
} from 'src/engine/workspace-manager/dev-seeder/data/constants/workspace-member-data-seeds.constant';
type OpportunityDataSeed = {
id: string;
@@ -177,7 +182,17 @@ const GENERATE_OPPORTUNITY_SEEDS = (): OpportunityDataSeed[] => {
CLOSE_DATE.setDate(CLOSE_DATE.getDate() + DAYS_AHEAD);
OPPORTUNITY_SEEDS.push({
const workspaceMemberId = Object.values(WORKSPACE_MEMBER_DATA_SEED_IDS)[
INDEX % 4
];
const workspaceMember = WORKSPACE_MEMBER_DATA_SEEDS.find(
(workspaceMember) => workspaceMember.id === workspaceMemberId,
);
const workspaceMemberName = isDefined(workspaceMember)
? `${workspaceMember?.nameFirstName} ${workspaceMember?.nameLastName}`
: 'Unkonwn';
const rawSeed: OpportunityDataSeed = {
id: OPPORTUNITY_DATA_SEED_IDS[`ID_${INDEX}`],
name: TEMPLATE.name,
amountAmountMicros: TEMPLATE.amount * 1000000,
@@ -195,12 +210,22 @@ const GENERATE_OPPORTUNITY_SEEDS = (): OpportunityDataSeed[] => {
] || COMPANY_DATA_SEED_IDS.ID_1,
ownerId: WORKSPACE_MEMBER_DATA_SEED_IDS.TIM,
createdBySource: 'MANUAL',
createdByWorkspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.TIM,
createdByName: 'Tim Cook',
updatedBySource: 'MANUAL',
updatedByWorkspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.TIM,
updatedByName: 'Tim Cook',
});
createdByWorkspaceMemberId: workspaceMemberId,
createdByName: workspaceMemberName,
updatedByWorkspaceMemberId: workspaceMemberId,
updatedByName: workspaceMemberName,
};
const opportunityDataSeedWithSQLColumnOrder: OpportunityDataSeed =
Object.fromEntries(
OPPORTUNITY_DATA_SEED_COLUMNS.map((column) => [
column,
rawSeed[column as keyof OpportunityDataSeed],
]),
) as OpportunityDataSeed;
OPPORTUNITY_SEEDS.push(opportunityDataSeedWithSQLColumnOrder);
}
return OPPORTUNITY_SEEDS;
@@ -1,5 +1,10 @@
import { isDefined } from 'twenty-shared/utils';
import { COMPANY_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/company-data-seeds.constant';
import { WORKSPACE_MEMBER_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/workspace-member-data-seeds.constant';
import {
WORKSPACE_MEMBER_DATA_SEED_IDS,
WORKSPACE_MEMBER_DATA_SEEDS,
} from 'src/engine/workspace-manager/dev-seeder/data/constants/workspace-member-data-seeds.constant';
type PersonDataSeed = {
id: string;
@@ -11,12 +16,12 @@ type PersonDataSeed = {
linkedinLinkPrimaryLinkUrl: string;
jobTitle: string;
companyId: string;
createdBySource: string;
createdByWorkspaceMemberId: string;
createdByName: string;
phonesPrimaryPhoneNumber: string;
phonesPrimaryPhoneCountryCode: string;
phonesPrimaryPhoneCallingCode: string;
createdBySource: string;
createdByWorkspaceMemberId: string;
createdByName: string;
updatedBySource: string;
updatedByWorkspaceMemberId: string;
updatedByName: string;
@@ -33,12 +38,12 @@ export const PERSON_DATA_SEED_COLUMNS: (keyof PersonDataSeed)[] = [
'linkedinLinkPrimaryLinkUrl',
'jobTitle',
'companyId',
'createdBySource',
'createdByWorkspaceMemberId',
'createdByName',
'phonesPrimaryPhoneNumber',
'phonesPrimaryPhoneCountryCode',
'phonesPrimaryPhoneCallingCode',
'createdBySource',
'createdByWorkspaceMemberId',
'createdByName',
'updatedBySource',
'updatedByWorkspaceMemberId',
'updatedByName',
@@ -21654,13 +21659,37 @@ const PERSON_DATA_SEEDS_RAW = [
];
export const PERSON_DATA_SEEDS: PersonDataSeed[] = PERSON_DATA_SEEDS_RAW.map(
(person, index) => ({
...person,
updatedBySource: person.createdBySource,
updatedByWorkspaceMemberId: person.createdByWorkspaceMemberId,
updatedByName: person.createdByName,
position: index + 1,
}),
(person, index) => {
const workspaceMemberId = Object.values(WORKSPACE_MEMBER_DATA_SEED_IDS)[
index % 4
];
const workspaceMember = WORKSPACE_MEMBER_DATA_SEEDS.find(
(workspaceMember) => workspaceMember.id === workspaceMemberId,
);
const workspaceMemberName = isDefined(workspaceMember)
? `${workspaceMember?.nameFirstName} ${workspaceMember?.nameLastName}`
: 'Unkonwn';
const dataSeed: PersonDataSeed = {
...person,
createdBySource: person.createdBySource,
createdByWorkspaceMemberId: workspaceMemberId,
createdByName: workspaceMemberName,
updatedBySource: person.createdBySource,
updatedByWorkspaceMemberId: workspaceMemberId,
updatedByName: workspaceMemberName,
position: index + 1,
};
const personDataSeedWithSQLColumnOrder: PersonDataSeed = Object.fromEntries(
PERSON_DATA_SEED_COLUMNS.map((column) => [
column,
dataSeed[column as keyof PersonDataSeed],
]),
) as PersonDataSeed;
return personDataSeedWithSQLColumnOrder;
},
);
// Map for O(1) lookups by person ID
@@ -103,6 +103,7 @@ export type LinksFilter = {
export type ActorFilter = {
name?: StringFilter;
source?: SelectFilter;
workspaceMemberId?: UUIDFilter;
};
export type EmailsFilter = {
@@ -1153,6 +1153,71 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
}
}
if (subFieldName === 'workspaceMemberId') {
const { isCurrentWorkspaceMemberSelected, selectedRecordIds } =
jsonRelationFilterValueSchema
.catch({
isCurrentWorkspaceMemberSelected: false,
selectedRecordIds: arrayOfUuidOrVariableSchema.parse(
recordFilter.value,
),
})
.parse(recordFilter.value);
const workspaceMemberIds = isCurrentWorkspaceMemberSelected
? [
...selectedRecordIds,
filterValueDependencies?.currentWorkspaceMemberId,
].filter(isDefined)
: selectedRecordIds;
if (!isDefined(workspaceMemberIds) || workspaceMemberIds.length === 0) {
return;
}
switch (recordFilter.operand) {
case RecordFilterOperand.IS:
return {
[correspondingFieldMetadataItem.name]: {
workspaceMemberId: {
in: workspaceMemberIds,
} satisfies UUIDFilter,
},
};
case RecordFilterOperand.IS_NOT: {
return {
or: [
{
not: {
[correspondingFieldMetadataItem.name]: {
workspaceMemberId: {
in: workspaceMemberIds,
} satisfies UUIDFilter,
},
},
},
{
[correspondingFieldMetadataItem.name]: {
workspaceMemberId: {
is: 'NULL',
} satisfies UUIDFilter,
},
},
],
};
}
default: {
const fieldForRecordFilter = fieldMetadataItems.find(
(field) => field.id === recordFilter.fieldMetadataId,
);
throw new Error(
`Unknown operand ${recordFilter.operand} for ${fieldForRecordFilter?.label ?? ''} filter`,
);
}
}
}
switch (recordFilter.operand) {
case RecordFilterOperand.CONTAINS:
return {