diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/command-menu/components/AdvancedFilterCommandMenuValueFormCompositeFieldInput.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/command-menu/components/AdvancedFilterCommandMenuValueFormCompositeFieldInput.tsx
index 807d3ded95..435771aa41 100644
--- a/packages/twenty-front/src/modules/object-record/advanced-filter/command-menu/components/AdvancedFilterCommandMenuValueFormCompositeFieldInput.tsx
+++ b/packages/twenty-front/src/modules/object-record/advanced-filter/command-menu/components/AdvancedFilterCommandMenuValueFormCompositeFieldInput.tsx
@@ -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' ? (
+
) : (
{
+ applyObjectFilterDropdownFilterValue('');
+ };
+
const handleRelativeDateFilterChange = (newValue: RelativeDateFilter) => {
applyObjectFilterDropdownFilterValue(stringifyRelativeDateFilter(newValue));
};
@@ -144,6 +148,7 @@ export const AdvancedFilterCommandMenuValueFormInput = ({
);
}
diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterDropdownFilterInput.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterDropdownFilterInput.tsx
index 28ef8b6472..a539ee8116 100644
--- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterDropdownFilterInput.tsx
+++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterDropdownFilterInput.tsx
@@ -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 ? (
+ ) : isActorWorkspaceMemberCompositeFilter ? (
+
+
+
+
+
) : (
))}
diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownActorSelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownActorSelect.tsx
new file mode 100644
index 0000000000..d1d5803d76
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownActorSelect.tsx
@@ -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 && (
+ <>
+
+
+ >
+ )}
+
+ >
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getSubMenuOptions.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getSubMenuOptions.ts
index ea268d59b9..acf8c04fc5 100644
--- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getSubMenuOptions.ts
+++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getSubMenuOptions.ts
@@ -14,6 +14,11 @@ export const getSubMenuOptions = (subMenu: FilterableFieldType | null) => {
icon: 'IconId',
type: 'ACTOR',
},
+ {
+ name: 'Workspace Member',
+ icon: 'IconUserCircle',
+ type: 'WORKSPACE_MEMBER',
+ },
];
default:
return [];
diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/isFilterOnActorWorkspaceMemberSubField.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/isFilterOnActorWorkspaceMemberSubField.ts
new file mode 100644
index 0000000000..53683738cc
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/isFilterOnActorWorkspaceMemberSubField.ts
@@ -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);
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/components/FormFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/components/FormFieldInput.tsx
index b718f6ae02..8e8f09ba7b 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/ui/components/FormFieldInput.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-field/ui/components/FormFieldInput.tsx
@@ -59,6 +59,7 @@ type FormFieldInputProps = {
field: Pick, '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 | string
}
+ onClear={onClear}
onChange={onChange}
VariablePicker={VariablePicker}
readonly={readonly}
diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormMultiSelectFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormMultiSelectFieldInput.tsx
index c876bf1313..151e350c69 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormMultiSelectFieldInput.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormMultiSelectFieldInput.tsx
@@ -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';
diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormRelationToOneFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormRelationToOneFieldInput.tsx
index a414859534..6b2af086e1 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormRelationToOneFieldInput.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormRelationToOneFieldInput.tsx
@@ -13,6 +13,7 @@ export type FormRelationToOneFieldInputProps = {
objectNameSingular?: string;
defaultValue?: FieldRelationValue | 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}
diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormSingleRecordPicker.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormSingleRecordPicker.tsx
index ce3585df51..06f3d3074a 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormSingleRecordPicker.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormSingleRecordPicker.tsx
@@ -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) => {
// Prevents the dropdown to open when clicking on the chip
event?.stopPropagation();
- onChange(null);
+ onClear?.();
};
const setRecordPickerSelectedId = useSetRecoilComponentState(
diff --git a/packages/twenty-front/src/modules/object-record/record-filter/constants/IconNameBySubField.ts b/packages/twenty-front/src/modules/object-record/record-filter/constants/IconNameBySubField.ts
index 72c0bb2946..1a278db8d6 100644
--- a/packages/twenty-front/src/modules/object-record/record-filter/constants/IconNameBySubField.ts
+++ b/packages/twenty-front/src/modules/object-record/record-filter/constants/IconNameBySubField.ts
@@ -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',
diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/getRecordFilterOperands.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/getRecordFilterOperands.ts
index 85b8600ee8..e99cb36566 100644
--- a/packages/twenty-front/src/modules/object-record/record-filter/utils/getRecordFilterOperands.ts
+++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/getRecordFilterOperands.ts
@@ -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,
diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts
index f91dd70755..f6524d72dd 100644
--- a/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts
+++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts
@@ -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({
diff --git a/packages/twenty-front/src/modules/settings/data-model/constants/CompositeFieldSubFieldLabel.ts b/packages/twenty-front/src/modules/settings/data-model/constants/CompositeFieldSubFieldLabel.ts
index 60360c3a93..2bd4332522 100644
--- a/packages/twenty-front/src/modules/settings/data-model/constants/CompositeFieldSubFieldLabel.ts
+++ b/packages/twenty-front/src/modules/settings/data-model/constants/CompositeFieldSubFieldLabel.ts
@@ -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]: {
diff --git a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsCompositeFieldTypeConfigs.ts b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsCompositeFieldTypeConfigs.ts
index 07271bf318..19f2ee496c 100644
--- a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsCompositeFieldTypeConfigs.ts
+++ b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsCompositeFieldTypeConfigs.ts
@@ -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,
},
{
diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/filters/components/WorkflowStepFilterValueCompositeInput.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/filters/components/WorkflowStepFilterValueCompositeInput.tsx
index e463d2da7b..e8ef3b4785 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-steps/filters/components/WorkflowStepFilterValueCompositeInput.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-steps/filters/components/WorkflowStepFilterValueCompositeInput.tsx
@@ -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' ? (
-
+ | string
+ }
onChange={onChange}
- disabled={readonly}
+ onClear={onClear}
VariablePicker={WorkflowVariablePicker}
+ readonly={readonly}
/>
) : (
{
+ upsertStepFilterSettings({
+ stepFilterToUpsert: {
+ ...stepFilter,
+ value: '',
+ },
+ });
+ };
+
const handleRelativeDateFilterChange = (
newRelativeDateFilter: RelativeDateFilter,
) => {
@@ -182,6 +191,7 @@ export const WorkflowStepFilterValueInput = ({
);
}
@@ -269,6 +279,7 @@ export const WorkflowStepFilterValueInput = ({
field={field}
defaultValue={stepFilter.value}
onChange={handleValueChange}
+ onClear={handleClearValue}
readonly={readonly}
VariablePicker={WorkflowVariablePicker}
placeholder={t`Enter value`}
diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/opportunity-data-seeds.constant.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/opportunity-data-seeds.constant.ts
index 59dc61e181..0ce21bb5f8 100644
--- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/opportunity-data-seeds.constant.ts
+++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/opportunity-data-seeds.constant.ts
@@ -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;
diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/person-data-seeds.constant.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/person-data-seeds.constant.ts
index 4aa10a95df..2eee11d797 100644
--- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/person-data-seeds.constant.ts
+++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/person-data-seeds.constant.ts
@@ -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
diff --git a/packages/twenty-shared/src/types/RecordGqlOperationFilter.ts b/packages/twenty-shared/src/types/RecordGqlOperationFilter.ts
index d4c612d86e..11d4b3a730 100644
--- a/packages/twenty-shared/src/types/RecordGqlOperationFilter.ts
+++ b/packages/twenty-shared/src/types/RecordGqlOperationFilter.ts
@@ -103,6 +103,7 @@ export type LinksFilter = {
export type ActorFilter = {
name?: StringFilter;
source?: SelectFilter;
+ workspaceMemberId?: UUIDFilter;
};
export type EmailsFilter = {
diff --git a/packages/twenty-shared/src/utils/filter/turnRecordFilterIntoGqlOperationFilter.ts b/packages/twenty-shared/src/utils/filter/turnRecordFilterIntoGqlOperationFilter.ts
index 104d848a6a..e41bb4b1df 100644
--- a/packages/twenty-shared/src/utils/filter/turnRecordFilterIntoGqlOperationFilter.ts
+++ b/packages/twenty-shared/src/utils/filter/turnRecordFilterIntoGqlOperationFilter.ts
@@ -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 {