Support the "Me" filter for workspace members in dashboard widgets and add multi select (#20971)

Fixes https://github.com/twentyhq/twenty/issues/20225

The "Me" filter (current workspace member) worked in view filters but
not in dashboard widget filters — the server never resolved the
placeholder, and the widget side-panel UI had no "Me" option and only
allowed single selection.

Backend: `ChartDataQueryService` now forwards the current workspace
member id (from authContext) into filterValueDependencies, so the shared
filter logic resolves "Me" the same way it does for view filters. Added
unit tests for the converter.

Frontend: new multi-select picker for workspace member filters in the
widget side panel, mirroring the view filter's actor select: search
input, "Me" pinned item, and a multi-select workspace member list.

## Before
<img width="3024" height="1488" alt="CleanShot 2026-05-27 at 17 16
36@2x"
src="https://github.com/user-attachments/assets/b2cff46c-53e5-4e8a-a463-b106daf96c8c"
/>

## After
<img width="3024" height="1488" alt="CleanShot 2026-05-27 at 17 14
05@2x"
src="https://github.com/user-attachments/assets/8b3b5f11-44b9-4ae5-a2f3-9c7a689f4bb2"
/>
This commit is contained in:
Raphaël Bosi
2026-05-28 15:43:47 +02:00
committed by GitHub
parent c3d1af89ae
commit bb4e28904f
8 changed files with 596 additions and 4 deletions
@@ -16,6 +16,7 @@ import {
import { ObjectRecordGroupBy } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { CommonGroupByQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-group-by-query-runner.service';
import { isUserAuthContext } from 'src/engine/core-modules/auth/guards/is-user-auth-context.guard';
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
@@ -91,10 +92,15 @@ export class ChartDataQueryService {
secondaryAxisOrderBy,
splitMultiValueFields,
}: ExecuteGroupByQueryParams): Promise<GroupByRawResult[]> {
const currentWorkspaceMemberId = isUserAuthContext(authContext)
? authContext.workspaceMemberId
: undefined;
const gqlOperationFilter = convertChartFilterToGqlOperationFilter({
filter,
flatFieldMetadataMaps,
userTimezone,
currentWorkspaceMemberId,
});
const primaryGroupByField = getFieldMetadata(
@@ -0,0 +1,95 @@
import {
type ChartFilter,
FieldMetadataType,
ViewFilterOperand,
} from 'twenty-shared/types';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { convertChartFilterToGqlOperationFilter } from 'src/modules/dashboard/chart-data/utils/convert-chart-filter-to-gql-operation-filter.util';
const userTimezone = 'Europe/Paris';
const currentWorkspaceMemberId = 'd9f0e9c0-1a3b-4f6f-9c1f-7b1d6a2c8b11';
const createdByFieldMetadata = {
id: 'created-by-field-id',
name: 'createdBy',
type: FieldMetadataType.RELATION,
universalIdentifier: 'created-by-universal-id',
relationTargetObjectMetadataId: 'workspace-member-object-id',
} as FlatFieldMetadata;
const flatFieldMetadataMaps = {
byUniversalIdentifier: {
[createdByFieldMetadata.universalIdentifier]: createdByFieldMetadata,
},
universalIdentifierById: {
[createdByFieldMetadata.id]: createdByFieldMetadata.universalIdentifier,
},
universalIdentifiersByApplicationId: {},
} as unknown as FlatEntityMaps<FlatFieldMetadata>;
describe('convertChartFilterToGqlOperationFilter', () => {
it('returns an empty filter when no filter is provided', () => {
const result = convertChartFilterToGqlOperationFilter({
filter: undefined,
flatFieldMetadataMaps,
userTimezone,
});
expect(result).toEqual({});
});
it('forwards currentWorkspaceMemberId so the "me" filter resolves to the current workspace member', () => {
const filter: ChartFilter = {
recordFilters: [
{
fieldMetadataId: createdByFieldMetadata.id,
operand: ViewFilterOperand.IS,
value: JSON.stringify({
isCurrentWorkspaceMemberSelected: true,
selectedRecordIds: [],
}),
type: FieldMetadataType.RELATION,
},
],
};
const result = convertChartFilterToGqlOperationFilter({
filter,
flatFieldMetadataMaps,
userTimezone,
currentWorkspaceMemberId,
});
expect(result).toEqual({
[`${createdByFieldMetadata.name}Id`]: {
in: [currentWorkspaceMemberId],
},
});
});
it('discards a "me" filter when no currentWorkspaceMemberId is provided', () => {
const filter: ChartFilter = {
recordFilters: [
{
fieldMetadataId: createdByFieldMetadata.id,
operand: ViewFilterOperand.IS,
value: JSON.stringify({
isCurrentWorkspaceMemberSelected: true,
selectedRecordIds: [],
}),
type: FieldMetadataType.RELATION,
},
],
};
const result = convertChartFilterToGqlOperationFilter({
filter,
flatFieldMetadataMaps,
userTimezone,
});
expect(result).toEqual({});
});
});
@@ -22,12 +22,14 @@ type ConvertChartFilterToGqlOperationFilterParams = {
filter: ChartFilter | undefined;
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
userTimezone: string;
currentWorkspaceMemberId?: string;
};
export const convertChartFilterToGqlOperationFilter = ({
filter,
flatFieldMetadataMaps,
userTimezone,
currentWorkspaceMemberId,
}: ConvertChartFilterToGqlOperationFilterParams): ObjectRecordFilter => {
if (!isDefined(filter)) {
return {};
@@ -81,6 +83,7 @@ export const convertChartFilterToGqlOperationFilter = ({
recordFilterGroups: convertedRecordFilterGroups,
filterValueDependencies: {
timeZone: userTimezone,
currentWorkspaceMemberId,
},
});
};