Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69ee0bd6b0 | ||
|
|
9c693002d8 |
+1
-9
@@ -62,17 +62,9 @@ export const AdvancedFilterRelationTargetFieldSelectMenu = ({
|
||||
? sourceFieldMetadataItem.relation.targetObjectMetadata.id
|
||||
: null;
|
||||
|
||||
const { filterableFieldMetadataItems: allTargetFields } =
|
||||
const { filterableFieldMetadataItems: relationTargetFields } =
|
||||
useFilterableFieldMetadataItems(targetObjectMetadataId ?? '');
|
||||
|
||||
// The backend supports a single hop only. Exclude many-to-one relations
|
||||
// from the target list so the user can't compose multi-hop traversals
|
||||
// (e.g. Person → Company → ParentCompany) that the dispatcher would
|
||||
// collapse back to a filter-by-id on the intermediate relation.
|
||||
const relationTargetFields = allTargetFields.filter(
|
||||
(field) => !isManyToOneRelationField(field),
|
||||
);
|
||||
|
||||
if (
|
||||
!isDefined(sourceFieldMetadataItem) ||
|
||||
!isManyToOneRelationField(sourceFieldMetadataItem)
|
||||
|
||||
+35
-1
@@ -1,11 +1,14 @@
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
|
||||
import { getRelationObjectMetadataNameSingular } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
|
||||
import { getFieldMetadataItemById } from '@/object-metadata/utils/getFieldMetadataItemById';
|
||||
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 { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector';
|
||||
import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState';
|
||||
import { relationTargetFieldMetadataIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/relationTargetFieldMetadataIdUsedInDropdownComponentState';
|
||||
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { MultipleSelectDropdown } from '@/object-record/select/components/MultipleSelectDropdown';
|
||||
@@ -66,6 +69,13 @@ export const ObjectFilterDropdownRecordSelect = ({
|
||||
currentRecordFiltersComponentState,
|
||||
);
|
||||
|
||||
const relationTargetFieldMetadataIdUsedInDropdown =
|
||||
useAtomComponentStateValue(
|
||||
relationTargetFieldMetadataIdUsedInDropdownComponentState,
|
||||
);
|
||||
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
|
||||
|
||||
const { isCurrentWorkspaceMemberSelected } = jsonRelationFilterValueSchema
|
||||
.catch({
|
||||
isCurrentWorkspaceMemberSelected: false,
|
||||
@@ -79,8 +89,32 @@ export const ObjectFilterDropdownRecordSelect = ({
|
||||
throw new Error('fieldMetadataItemUsedInFilterDropdown is not defined');
|
||||
}
|
||||
|
||||
// For a nested relation filter (e.g. company → accountOwner) the records to
|
||||
// choose from belong to the leaf relation's target object (WorkspaceMember),
|
||||
// not the source relation's object (Company). Only a direct relation filter
|
||||
// (no leaf) falls back to the source field; a leaf that is set but cannot be
|
||||
// resolved must not load records from the source object.
|
||||
const relationTargetFieldMetadataItem = isDefined(
|
||||
relationTargetFieldMetadataIdUsedInDropdown,
|
||||
)
|
||||
? getFieldMetadataItemById({
|
||||
fieldMetadataId: relationTargetFieldMetadataIdUsedInDropdown,
|
||||
objectMetadataItems,
|
||||
}).fieldMetadataItem
|
||||
: undefined;
|
||||
|
||||
const effectiveFieldMetadataItem = isDefined(
|
||||
relationTargetFieldMetadataIdUsedInDropdown,
|
||||
)
|
||||
? relationTargetFieldMetadataItem
|
||||
: fieldMetadataItemUsedInFilterDropdown;
|
||||
|
||||
if (!isDefined(effectiveFieldMetadataItem)) {
|
||||
throw new Error('effectiveFieldMetadataItem is not defined');
|
||||
}
|
||||
|
||||
const objectNameSingular = getRelationObjectMetadataNameSingular({
|
||||
field: fieldMetadataItemUsedInFilterDropdown,
|
||||
field: effectiveFieldMetadataItem,
|
||||
});
|
||||
|
||||
if (!isDefined(objectNameSingular)) {
|
||||
|
||||
+2
@@ -30,6 +30,7 @@ import { CoreEntityCacheService } from 'src/engine/core-entity-cache/services/co
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
||||
import { PrefillLogicFunctionService } from 'src/engine/workspace-manager/standard-objects-prefill-data/services/prefill-logic-function.service';
|
||||
import { SeedOnboardingPrivateViewService } from 'src/engine/workspace-manager/standard-objects-prefill-data/services/seed-onboarding-private-view.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { PreInstalledAppsService } from 'src/engine/core-modules/application/pre-installed-apps/pre-installed-apps.service';
|
||||
import { SdkClientGenerationService } from 'src/engine/core-modules/sdk-client/sdk-client-generation.service';
|
||||
@@ -101,6 +102,7 @@ describe('WorkspaceService', () => {
|
||||
PreInstalledAppsService,
|
||||
SdkClientGenerationService,
|
||||
PrefillLogicFunctionService,
|
||||
SeedOnboardingPrivateViewService,
|
||||
WorkspaceMigrationValidateBuildAndRunService,
|
||||
UpgradeMigrationService,
|
||||
UpgradeSequenceReaderService,
|
||||
|
||||
+28
@@ -65,6 +65,7 @@ import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage
|
||||
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
|
||||
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
||||
import { PrefillLogicFunctionService } from 'src/engine/workspace-manager/standard-objects-prefill-data/services/prefill-logic-function.service';
|
||||
import { SeedOnboardingPrivateViewService } from 'src/engine/workspace-manager/standard-objects-prefill-data/services/seed-onboarding-private-view.service';
|
||||
import { prefillCompanies } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-companies.util';
|
||||
import { prefillDashboards } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-dashboards.util';
|
||||
import { prefillOpportunities } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-opportunities.util';
|
||||
@@ -122,6 +123,7 @@ export class WorkspaceService extends TypeOrmQueryService<WorkspaceEntity> {
|
||||
private readonly userWorkspaceService: UserWorkspaceService,
|
||||
private readonly twentyConfigService: TwentyConfigService,
|
||||
private readonly exceptionHandlerService: ExceptionHandlerService,
|
||||
private readonly seedOnboardingPrivateViewService: SeedOnboardingPrivateViewService,
|
||||
private readonly permissionsService: PermissionsService,
|
||||
private readonly dnsManagerService: DnsManagerService,
|
||||
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
@@ -366,9 +368,16 @@ export class WorkspaceService extends TypeOrmQueryService<WorkspaceEntity> {
|
||||
|
||||
await this.userWorkspaceService.createWorkspaceMember(workspace.id, user);
|
||||
|
||||
const creatorUserWorkspace =
|
||||
await this.userWorkspaceRepository.findOneByOrFail({
|
||||
userId: user.id,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
await this.prefillCreatedWorkspaceRecords({
|
||||
workspaceId: workspace.id,
|
||||
schemaName: getWorkspaceSchemaName(workspace.id),
|
||||
createdByUserWorkspaceId: creatorUserWorkspace.id,
|
||||
});
|
||||
|
||||
await this.activateAndInitializeUpgradeState({
|
||||
@@ -785,9 +794,11 @@ export class WorkspaceService extends TypeOrmQueryService<WorkspaceEntity> {
|
||||
private async prefillCreatedWorkspaceRecords({
|
||||
workspaceId,
|
||||
schemaName,
|
||||
createdByUserWorkspaceId,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
schemaName: string;
|
||||
createdByUserWorkspaceId: string;
|
||||
}): Promise<void> {
|
||||
const {
|
||||
flatObjectMetadataMaps,
|
||||
@@ -851,6 +862,23 @@ export class WorkspaceService extends TypeOrmQueryService<WorkspaceEntity> {
|
||||
await queryRunner.release();
|
||||
}
|
||||
|
||||
// Seed a private onboarding kanban for the workspace creator alongside the
|
||||
// other workspace records. Best-effort: a failure must never block
|
||||
// activation. Runs after the record transaction because views are metadata
|
||||
// created through their own migrations.
|
||||
try {
|
||||
await this.seedOnboardingPrivateViewService.seedForWorkspace({
|
||||
workspaceId,
|
||||
createdByUserWorkspaceId,
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`failed to seed onboarding private view for workspace ${workspaceId}`,
|
||||
error,
|
||||
);
|
||||
this.exceptionHandlerService.captureExceptions([error as Error]);
|
||||
}
|
||||
|
||||
try {
|
||||
await prefillWorkflowCommandMenuItems({
|
||||
workspaceId,
|
||||
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
|
||||
import {
|
||||
AggregateOperations,
|
||||
ViewFilterOperand,
|
||||
ViewType,
|
||||
ViewVisibility,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { ViewFieldService } from 'src/engine/metadata-modules/view-field/services/view-field.service';
|
||||
import { ViewFilterService } from 'src/engine/metadata-modules/view-filter/services/view-filter.service';
|
||||
import { ViewService } from 'src/engine/metadata-modules/view/services/view.service';
|
||||
import { SeedOnboardingPrivateViewService } from 'src/engine/workspace-manager/standard-objects-prefill-data/services/seed-onboarding-private-view.service';
|
||||
|
||||
const opportunityStandardObject = STANDARD_OBJECTS.opportunity;
|
||||
const companyStandardObject = STANDARD_OBJECTS.company;
|
||||
|
||||
const FIELD_METADATA_IDS = {
|
||||
stage: 'stage-field-id',
|
||||
name: 'name-field-id',
|
||||
amount: 'amount-field-id',
|
||||
createdBy: 'created-by-field-id',
|
||||
closeDate: 'close-date-field-id',
|
||||
company: 'company-field-id',
|
||||
pointOfContact: 'point-of-contact-field-id',
|
||||
accountOwner: 'account-owner-field-id',
|
||||
};
|
||||
|
||||
const OPPORTUNITY_OBJECT_METADATA_ID = 'opportunity-object-id';
|
||||
const SEEDED_VIEW_ID = 'seeded-view-id';
|
||||
const WORKSPACE_ID = 'workspace-id';
|
||||
const CREATED_BY_USER_WORKSPACE_ID = 'user-workspace-id';
|
||||
|
||||
const flatObjectMetadataMaps = {
|
||||
byUniversalIdentifier: {
|
||||
[opportunityStandardObject.universalIdentifier]: {
|
||||
id: OPPORTUNITY_OBJECT_METADATA_ID,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const flatFieldMetadataMaps = {
|
||||
byUniversalIdentifier: {
|
||||
[opportunityStandardObject.fields.stage.universalIdentifier]: {
|
||||
id: FIELD_METADATA_IDS.stage,
|
||||
},
|
||||
[opportunityStandardObject.fields.name.universalIdentifier]: {
|
||||
id: FIELD_METADATA_IDS.name,
|
||||
},
|
||||
[opportunityStandardObject.fields.amount.universalIdentifier]: {
|
||||
id: FIELD_METADATA_IDS.amount,
|
||||
},
|
||||
[opportunityStandardObject.fields.createdBy.universalIdentifier]: {
|
||||
id: FIELD_METADATA_IDS.createdBy,
|
||||
},
|
||||
[opportunityStandardObject.fields.closeDate.universalIdentifier]: {
|
||||
id: FIELD_METADATA_IDS.closeDate,
|
||||
},
|
||||
[opportunityStandardObject.fields.company.universalIdentifier]: {
|
||||
id: FIELD_METADATA_IDS.company,
|
||||
},
|
||||
[opportunityStandardObject.fields.pointOfContact.universalIdentifier]: {
|
||||
id: FIELD_METADATA_IDS.pointOfContact,
|
||||
},
|
||||
[companyStandardObject.fields.accountOwner.universalIdentifier]: {
|
||||
id: FIELD_METADATA_IDS.accountOwner,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('SeedOnboardingPrivateViewService', () => {
|
||||
let service: SeedOnboardingPrivateViewService;
|
||||
let viewService: jest.Mocked<ViewService>;
|
||||
let viewFieldService: jest.Mocked<ViewFieldService>;
|
||||
let viewFilterService: jest.Mocked<ViewFilterService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
SeedOnboardingPrivateViewService,
|
||||
{
|
||||
provide: ViewService,
|
||||
useValue: {
|
||||
createOne: jest.fn().mockResolvedValue({ id: SEEDED_VIEW_ID }),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: ViewFieldService,
|
||||
useValue: { createMany: jest.fn().mockResolvedValue([]) },
|
||||
},
|
||||
{
|
||||
provide: ViewFilterService,
|
||||
useValue: { createOne: jest.fn().mockResolvedValue({}) },
|
||||
},
|
||||
{
|
||||
provide: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
useValue: {
|
||||
getOrRecomputeManyOrAllFlatEntityMaps: jest.fn().mockResolvedValue({
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get(SeedOnboardingPrivateViewService);
|
||||
viewService = module.get(ViewService);
|
||||
viewFieldService = module.get(ViewFieldService);
|
||||
viewFilterService = module.get(ViewFilterService);
|
||||
});
|
||||
|
||||
it('creates a private kanban view grouped by stage for the workspace creator', async () => {
|
||||
await service.seedForWorkspace({
|
||||
workspaceId: WORKSPACE_ID,
|
||||
createdByUserWorkspaceId: CREATED_BY_USER_WORKSPACE_ID,
|
||||
});
|
||||
|
||||
expect(viewService.createOne).toHaveBeenCalledWith({
|
||||
workspaceId: WORKSPACE_ID,
|
||||
createdByUserWorkspaceId: CREATED_BY_USER_WORKSPACE_ID,
|
||||
createViewInput: expect.objectContaining({
|
||||
name: 'My Pipeline',
|
||||
objectMetadataId: OPPORTUNITY_OBJECT_METADATA_ID,
|
||||
type: ViewType.KANBAN,
|
||||
mainGroupByFieldMetadataId: FIELD_METADATA_IDS.stage,
|
||||
kanbanAggregateOperation: AggregateOperations.SUM,
|
||||
kanbanAggregateOperationFieldMetadataId: FIELD_METADATA_IDS.amount,
|
||||
visibility: ViewVisibility.UNLISTED,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it('seeds the standard kanban card fields on the new view', async () => {
|
||||
await service.seedForWorkspace({
|
||||
workspaceId: WORKSPACE_ID,
|
||||
createdByUserWorkspaceId: CREATED_BY_USER_WORKSPACE_ID,
|
||||
});
|
||||
|
||||
expect(viewFieldService.createMany).toHaveBeenCalledWith({
|
||||
workspaceId: WORKSPACE_ID,
|
||||
createViewFieldInputs: [
|
||||
FIELD_METADATA_IDS.name,
|
||||
FIELD_METADATA_IDS.amount,
|
||||
FIELD_METADATA_IDS.createdBy,
|
||||
FIELD_METADATA_IDS.closeDate,
|
||||
FIELD_METADATA_IDS.company,
|
||||
FIELD_METADATA_IDS.pointOfContact,
|
||||
].map((fieldMetadataId, position) => ({
|
||||
fieldMetadataId,
|
||||
viewId: SEEDED_VIEW_ID,
|
||||
position,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
})),
|
||||
});
|
||||
});
|
||||
|
||||
it('seeds the nested company.accountOwner = me advanced filter', async () => {
|
||||
await service.seedForWorkspace({
|
||||
workspaceId: WORKSPACE_ID,
|
||||
createdByUserWorkspaceId: CREATED_BY_USER_WORKSPACE_ID,
|
||||
});
|
||||
|
||||
expect(viewFilterService.createOne).toHaveBeenCalledWith({
|
||||
workspaceId: WORKSPACE_ID,
|
||||
createViewFilterInput: {
|
||||
viewId: SEEDED_VIEW_ID,
|
||||
fieldMetadataId: FIELD_METADATA_IDS.company,
|
||||
relationTargetFieldMetadataId: FIELD_METADATA_IDS.accountOwner,
|
||||
operand: ViewFilterOperand.IS,
|
||||
value: { isCurrentWorkspaceMemberSelected: true, selectedRecordIds: [] },
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
|
||||
import {
|
||||
AggregateOperations,
|
||||
ViewFilterOperand,
|
||||
ViewType,
|
||||
ViewVisibility,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util';
|
||||
import { ViewFieldService } from 'src/engine/metadata-modules/view-field/services/view-field.service';
|
||||
import { ViewFilterService } from 'src/engine/metadata-modules/view-filter/services/view-filter.service';
|
||||
import { ViewService } from 'src/engine/metadata-modules/view/services/view.service';
|
||||
|
||||
const SEEDED_VIEW_CARD_FIELD_SIZE = 150;
|
||||
|
||||
// Seeds a private "My Pipeline" kanban for the workspace creator: opportunities
|
||||
// grouped by stage, filtered to the accounts they own
|
||||
// (company.accountOwner = me). It introduces new users to kanban views, nested
|
||||
// relation filters and private (per-user) views through a single example view.
|
||||
@Injectable()
|
||||
export class SeedOnboardingPrivateViewService {
|
||||
constructor(
|
||||
private readonly viewService: ViewService,
|
||||
private readonly viewFieldService: ViewFieldService,
|
||||
private readonly viewFilterService: ViewFilterService,
|
||||
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
) {}
|
||||
|
||||
async seedForWorkspace({
|
||||
workspaceId,
|
||||
createdByUserWorkspaceId,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
createdByUserWorkspaceId: string;
|
||||
}): Promise<void> {
|
||||
const { flatObjectMetadataMaps, flatFieldMetadataMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatObjectMetadataMaps', 'flatFieldMetadataMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const opportunityStandardObject = STANDARD_OBJECTS.opportunity;
|
||||
const companyStandardObject = STANDARD_OBJECTS.company;
|
||||
|
||||
const opportunityObjectMetadataId =
|
||||
findFlatEntityByUniversalIdentifierOrThrow({
|
||||
universalIdentifier: opportunityStandardObject.universalIdentifier,
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
}).id;
|
||||
|
||||
const resolveFieldMetadataId = (universalIdentifier: string) =>
|
||||
findFlatEntityByUniversalIdentifierOrThrow({
|
||||
universalIdentifier,
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
}).id;
|
||||
|
||||
const stageFieldMetadataId = resolveFieldMetadataId(
|
||||
opportunityStandardObject.fields.stage.universalIdentifier,
|
||||
);
|
||||
const nameFieldMetadataId = resolveFieldMetadataId(
|
||||
opportunityStandardObject.fields.name.universalIdentifier,
|
||||
);
|
||||
const amountFieldMetadataId = resolveFieldMetadataId(
|
||||
opportunityStandardObject.fields.amount.universalIdentifier,
|
||||
);
|
||||
const createdByFieldMetadataId = resolveFieldMetadataId(
|
||||
opportunityStandardObject.fields.createdBy.universalIdentifier,
|
||||
);
|
||||
const closeDateFieldMetadataId = resolveFieldMetadataId(
|
||||
opportunityStandardObject.fields.closeDate.universalIdentifier,
|
||||
);
|
||||
const companyFieldMetadataId = resolveFieldMetadataId(
|
||||
opportunityStandardObject.fields.company.universalIdentifier,
|
||||
);
|
||||
const pointOfContactFieldMetadataId = resolveFieldMetadataId(
|
||||
opportunityStandardObject.fields.pointOfContact.universalIdentifier,
|
||||
);
|
||||
const accountOwnerFieldMetadataId = resolveFieldMetadataId(
|
||||
companyStandardObject.fields.accountOwner.universalIdentifier,
|
||||
);
|
||||
|
||||
const view = await this.viewService.createOne({
|
||||
workspaceId,
|
||||
createdByUserWorkspaceId,
|
||||
createViewInput: {
|
||||
name: 'My Pipeline',
|
||||
objectMetadataId: opportunityObjectMetadataId,
|
||||
type: ViewType.KANBAN,
|
||||
icon: 'IconLayoutKanban',
|
||||
position: 3,
|
||||
mainGroupByFieldMetadataId: stageFieldMetadataId,
|
||||
kanbanAggregateOperation: AggregateOperations.SUM,
|
||||
kanbanAggregateOperationFieldMetadataId: amountFieldMetadataId,
|
||||
visibility: ViewVisibility.UNLISTED,
|
||||
},
|
||||
});
|
||||
|
||||
// Card fields mirror the standard "By Stage" kanban so the seeded board
|
||||
// looks complete (createOne does not generate view fields).
|
||||
await this.viewFieldService.createMany({
|
||||
workspaceId,
|
||||
createViewFieldInputs: [
|
||||
nameFieldMetadataId,
|
||||
amountFieldMetadataId,
|
||||
createdByFieldMetadataId,
|
||||
closeDateFieldMetadataId,
|
||||
companyFieldMetadataId,
|
||||
pointOfContactFieldMetadataId,
|
||||
].map((fieldMetadataId, position) => ({
|
||||
fieldMetadataId,
|
||||
viewId: view.id,
|
||||
position,
|
||||
isVisible: true,
|
||||
size: SEEDED_VIEW_CARD_FIELD_SIZE,
|
||||
})),
|
||||
});
|
||||
|
||||
// Nested relation filter: opportunity.company → company.accountOwner = me.
|
||||
await this.viewFilterService.createOne({
|
||||
workspaceId,
|
||||
createViewFilterInput: {
|
||||
viewId: view.id,
|
||||
fieldMetadataId: companyFieldMetadataId,
|
||||
relationTargetFieldMetadataId: accountOwnerFieldMetadataId,
|
||||
operand: ViewFilterOperand.IS,
|
||||
value: { isCurrentWorkspaceMemberSelected: true, selectedRecordIds: [] },
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
+17
-2
@@ -5,8 +5,12 @@ import { FileStorageModule } from 'src/engine/core-modules/file-storage/file-sto
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
|
||||
import { FrontComponentModule } from 'src/engine/metadata-modules/front-component/front-component.module';
|
||||
import { LogicFunctionModule } from 'src/engine/metadata-modules/logic-function/logic-function.module';
|
||||
import { ViewFieldModule } from 'src/engine/metadata-modules/view-field/view-field.module';
|
||||
import { ViewFilterModule } from 'src/engine/metadata-modules/view-filter/view-filter.module';
|
||||
import { ViewModule } from 'src/engine/metadata-modules/view/view.module';
|
||||
import { PrefillFrontComponentService } from 'src/engine/workspace-manager/standard-objects-prefill-data/services/prefill-front-component.service';
|
||||
import { PrefillLogicFunctionService } from 'src/engine/workspace-manager/standard-objects-prefill-data/services/prefill-logic-function.service';
|
||||
import { SeedOnboardingPrivateViewService } from 'src/engine/workspace-manager/standard-objects-prefill-data/services/seed-onboarding-private-view.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -15,8 +19,19 @@ import { PrefillLogicFunctionService } from 'src/engine/workspace-manager/standa
|
||||
FileStorageModule,
|
||||
ApplicationModule,
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
ViewModule,
|
||||
ViewFieldModule,
|
||||
ViewFilterModule,
|
||||
],
|
||||
providers: [
|
||||
PrefillLogicFunctionService,
|
||||
PrefillFrontComponentService,
|
||||
SeedOnboardingPrivateViewService,
|
||||
],
|
||||
exports: [
|
||||
PrefillLogicFunctionService,
|
||||
PrefillFrontComponentService,
|
||||
SeedOnboardingPrivateViewService,
|
||||
],
|
||||
providers: [PrefillLogicFunctionService, PrefillFrontComponentService],
|
||||
exports: [PrefillLogicFunctionService, PrefillFrontComponentService],
|
||||
})
|
||||
export class StandardObjectsPrefillModule {}
|
||||
|
||||
+18
@@ -11,6 +11,18 @@ export const prefillCompanies = async (
|
||||
entityManager: EntityManager,
|
||||
schemaName: string,
|
||||
) => {
|
||||
// The workspace creator is the first member created at activation time;
|
||||
// order by creation so the seeded account owner is deterministically them.
|
||||
const workspaceCreator = await entityManager
|
||||
.createQueryBuilder()
|
||||
.select('id', 'id')
|
||||
.from(`${schemaName}.workspaceMember`, 'workspaceMember')
|
||||
.orderBy('"createdAt"', 'ASC')
|
||||
.limit(1)
|
||||
.getRawOne<{ id: string }>();
|
||||
|
||||
const accountOwnerId = workspaceCreator?.id ?? null;
|
||||
|
||||
await entityManager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
@@ -26,6 +38,7 @@ export const prefillCompanies = async (
|
||||
'addressAddressCountry',
|
||||
'employees',
|
||||
'position',
|
||||
'accountOwnerId',
|
||||
'createdBySource',
|
||||
'createdByWorkspaceMemberId',
|
||||
'createdByName',
|
||||
@@ -47,6 +60,7 @@ export const prefillCompanies = async (
|
||||
addressAddressCountry: 'United States',
|
||||
employees: 5000,
|
||||
position: 1,
|
||||
accountOwnerId,
|
||||
createdBySource: FieldActorSource.SYSTEM,
|
||||
createdByWorkspaceMemberId: null,
|
||||
createdByName: 'System',
|
||||
@@ -66,6 +80,7 @@ export const prefillCompanies = async (
|
||||
addressAddressCountry: 'United States',
|
||||
employees: 1100,
|
||||
position: 2,
|
||||
accountOwnerId,
|
||||
createdBySource: FieldActorSource.SYSTEM,
|
||||
createdByWorkspaceMemberId: null,
|
||||
createdByName: 'System',
|
||||
@@ -85,6 +100,7 @@ export const prefillCompanies = async (
|
||||
addressAddressCountry: 'Ireland',
|
||||
employees: 8000,
|
||||
position: 3,
|
||||
accountOwnerId,
|
||||
createdBySource: FieldActorSource.SYSTEM,
|
||||
createdByWorkspaceMemberId: null,
|
||||
createdByName: 'System',
|
||||
@@ -104,6 +120,7 @@ export const prefillCompanies = async (
|
||||
addressAddressCountry: 'United States',
|
||||
employees: 800,
|
||||
position: 4,
|
||||
accountOwnerId,
|
||||
createdBySource: FieldActorSource.SYSTEM,
|
||||
createdByWorkspaceMemberId: null,
|
||||
createdByName: 'System',
|
||||
@@ -123,6 +140,7 @@ export const prefillCompanies = async (
|
||||
addressAddressCountry: 'United States',
|
||||
employees: 400,
|
||||
position: 5,
|
||||
accountOwnerId,
|
||||
createdBySource: FieldActorSource.SYSTEM,
|
||||
createdByWorkspaceMemberId: null,
|
||||
createdByName: 'System',
|
||||
|
||||
+62
@@ -49,6 +49,12 @@ const fields = [
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Company',
|
||||
},
|
||||
{
|
||||
id: 'f-relation-account-owner',
|
||||
name: 'accountOwner',
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Account Owner',
|
||||
},
|
||||
{
|
||||
id: 'f-bool',
|
||||
name: 'isActive',
|
||||
@@ -1038,5 +1044,61 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
|
||||
expect(result).toHaveProperty('companyId.in');
|
||||
});
|
||||
|
||||
// A relation target field compiles to a single-hop foreign-key compare on
|
||||
// the already-joined table (company.accountOwnerId), never a second
|
||||
// relation block — the backend rejects deeper `company.accountOwner.<field>`
|
||||
// traversals.
|
||||
it('should resolve a relation target field to its foreign key', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: {
|
||||
...makeFilter(
|
||||
'f-relation',
|
||||
RecordFilterOperand.IS,
|
||||
'["550e8400-e29b-41d4-a716-446655440000"]',
|
||||
'RELATION',
|
||||
),
|
||||
relationTargetFieldMetadataId: 'f-relation-account-owner',
|
||||
} as RecordFilter,
|
||||
fieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
company: {
|
||||
accountOwnerId: { in: ['550e8400-e29b-41d4-a716-446655440000'] },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// The seeded "My Pipeline" onboarding view (company.accountOwner = me)
|
||||
// resolves the current workspace member into that same FK compare.
|
||||
it('should resolve a relation target field set to the current workspace member', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies: {
|
||||
...filterValueDependencies,
|
||||
currentWorkspaceMemberId: '11111111-1111-4111-8111-111111111111',
|
||||
},
|
||||
recordFilter: {
|
||||
...makeFilter(
|
||||
'f-relation',
|
||||
RecordFilterOperand.IS,
|
||||
JSON.stringify({
|
||||
isCurrentWorkspaceMemberSelected: true,
|
||||
selectedRecordIds: [],
|
||||
}),
|
||||
'RELATION',
|
||||
),
|
||||
relationTargetFieldMetadataId: 'f-relation-account-owner',
|
||||
} as RecordFilter,
|
||||
fieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
company: {
|
||||
accountOwnerId: { in: ['11111111-1111-4111-8111-111111111111'] },
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user