dashboard workspace standard seeds (#16962)

# Introduction

In this pull request we're introducing new standard page layout, tabs
and widget ( 1 page layout, 1 tab and 8 widgets ) and also a new
opportunity field

Also now prefilling new records, 6 opportunities and a dashboard.

## Standard declaration
### New workspace creation
Relies on existing standard declaration builder

### Backfill command
We've been hacking through the standard builder in order to extract only
the standard page layout entities, updated their entity dependencies to
match the workspace ids so the validation passes


## Remark
- Refactored the `PageLayoutWidget` configuration type to be dynamically
typed through a generic discriminated union

---------

Co-authored-by: prastoin <paul@twenty.com>
This commit is contained in:
nitin
2026-01-12 15:04:15 +05:30
committed by GitHub
parent 89af749b94
commit 46e5420d20
63 changed files with 4930 additions and 101 deletions
@@ -12,6 +12,7 @@ type OpportunityDataSeed = {
position: number;
pointOfContactId: string;
companyId: string;
ownerId: string;
createdBySource: string;
createdByWorkspaceMemberId: string;
createdByName: string;
@@ -30,6 +31,7 @@ export const OPPORTUNITY_DATA_SEED_COLUMNS: (keyof OpportunityDataSeed)[] = [
'position',
'pointOfContactId',
'companyId',
'ownerId',
'createdBySource',
'createdByWorkspaceMemberId',
'createdByName',
@@ -191,6 +193,7 @@ const GENERATE_OPPORTUNITY_SEEDS = (): OpportunityDataSeed[] => {
COMPANY_DATA_SEED_IDS[
`ID_${Math.ceil(INDEX / 2)}` as keyof typeof COMPANY_DATA_SEED_IDS
] || 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',
@@ -0,0 +1,65 @@
import { FieldActorSource } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type EntityManager } from 'typeorm';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
import { type FlatPageLayout } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout.type';
import { STANDARD_PAGE_LAYOUTS } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-page-layout.constant';
export const MY_FIRST_DASHBOARD_ID = 'f31ecf3b-87d3-4e8a-a84b-b6f0f3f8c7e2';
export const prefillDashboards = async (
entityManager: EntityManager,
schemaName: string,
flatPageLayoutMaps: FlatEntityMaps<FlatPageLayout>,
) => {
const myFirstDashboardPageLayout = findFlatEntityByUniversalIdentifier({
flatEntityMaps: flatPageLayoutMaps,
universalIdentifier:
STANDARD_PAGE_LAYOUTS.myFirstDashboard.universalIdentifier,
});
if (!isDefined(myFirstDashboardPageLayout)) {
throw new Error(
`Page layout with universalIdentifier '${STANDARD_PAGE_LAYOUTS.myFirstDashboard.universalIdentifier}' not found`,
);
}
await entityManager
.createQueryBuilder()
.insert()
.into(`${schemaName}.dashboard`, [
'id',
'title',
'pageLayoutId',
'position',
'createdBySource',
'createdByWorkspaceMemberId',
'createdByName',
'createdByContext',
'updatedBySource',
'updatedByWorkspaceMemberId',
'updatedByName',
'updatedByContext',
])
.orIgnore()
.values([
{
id: MY_FIRST_DASHBOARD_ID,
title: 'My First Dashboard',
pageLayoutId: myFirstDashboardPageLayout.id,
position: 0,
createdBySource: FieldActorSource.SYSTEM,
createdByWorkspaceMemberId: null,
createdByName: 'System',
createdByContext: {},
updatedBySource: FieldActorSource.SYSTEM,
updatedByWorkspaceMemberId: null,
updatedByName: 'System',
updatedByContext: {},
},
])
.returning('*')
.execute();
};
@@ -0,0 +1,179 @@
import { FieldActorSource } from 'twenty-shared/types';
import { type EntityManager } from 'typeorm';
import {
AIRBNB_ID,
ANTHROPIC_ID,
FIGMA_ID,
NOTION_ID,
STRIPE_ID,
} from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-companies';
import {
BRIAN_CHESKY_ID,
DARIO_AMODEI_ID,
DYLAN_FIELD_ID,
IVAN_ZHAO_ID,
PATRICK_COLLISON_ID,
} from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-people';
export const OPPORTUNITY_STRIPE_PLATFORM_MIGRATION_ID =
'822639e5-9bf7-40f1-8882-a11140362339';
export const OPPORTUNITY_ANTHROPIC_AI_MODEL_ID =
'fc747edc-cb00-4078-8d6b-1fab2611dae4';
export const OPPORTUNITY_NOTION_WORKSPACE_ID =
'75de302f-1044-4957-8da4-1f67ebefd52b';
export const OPPORTUNITY_STRIPE_API_INTEGRATION_ID =
'2beb07b0-340c-41d7-be33-5aa91757f329';
export const OPPORTUNITY_AIRBNB_ENTERPRISE_ID =
'9543adcf-ec03-44e2-9233-3c2d3ebae98a';
export const OPPORTUNITY_FIGMA_DESIGN_ID =
'9457f8e9-16ae-43b9-92ee-cbd21f3dded5';
export const prefillOpportunities = async (
entityManager: EntityManager,
schemaName: string,
) => {
const workspaceMember = await entityManager
.createQueryBuilder()
.select('id')
.from(`${schemaName}.workspaceMember`, 'workspaceMember')
.limit(1)
.getRawOne();
const ownerId = workspaceMember?.id ?? null;
await entityManager
.createQueryBuilder()
.insert()
.into(`${schemaName}.opportunity`, [
'id',
'name',
'amountAmountMicros',
'amountCurrencyCode',
'closeDate',
'stage',
'position',
'companyId',
'pointOfContactId',
'ownerId',
'createdBySource',
'createdByWorkspaceMemberId',
'createdByName',
'updatedBySource',
'updatedByWorkspaceMemberId',
'updatedByName',
])
.orIgnore()
.values([
{
id: OPPORTUNITY_STRIPE_PLATFORM_MIGRATION_ID,
name: 'Platform Migration',
amountAmountMicros: 60000000000,
amountCurrencyCode: 'USD',
closeDate: new Date('2026-01-31T16:25:00.000Z'),
stage: 'PROPOSAL',
position: 1,
companyId: STRIPE_ID,
pointOfContactId: PATRICK_COLLISON_ID,
ownerId,
createdBySource: FieldActorSource.SYSTEM,
createdByWorkspaceMemberId: null,
createdByName: 'System',
updatedBySource: FieldActorSource.SYSTEM,
updatedByWorkspaceMemberId: null,
updatedByName: 'System',
},
{
id: OPPORTUNITY_ANTHROPIC_AI_MODEL_ID,
name: 'AI Model Training',
amountAmountMicros: 100000000000,
amountCurrencyCode: 'USD',
closeDate: new Date('2026-02-15T16:25:00.000Z'),
stage: 'CUSTOMER',
position: 2,
companyId: ANTHROPIC_ID,
pointOfContactId: DARIO_AMODEI_ID,
ownerId,
createdBySource: FieldActorSource.SYSTEM,
createdByWorkspaceMemberId: null,
createdByName: 'System',
updatedBySource: FieldActorSource.SYSTEM,
updatedByWorkspaceMemberId: null,
updatedByName: 'System',
},
{
id: OPPORTUNITY_NOTION_WORKSPACE_ID,
name: 'Workspace Expansion',
amountAmountMicros: 45000000000,
amountCurrencyCode: 'USD',
closeDate: new Date('2026-01-20T16:26:00.000Z'),
stage: 'MEETING',
position: 3,
companyId: NOTION_ID,
pointOfContactId: IVAN_ZHAO_ID,
ownerId,
createdBySource: FieldActorSource.SYSTEM,
createdByWorkspaceMemberId: null,
createdByName: 'System',
updatedBySource: FieldActorSource.SYSTEM,
updatedByWorkspaceMemberId: null,
updatedByName: 'System',
},
{
id: OPPORTUNITY_STRIPE_API_INTEGRATION_ID,
name: 'API Integration Deal',
amountAmountMicros: 75000000000,
amountCurrencyCode: 'USD',
closeDate: new Date('2026-01-25T16:26:00.000Z'),
stage: 'SCREENING',
position: 4,
companyId: STRIPE_ID,
pointOfContactId: PATRICK_COLLISON_ID,
ownerId,
createdBySource: FieldActorSource.SYSTEM,
createdByWorkspaceMemberId: null,
createdByName: 'System',
updatedBySource: FieldActorSource.SYSTEM,
updatedByWorkspaceMemberId: null,
updatedByName: 'System',
},
{
id: OPPORTUNITY_AIRBNB_ENTERPRISE_ID,
name: 'Enterprise Plan Upgrade',
amountAmountMicros: 50000000000,
amountCurrencyCode: 'USD',
closeDate: new Date('2026-03-10T16:26:00.000Z'),
stage: 'NEW',
position: 5,
companyId: AIRBNB_ID,
pointOfContactId: BRIAN_CHESKY_ID,
ownerId,
createdBySource: FieldActorSource.SYSTEM,
createdByWorkspaceMemberId: null,
createdByName: 'System',
updatedBySource: FieldActorSource.SYSTEM,
updatedByWorkspaceMemberId: null,
updatedByName: 'System',
},
{
id: OPPORTUNITY_FIGMA_DESIGN_ID,
name: 'Design Partnership',
amountAmountMicros: 30000000000,
amountCurrencyCode: 'USD',
closeDate: new Date('2026-01-15T16:27:00.000Z'),
stage: 'NEW',
position: 6,
companyId: FIGMA_ID,
pointOfContactId: DYLAN_FIELD_ID,
ownerId,
createdBySource: FieldActorSource.SYSTEM,
createdByWorkspaceMemberId: null,
createdByName: 'System',
updatedBySource: FieldActorSource.SYSTEM,
updatedByWorkspaceMemberId: null,
updatedByName: 'System',
},
])
.returning('*')
.execute();
};
@@ -1,5 +1,5 @@
import { type EntityManager } from 'typeorm';
import { FieldActorSource } from 'twenty-shared/types';
import { type EntityManager } from 'typeorm';
import {
AIRBNB_ID,
@@ -9,6 +9,12 @@ import {
STRIPE_ID,
} from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-companies';
export const BRIAN_CHESKY_ID = 'a2e78a5e-338b-46df-8811-fa08c7d19d35'; // Airbnb
export const DARIO_AMODEI_ID = '93c72d2e-e65c-44c4-99ad-f87f50349dcf'; // Anthropic
export const PATRICK_COLLISON_ID = 'edf6d445-13a7-4373-9a47-8f89e8c0a877'; // Stripe
export const DYLAN_FIELD_ID = 'b1e26fa6-c757-4c88-abfa-4b11f5cf3acf'; // Figma
export const IVAN_ZHAO_ID = '7a93d1e5-3f74-4945-8a65-d7f996083f72'; // Notion
export const prefillPeople = async (
entityManager: EntityManager,
schemaName: string,
@@ -17,6 +23,7 @@ export const prefillPeople = async (
.createQueryBuilder()
.insert()
.into(`${schemaName}.person`, [
'id',
'nameFirstName',
'nameLastName',
'city',
@@ -36,6 +43,7 @@ export const prefillPeople = async (
.orIgnore()
.values([
{
id: BRIAN_CHESKY_ID,
nameFirstName: 'Brian',
nameLastName: 'Chesky',
city: 'San Francisco',
@@ -54,6 +62,7 @@ export const prefillPeople = async (
companyId: AIRBNB_ID,
},
{
id: DARIO_AMODEI_ID,
nameFirstName: 'Dario',
nameLastName: 'Amodei',
city: 'San Francisco',
@@ -72,6 +81,7 @@ export const prefillPeople = async (
companyId: ANTHROPIC_ID,
},
{
id: PATRICK_COLLISON_ID,
nameFirstName: 'Patrick',
nameLastName: 'Collison',
city: 'San Francisco',
@@ -90,6 +100,7 @@ export const prefillPeople = async (
companyId: STRIPE_ID,
},
{
id: DYLAN_FIELD_ID,
nameFirstName: 'Dylan',
nameLastName: 'Field',
city: 'San Francisco',
@@ -108,6 +119,7 @@ export const prefillPeople = async (
companyId: FIGMA_ID,
},
{
id: IVAN_ZHAO_ID,
nameFirstName: 'Ivan',
nameLastName: 'Zhao',
city: 'San Francisco',
@@ -1142,6 +1142,7 @@ export const STANDARD_OBJECTS = {
universalIdentifier: OPPORTUNITY_STANDARD_FIELD_IDS.pointOfContact,
},
company: { universalIdentifier: OPPORTUNITY_STANDARD_FIELD_IDS.company },
owner: { universalIdentifier: '20202020-be7e-4d1e-8e19-3d5c7c4b9f2a' },
favorites: {
universalIdentifier: OPPORTUNITY_STANDARD_FIELD_IDS.favorites,
},
@@ -1944,6 +1945,9 @@ export const STANDARD_OBJECTS = {
assignedTasks: {
universalIdentifier: WORKSPACE_MEMBER_STANDARD_FIELD_IDS.assignedTasks,
},
ownedOpportunities: {
universalIdentifier: '20202020-9e4d-4b3a-8c1f-6d7e8f9a0b1c',
},
favorites: {
universalIdentifier: WORKSPACE_MEMBER_STANDARD_FIELD_IDS.favorites,
},
@@ -0,0 +1,36 @@
export const STANDARD_PAGE_LAYOUTS = {
myFirstDashboard: {
universalIdentifier: '20202020-d001-4d01-8d01-da5ab0a00001',
tabs: {
tab1: {
universalIdentifier: '20202020-d011-4d11-8d11-da5ab0a01001',
widgets: {
welcomeRichText: {
universalIdentifier: '20202020-d111-4d11-8d11-da5ab0a11001',
},
dealsByCompany: {
universalIdentifier: '20202020-d111-4d11-8d11-da5ab0a11002',
},
pipelineValueByStage: {
universalIdentifier: '20202020-d111-4d11-8d11-da5ab0a11003',
},
revenueTimeline: {
universalIdentifier: '20202020-d111-4d11-8d11-da5ab0a11004',
},
opportunitiesByOwner: {
universalIdentifier: '20202020-d111-4d11-8d11-da5ab0a11005',
},
stockMarketIframe: {
universalIdentifier: '20202020-d111-4d11-8d11-da5ab0a11006',
},
dealsCreatedThisMonth: {
universalIdentifier: '20202020-d111-4d11-8d11-da5ab0a11007',
},
dealValueCreatedThisMonth: {
universalIdentifier: '20202020-d111-4d11-8d11-da5ab0a11008',
},
},
},
},
},
} as const;
@@ -11,4 +11,7 @@ export const TWENTY_STANDARD_ALL_METADATA_NAME = [
'role',
'agent',
'skill',
'pageLayout',
'pageLayoutTab',
'pageLayoutWidget',
] as const satisfies AllMetadataName[];
@@ -0,0 +1,3 @@
import { type STANDARD_PAGE_LAYOUTS } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-page-layout.constant';
export type AllStandardPageLayoutName = keyof typeof STANDARD_PAGE_LAYOUTS;
@@ -0,0 +1,6 @@
import { type STANDARD_PAGE_LAYOUTS } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-page-layout.constant';
import { type AllStandardPageLayoutName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-page-layout-name.type';
export type AllStandardPageLayoutTabName<
T extends AllStandardPageLayoutName = AllStandardPageLayoutName,
> = keyof (typeof STANDARD_PAGE_LAYOUTS)[T]['tabs'];
@@ -0,0 +1,12 @@
import { type STANDARD_PAGE_LAYOUTS } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-page-layout.constant';
import { type AllStandardPageLayoutName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-page-layout-name.type';
import { type AllStandardPageLayoutTabName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-page-layout-tab-name.type';
export type AllStandardPageLayoutWidgetName<
PageLayout extends AllStandardPageLayoutName,
PageLayouTab extends AllStandardPageLayoutTabName<PageLayout>,
> = (typeof STANDARD_PAGE_LAYOUTS)[PageLayout]['tabs'][PageLayouTab] extends {
widgets: infer Widgets;
}
? keyof Widgets
: never;
@@ -0,0 +1,40 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`getStandardPageLayoutMetadataRelatedEntityIds should return standard page layout metadata related entity ids 1`] = `
{
"myFirstDashboard": {
"id": "00000000-0000-0000-0000-000000000010",
"tabs": {
"tab1": {
"id": "00000000-0000-0000-0000-000000000009",
"widgets": {
"dealValueCreatedThisMonth": {
"id": "00000000-0000-0000-0000-000000000008",
},
"dealsByCompany": {
"id": "00000000-0000-0000-0000-000000000002",
},
"dealsCreatedThisMonth": {
"id": "00000000-0000-0000-0000-000000000007",
},
"opportunitiesByOwner": {
"id": "00000000-0000-0000-0000-000000000005",
},
"pipelineValueByStage": {
"id": "00000000-0000-0000-0000-000000000003",
},
"revenueTimeline": {
"id": "00000000-0000-0000-0000-000000000004",
},
"stockMarketIframe": {
"id": "00000000-0000-0000-0000-000000000006",
},
"welcomeRichText": {
"id": "00000000-0000-0000-0000-000000000001",
},
},
},
},
},
}
`;
@@ -0,0 +1,25 @@
let uuidCounter = 0;
jest.mock('uuid', () => ({
v4: jest.fn(
() => `00000000-0000-0000-0000-${String(++uuidCounter).padStart(12, '0')}`,
),
}));
import { getStandardObjectMetadataRelatedEntityIds } from 'src/engine/workspace-manager/twenty-standard-application/utils/get-standard-object-metadata-related-entity-ids.util';
describe('getStandardObjectMetadataRelatedEntityIds', () => {
beforeEach(() => {
uuidCounter = 0;
});
afterAll(() => {
jest.restoreAllMocks();
});
it('should return standard object metadata related entity ids', () => {
const result = getStandardObjectMetadataRelatedEntityIds();
expect(result).toMatchSnapshot();
});
});
@@ -0,0 +1,25 @@
let uuidCounter = 0;
jest.mock('uuid', () => ({
v4: jest.fn(
() => `00000000-0000-0000-0000-${String(++uuidCounter).padStart(12, '0')}`,
),
}));
import { getStandardPageLayoutMetadataRelatedEntityIds } from 'src/engine/workspace-manager/twenty-standard-application/utils/get-standard-page-layout-metadata-related-entity-ids.util';
describe('getStandardPageLayoutMetadataRelatedEntityIds', () => {
beforeEach(() => {
uuidCounter = 0;
});
afterAll(() => {
jest.restoreAllMocks();
});
it('should return standard page layout metadata related entity ids', () => {
const result = getStandardPageLayoutMetadataRelatedEntityIds();
expect(result).toMatchSnapshot();
});
});
@@ -442,4 +442,28 @@ export const buildOpportunityStandardFlatFieldMetadatas = ({
twentyStandardApplicationId,
now,
}),
owner: createStandardRelationFieldFlatMetadata({
objectName,
workspaceId,
context: {
type: FieldMetadataType.RELATION,
morphId: null,
fieldName: 'owner',
label: 'Owner',
description: 'Opportunity owner',
icon: 'IconUserCircle',
isNullable: true,
targetObjectName: 'workspaceMember',
targetFieldName: 'ownedOpportunities',
settings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: RelationOnDeleteAction.SET_NULL,
joinColumnName: 'ownerId',
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
});
@@ -596,4 +596,27 @@ export const buildWorkspaceMemberStandardFlatFieldMetadatas = ({
twentyStandardApplicationId,
now,
}),
ownedOpportunities: createStandardRelationFieldFlatMetadata({
objectName,
workspaceId,
context: {
type: FieldMetadataType.RELATION,
morphId: null,
fieldName: 'ownedOpportunities',
label: 'Owned opportunities',
description: 'Opportunities owned by the workspace member',
icon: 'IconTargetArrow',
isNullable: false,
isUIReadOnly: true,
targetObjectName: 'opportunity',
targetFieldName: 'owner',
settings: {
relationType: RelationType.ONE_TO_MANY,
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
});
@@ -0,0 +1,101 @@
import { v4 } from 'uuid';
import { STANDARD_PAGE_LAYOUTS } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-page-layout.constant';
import { type AllStandardPageLayoutName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-page-layout-name.type';
import { type AllStandardPageLayoutTabName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-page-layout-tab-name.type';
import { type AllStandardPageLayoutWidgetName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-page-layout-widget-name.type';
type StandardPageLayoutWidgetIds<
L extends AllStandardPageLayoutName,
T extends AllStandardPageLayoutTabName<L>,
> = Record<AllStandardPageLayoutWidgetName<L, T>, { id: string }>;
type StandardPageLayoutTabIds<L extends AllStandardPageLayoutName> = {
[T in AllStandardPageLayoutTabName<L>]: {
id: string;
widgets: StandardPageLayoutWidgetIds<L, T>;
};
};
export type StandardPageLayoutMetadataRelatedEntityIds = {
[L in AllStandardPageLayoutName]: {
id: string;
tabs: StandardPageLayoutTabIds<L>;
};
};
const computeStandardPageLayoutWidgetIds = <
L extends AllStandardPageLayoutName,
T extends AllStandardPageLayoutTabName<L>,
>({
layoutName,
tabName,
}: {
layoutName: L;
tabName: T;
}): StandardPageLayoutWidgetIds<L, T> => {
// @ts-expect-error legacy generic pattern
const tabDefinition = STANDARD_PAGE_LAYOUTS[layoutName].tabs[tabName] as {
widgets: Record<string, unknown>;
};
const widgetNames = Object.keys(
tabDefinition.widgets,
) as AllStandardPageLayoutWidgetName<L, T>[];
const widgetIds = {} as StandardPageLayoutWidgetIds<L, T>;
for (const widgetName of widgetNames) {
widgetIds[widgetName] = { id: v4() };
}
return widgetIds;
};
const computeStandardPageLayoutTabIds = <L extends AllStandardPageLayoutName>({
layoutName,
}: {
layoutName: L;
}): StandardPageLayoutTabIds<L> => {
const layoutDefinition = STANDARD_PAGE_LAYOUTS[layoutName];
const tabNames = Object.keys(
layoutDefinition.tabs,
) as AllStandardPageLayoutTabName<L>[];
const tabIds = {} as StandardPageLayoutTabIds<L>;
for (const tabName of tabNames) {
const widgetIds = computeStandardPageLayoutWidgetIds({
layoutName,
tabName,
});
tabIds[tabName] = {
id: v4(),
widgets: widgetIds,
};
}
return tabIds;
};
export const getStandardPageLayoutMetadataRelatedEntityIds =
(): StandardPageLayoutMetadataRelatedEntityIds => {
const result = {} as StandardPageLayoutMetadataRelatedEntityIds;
for (const layoutName of Object.keys(
STANDARD_PAGE_LAYOUTS,
) as AllStandardPageLayoutName[]) {
const tabIds = computeStandardPageLayoutTabIds({
layoutName,
});
result[layoutName] = {
id: v4(),
tabs: tabIds,
};
}
return result;
};
@@ -0,0 +1,30 @@
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-maps-or-throw.util';
import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type';
import { type CreateStandardPageLayoutTabArgs } from 'src/engine/workspace-manager/twenty-standard-application/utils/page-layout-tab/create-standard-page-layout-tab-flat-metadata.util';
import { STANDARD_FLAT_PAGE_LAYOUT_TAB_BUILDERS_BY_LAYOUT_NAME } from 'src/engine/workspace-manager/twenty-standard-application/utils/page-layout-tab/standard-flat-page-layout-tab-builders.constant';
export type BuildStandardFlatPageLayoutTabMetadataMapsArgs = Omit<
CreateStandardPageLayoutTabArgs,
'context'
>;
export const buildStandardFlatPageLayoutTabMetadataMaps = (
args: BuildStandardFlatPageLayoutTabMetadataMapsArgs,
): FlatEntityMaps<FlatPageLayoutTab> => {
const allPageLayoutTabMetadatas: FlatPageLayoutTab[] = Object.values(
STANDARD_FLAT_PAGE_LAYOUT_TAB_BUILDERS_BY_LAYOUT_NAME,
).flatMap((builder) => builder(args));
let flatPageLayoutTabMaps = createEmptyFlatEntityMaps();
for (const pageLayoutTabMetadata of allPageLayoutTabMetadatas) {
flatPageLayoutTabMaps = addFlatEntityToFlatEntityMapsOrThrow({
flatEntity: pageLayoutTabMetadata,
flatEntityMaps: flatPageLayoutTabMaps,
});
}
return flatPageLayoutTabMaps;
};
@@ -0,0 +1,68 @@
import { isDefined } from 'class-validator';
import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type';
import { STANDARD_PAGE_LAYOUTS } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-page-layout.constant';
import { type AllStandardPageLayoutName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-page-layout-name.type';
import { type AllStandardPageLayoutTabName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-page-layout-tab-name.type';
import { type StandardPageLayoutMetadataRelatedEntityIds } from 'src/engine/workspace-manager/twenty-standard-application/utils/get-standard-page-layout-metadata-related-entity-ids.util';
export type CreateStandardPageLayoutTabContext<
L extends AllStandardPageLayoutName,
> = {
layoutName: L;
tabName: AllStandardPageLayoutTabName<L>;
title: string;
position: number;
};
export type CreateStandardPageLayoutTabArgs<
L extends AllStandardPageLayoutName = AllStandardPageLayoutName,
> = {
now: string;
workspaceId: string;
twentyStandardApplicationId: string;
standardPageLayoutMetadataRelatedEntityIds: StandardPageLayoutMetadataRelatedEntityIds;
context: CreateStandardPageLayoutTabContext<L>;
};
export const createStandardPageLayoutTabFlatMetadata = <
L extends AllStandardPageLayoutName,
>({
context: { layoutName, tabName, title, position },
workspaceId,
twentyStandardApplicationId,
standardPageLayoutMetadataRelatedEntityIds,
now,
}: CreateStandardPageLayoutTabArgs<L>): FlatPageLayoutTab => {
const layoutIds = standardPageLayoutMetadataRelatedEntityIds[layoutName];
// @ts-expect-error ignore
const tabDefinition = STANDARD_PAGE_LAYOUTS[layoutName].tabs[tabName] as {
universalIdentifier: string;
};
if (!isDefined(tabDefinition)) {
throw new Error(
`Invalid configuration ${layoutName} ${tabName.toString()}`,
);
}
const tabIds = layoutIds.tabs[tabName];
const widgetIds = Object.values(tabIds.widgets).map(
(widget: { id: string }) => widget.id,
);
return {
id: tabIds.id,
universalIdentifier: tabDefinition.universalIdentifier,
applicationId: twentyStandardApplicationId,
workspaceId,
title,
position,
pageLayoutId: layoutIds.id,
widgetIds,
createdAt: now,
updatedAt: now,
deletedAt: null,
};
};
@@ -0,0 +1,27 @@
import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type';
import {} from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-page-layout.constant';
import { type AllStandardPageLayoutName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-page-layout-name.type';
import { type AllStandardPageLayoutTabName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-page-layout-tab-name.type';
import {
type CreateStandardPageLayoutTabArgs,
createStandardPageLayoutTabFlatMetadata,
} from 'src/engine/workspace-manager/twenty-standard-application/utils/page-layout-tab/create-standard-page-layout-tab-flat-metadata.util';
type TabBuilderArgs = Omit<CreateStandardPageLayoutTabArgs, 'context'>;
export const STANDARD_FLAT_PAGE_LAYOUT_TAB_BUILDERS_BY_LAYOUT_NAME = {
tab1: (args: TabBuilderArgs) =>
createStandardPageLayoutTabFlatMetadata({
...args,
context: {
layoutName: 'myFirstDashboard',
tabName: 'tab1',
title: 'Tab 1',
position: 0,
},
}),
} satisfies {
[T in AllStandardPageLayoutTabName<AllStandardPageLayoutName>]: (
args: TabBuilderArgs,
) => FlatPageLayoutTab;
};
@@ -0,0 +1,30 @@
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-maps-or-throw.util';
import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
import { type CreateStandardPageLayoutWidgetArgs } from 'src/engine/workspace-manager/twenty-standard-application/utils/page-layout-widget/create-standard-page-layout-widget-flat-metadata.util';
import { STANDARD_FLAT_PAGE_LAYOUT_WIDGET_BUILDERS } from 'src/engine/workspace-manager/twenty-standard-application/utils/page-layout-widget/standard-flat-page-layout-widget-builders.constant';
export type BuildStandardFlatPageLayoutWidgetMetadataMapsArgs = Omit<
CreateStandardPageLayoutWidgetArgs,
'context'
>;
export const buildStandardFlatPageLayoutWidgetMetadataMaps = (
args: BuildStandardFlatPageLayoutWidgetMetadataMapsArgs,
): FlatEntityMaps<FlatPageLayoutWidget> => {
const allWidgetMetadatas: FlatPageLayoutWidget[] = Object.values(
STANDARD_FLAT_PAGE_LAYOUT_WIDGET_BUILDERS,
).flatMap((builder) => builder(args));
let flatPageLayoutWidgetMaps = createEmptyFlatEntityMaps();
for (const widgetMetadata of allWidgetMetadatas) {
flatPageLayoutWidgetMaps = addFlatEntityToFlatEntityMapsOrThrow({
flatEntity: widgetMetadata,
flatEntityMaps: flatPageLayoutWidgetMaps,
});
}
return flatPageLayoutWidgetMaps;
};
@@ -0,0 +1,91 @@
import { isDefined } from 'class-validator';
import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
import { type WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
import { type AllPageLayoutWidgetConfiguration } from 'src/engine/metadata-modules/page-layout-widget/types/all-page-layout-widget-configuration.type';
import { type GridPosition } from 'src/engine/metadata-modules/page-layout-widget/types/grid-position.type';
import { STANDARD_PAGE_LAYOUTS } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-page-layout.constant';
import { type AllStandardPageLayoutName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-page-layout-name.type';
import { type AllStandardPageLayoutTabName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-page-layout-tab-name.type';
import { type AllStandardPageLayoutWidgetName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-page-layout-widget-name.type';
import { type StandardObjectMetadataRelatedEntityIds } from 'src/engine/workspace-manager/twenty-standard-application/utils/get-standard-object-metadata-related-entity-ids.util';
import { type StandardPageLayoutMetadataRelatedEntityIds } from 'src/engine/workspace-manager/twenty-standard-application/utils/get-standard-page-layout-metadata-related-entity-ids.util';
export type CreateStandardPageLayoutWidgetContext<
L extends AllStandardPageLayoutName,
T extends AllStandardPageLayoutTabName<L>,
> = {
layoutName: L;
tabName: T;
widgetName: AllStandardPageLayoutWidgetName<L, T>;
title: string;
type: WidgetType;
gridPosition: GridPosition;
configuration: AllPageLayoutWidgetConfiguration;
objectMetadataId: string | null;
};
export type CreateStandardPageLayoutWidgetArgs<
L extends AllStandardPageLayoutName = AllStandardPageLayoutName,
T extends AllStandardPageLayoutTabName<L> = AllStandardPageLayoutTabName<L>,
> = {
now: string;
workspaceId: string;
twentyStandardApplicationId: string;
standardObjectMetadataRelatedEntityIds: StandardObjectMetadataRelatedEntityIds;
standardPageLayoutMetadataRelatedEntityIds: StandardPageLayoutMetadataRelatedEntityIds;
context: CreateStandardPageLayoutWidgetContext<L, T>;
};
export const createStandardPageLayoutWidgetFlatMetadata = <
L extends AllStandardPageLayoutName,
T extends AllStandardPageLayoutTabName<L>,
>({
context: {
layoutName,
tabName,
widgetName,
title,
type,
gridPosition,
configuration,
objectMetadataId,
},
workspaceId,
twentyStandardApplicationId,
standardPageLayoutMetadataRelatedEntityIds,
now,
}: CreateStandardPageLayoutWidgetArgs<L, T>): FlatPageLayoutWidget => {
const layoutIds = standardPageLayoutMetadataRelatedEntityIds[layoutName];
// @ts-expect-error ignore
const widgetDef = STANDARD_PAGE_LAYOUTS[layoutName].tabs[tabName].widgets[
widgetName
] as {
universalIdentifier: string;
};
if (!isDefined(widgetDef)) {
throw new Error(
`Invalid configuration ${layoutName} ${tabName.toString()} ${widgetName}`,
);
}
const tabIds = layoutIds.tabs[tabName];
const widgetIds = tabIds.widgets[widgetName];
return {
id: widgetIds.id,
universalIdentifier: widgetDef.universalIdentifier,
applicationId: twentyStandardApplicationId,
workspaceId,
pageLayoutTabId: tabIds.id,
title,
type,
gridPosition,
configuration,
objectMetadataId,
createdAt: now,
updatedAt: now,
deletedAt: null,
};
};
@@ -0,0 +1,464 @@
import { CalendarStartDay } from 'twenty-shared/constants';
import { v4 } from 'uuid';
import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
import { AxisNameDisplay } from 'src/engine/metadata-modules/page-layout-widget/enums/axis-name-display.enum';
import { BarChartLayout } from 'src/engine/metadata-modules/page-layout-widget/enums/bar-chart-layout.enum';
import { ObjectRecordGroupByDateGranularity } from 'src/engine/metadata-modules/page-layout-widget/enums/date-granularity.enum';
import { GraphOrderBy } from 'src/engine/metadata-modules/page-layout-widget/enums/graph-order-by.enum';
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
import { type AllStandardPageLayoutName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-page-layout-name.type';
import { type AllStandardPageLayoutTabName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-page-layout-tab-name.type';
import { type AllStandardPageLayoutWidgetName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-page-layout-widget-name.type';
import {
type CreateStandardPageLayoutWidgetArgs,
createStandardPageLayoutWidgetFlatMetadata,
} from 'src/engine/workspace-manager/twenty-standard-application/utils/page-layout-widget/create-standard-page-layout-widget-flat-metadata.util';
const createFilterConfig = (
filters: Array<{
type: string;
label: string;
value: string;
displayValue: string;
operand: string;
fieldMetadataId: string;
}>,
) => {
const groupId = v4();
return {
recordFilters: filters.map((filter) => ({
id: v4(),
type: filter.type,
label: filter.label,
value: filter.value,
displayValue: filter.displayValue,
operand: filter.operand,
fieldMetadataId: filter.fieldMetadataId,
recordFilterGroupId: groupId,
})),
recordFilterGroups: [
{
id: groupId,
logicalOperator: 'AND',
},
],
};
};
type WidgetBuilderArgs = Omit<CreateStandardPageLayoutWidgetArgs, 'context'>;
export const STANDARD_FLAT_PAGE_LAYOUT_WIDGET_BUILDERS = {
welcomeRichText: (args: WidgetBuilderArgs) =>
createStandardPageLayoutWidgetFlatMetadata({
...args,
context: {
layoutName: 'myFirstDashboard',
tabName: 'tab1',
widgetName: 'welcomeRichText',
title: 'Untitled Rich Text',
type: WidgetType.STANDALONE_RICH_TEXT,
gridPosition: { row: 0, column: 0, rowSpan: 6, columnSpan: 6 },
configuration: {
configurationType:
WidgetConfigurationType.STANDALONE_RICH_TEXT as const,
body: {
blocknote: JSON.stringify([
{
id: v4(),
type: 'heading',
props: {
textColor: 'default',
backgroundColor: 'default',
textAlignment: 'left',
level: 3,
},
content: [
{
type: 'text',
text: 'Welcome to your workspace',
styles: {},
},
],
children: [],
},
{
id: v4(),
type: 'paragraph',
props: {
textColor: 'default',
backgroundColor: 'default',
textAlignment: 'left',
},
content: [],
children: [],
},
{
id: v4(),
type: 'paragraph',
props: {
textColor: 'default',
backgroundColor: 'default',
textAlignment: 'left',
},
content: [
{
type: 'text',
text: 'You can edit this dashboard by clicking the ',
styles: {},
},
{ type: 'text', text: 'Edit', styles: { code: true } },
{
type: 'text',
text: ' button in the top-right corner to add your own charts or customize this one.',
styles: {},
},
],
children: [],
},
{
id: v4(),
type: 'paragraph',
props: {
textColor: 'default',
backgroundColor: 'default',
textAlignment: 'left',
},
content: [],
children: [],
},
{
id: v4(),
type: 'paragraph',
props: {
textColor: 'default',
backgroundColor: 'default',
textAlignment: 'left',
},
content: [
{
type: 'text',
text: "Don't forget to replace the sample data with your own.",
styles: {},
},
],
children: [],
},
{
id: v4(),
type: 'paragraph',
props: {
textColor: 'default',
backgroundColor: 'default',
textAlignment: 'left',
},
content: [],
children: [],
},
{
id: v4(),
type: 'paragraph',
props: {
textColor: 'default',
backgroundColor: 'default',
textAlignment: 'left',
},
content: [
{
type: 'text',
text: 'If you have any issues, you can check ',
styles: {},
},
{
type: 'link',
href: 'https://docs.twenty.com/user-guide/introduction',
content: [
{
type: 'text',
text: 'our documentation',
styles: {},
},
],
},
{
type: 'text',
text: ' or contact us through the Support section in Settings.',
styles: {},
},
],
children: [],
},
{
id: v4(),
type: 'paragraph',
props: {
textColor: 'default',
backgroundColor: 'default',
textAlignment: 'left',
},
content: [],
children: [],
},
]),
markdown: null,
},
},
objectMetadataId: null,
},
}),
dealsByCompany: (args: WidgetBuilderArgs) => {
const opportunityFields =
args.standardObjectMetadataRelatedEntityIds.opportunity.fields;
const opportunityObjectId =
args.standardObjectMetadataRelatedEntityIds.opportunity.id;
return createStandardPageLayoutWidgetFlatMetadata({
...args,
context: {
layoutName: 'myFirstDashboard',
tabName: 'tab1',
widgetName: 'dealsByCompany',
title: 'Deals by Company',
type: WidgetType.GRAPH,
gridPosition: { row: 0, column: 6, rowSpan: 6, columnSpan: 6 },
configuration: {
configurationType: WidgetConfigurationType.PIE_CHART,
groupByFieldMetadataId: opportunityFields.company.id,
groupBySubFieldName: 'name',
aggregateFieldMetadataId: opportunityFields.id.id,
aggregateOperation: AggregateOperations.COUNT,
orderBy: GraphOrderBy.FIELD_ASC,
displayDataLabel: false,
showCenterMetric: true,
displayLegend: true,
color: 'orange',
timezone: 'UTC',
firstDayOfTheWeek: CalendarStartDay.SUNDAY,
},
objectMetadataId: opportunityObjectId,
},
});
},
pipelineValueByStage: (args: WidgetBuilderArgs) => {
const opportunityFields =
args.standardObjectMetadataRelatedEntityIds.opportunity.fields;
const opportunityObjectId =
args.standardObjectMetadataRelatedEntityIds.opportunity.id;
return createStandardPageLayoutWidgetFlatMetadata({
...args,
context: {
layoutName: 'myFirstDashboard',
tabName: 'tab1',
widgetName: 'pipelineValueByStage',
title: 'Pipeline Value by Stage',
type: WidgetType.GRAPH,
gridPosition: { row: 6, column: 0, rowSpan: 6, columnSpan: 6 },
configuration: {
configurationType: WidgetConfigurationType.BAR_CHART,
aggregateFieldMetadataId: opportunityFields.amount.id,
aggregateOperation: AggregateOperations.SUM,
primaryAxisGroupByFieldMetadataId: opportunityFields.stage.id,
primaryAxisDateGranularity: ObjectRecordGroupByDateGranularity.DAY,
primaryAxisOrderBy: GraphOrderBy.FIELD_POSITION_ASC,
secondaryAxisGroupByFieldMetadataId: opportunityFields.company.id,
secondaryAxisGroupBySubFieldName: 'name',
secondaryAxisGroupByDateGranularity:
ObjectRecordGroupByDateGranularity.DAY,
secondaryAxisOrderBy: GraphOrderBy.FIELD_ASC,
axisNameDisplay: AxisNameDisplay.NONE,
displayDataLabel: true,
displayLegend: true,
color: 'green',
layout: BarChartLayout.VERTICAL,
timezone: 'UTC',
firstDayOfTheWeek: CalendarStartDay.SUNDAY,
},
objectMetadataId: opportunityObjectId,
},
});
},
revenueTimeline: (args: WidgetBuilderArgs) => {
const opportunityFields =
args.standardObjectMetadataRelatedEntityIds.opportunity.fields;
const opportunityObjectId =
args.standardObjectMetadataRelatedEntityIds.opportunity.id;
return createStandardPageLayoutWidgetFlatMetadata({
...args,
context: {
layoutName: 'myFirstDashboard',
tabName: 'tab1',
widgetName: 'revenueTimeline',
title: 'Revenue Timeline',
type: WidgetType.GRAPH,
gridPosition: { row: 6, column: 6, rowSpan: 6, columnSpan: 6 },
configuration: {
configurationType: WidgetConfigurationType.LINE_CHART,
aggregateFieldMetadataId: opportunityFields.amount.id,
aggregateOperation: AggregateOperations.SUM,
primaryAxisGroupByFieldMetadataId: opportunityFields.closeDate.id,
primaryAxisDateGranularity: ObjectRecordGroupByDateGranularity.DAY,
primaryAxisOrderBy: GraphOrderBy.FIELD_ASC,
axisNameDisplay: AxisNameDisplay.NONE,
displayDataLabel: false,
displayLegend: true,
color: 'crimson',
isCumulative: false,
timezone: 'UTC',
firstDayOfTheWeek: CalendarStartDay.SUNDAY,
},
objectMetadataId: opportunityObjectId,
},
});
},
opportunitiesByOwner: (args: WidgetBuilderArgs) => {
const opportunityFields =
args.standardObjectMetadataRelatedEntityIds.opportunity.fields;
const opportunityObjectId =
args.standardObjectMetadataRelatedEntityIds.opportunity.id;
return createStandardPageLayoutWidgetFlatMetadata({
...args,
context: {
layoutName: 'myFirstDashboard',
tabName: 'tab1',
widgetName: 'opportunitiesByOwner',
title: 'Opportunities by Owner',
type: WidgetType.GRAPH,
gridPosition: { row: 12, column: 0, rowSpan: 6, columnSpan: 6 },
configuration: {
configurationType: WidgetConfigurationType.BAR_CHART,
aggregateFieldMetadataId: opportunityFields.id.id,
aggregateOperation: AggregateOperations.COUNT,
primaryAxisGroupByFieldMetadataId: opportunityFields.owner.id,
primaryAxisGroupBySubFieldName: 'name.firstName',
primaryAxisDateGranularity: ObjectRecordGroupByDateGranularity.DAY,
primaryAxisOrderBy: GraphOrderBy.FIELD_ASC,
secondaryAxisGroupByFieldMetadataId: opportunityFields.owner.id,
secondaryAxisGroupBySubFieldName: 'name.firstName',
secondaryAxisGroupByDateGranularity:
ObjectRecordGroupByDateGranularity.DAY,
secondaryAxisOrderBy: GraphOrderBy.FIELD_ASC,
axisNameDisplay: AxisNameDisplay.NONE,
displayDataLabel: false,
displayLegend: true,
color: 'blue',
layout: BarChartLayout.HORIZONTAL,
isCumulative: false,
timezone: 'UTC',
firstDayOfTheWeek: CalendarStartDay.SUNDAY,
},
objectMetadataId: opportunityObjectId,
},
});
},
stockMarketIframe: (args: WidgetBuilderArgs) =>
createStandardPageLayoutWidgetFlatMetadata({
...args,
context: {
layoutName: 'myFirstDashboard',
tabName: 'tab1',
widgetName: 'stockMarketIframe',
title: 'Stock market (Iframe)',
type: WidgetType.IFRAME,
gridPosition: { row: 12, column: 6, rowSpan: 8, columnSpan: 6 },
configuration: {
configurationType: WidgetConfigurationType.IFRAME as const,
url: 'https://www.tradingview.com/embed-widget/hotlists/?locale=en',
},
objectMetadataId: null,
},
}),
dealsCreatedThisMonth: (args: WidgetBuilderArgs) => {
const opportunityFields =
args.standardObjectMetadataRelatedEntityIds.opportunity.fields;
const opportunityObjectId =
args.standardObjectMetadataRelatedEntityIds.opportunity.id;
return createStandardPageLayoutWidgetFlatMetadata({
...args,
context: {
layoutName: 'myFirstDashboard',
tabName: 'tab1',
widgetName: 'dealsCreatedThisMonth',
title: 'Deals created this month',
type: WidgetType.GRAPH,
gridPosition: { row: 18, column: 0, rowSpan: 2, columnSpan: 3 },
configuration: {
configurationType: WidgetConfigurationType.AGGREGATE_CHART,
aggregateFieldMetadataId: opportunityFields.id.id,
aggregateOperation: AggregateOperations.COUNT,
displayDataLabel: false,
filter: createFilterConfig([
{
type: 'DATE_TIME',
label: 'Creation date',
value: 'THIS_1_MONTH;;UTC;;SUNDAY;;',
displayValue: 'THIS_1_MONTH;;UTC;;SUNDAY;;',
operand: 'IS_RELATIVE',
fieldMetadataId: opportunityFields.createdAt.id,
},
]),
prefix: '',
timezone: 'UTC',
firstDayOfTheWeek: CalendarStartDay.SUNDAY,
},
objectMetadataId: opportunityObjectId,
},
});
},
dealValueCreatedThisMonth: (args: WidgetBuilderArgs) => {
const opportunityFields =
args.standardObjectMetadataRelatedEntityIds.opportunity.fields;
const opportunityObjectId =
args.standardObjectMetadataRelatedEntityIds.opportunity.id;
return createStandardPageLayoutWidgetFlatMetadata({
...args,
context: {
layoutName: 'myFirstDashboard',
tabName: 'tab1',
widgetName: 'dealValueCreatedThisMonth',
title: 'Deal value created this month',
type: WidgetType.GRAPH,
gridPosition: { row: 18, column: 3, rowSpan: 2, columnSpan: 3 },
configuration: {
configurationType: WidgetConfigurationType.AGGREGATE_CHART,
aggregateFieldMetadataId: opportunityFields.amount.id,
aggregateOperation: AggregateOperations.SUM,
displayDataLabel: false,
filter: createFilterConfig([
{
type: 'DATE_TIME',
label: 'Creation date',
value: 'THIS_1_MONTH;;UTC;;SUNDAY;;',
displayValue: 'THIS_1_MONTH;;UTC;;SUNDAY;;',
operand: 'IS_RELATIVE',
fieldMetadataId: opportunityFields.createdAt.id,
},
]),
prefix: '$',
timezone: 'UTC',
firstDayOfTheWeek: CalendarStartDay.SUNDAY,
},
objectMetadataId: opportunityObjectId,
},
});
},
} satisfies {
[T in AllStandardPageLayoutWidgetName<
AllStandardPageLayoutName,
AllStandardPageLayoutTabName
>]: (args: WidgetBuilderArgs) => FlatPageLayoutWidget;
};
@@ -0,0 +1,30 @@
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-maps-or-throw.util';
import { type FlatPageLayout } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout.type';
import { type CreateStandardPageLayoutArgs } from 'src/engine/workspace-manager/twenty-standard-application/utils/page-layout/create-standard-page-layout-flat-metadata.util';
import { STANDARD_FLAT_PAGE_LAYOUT_BUILDERS_BY_LAYOUT_NAME } from 'src/engine/workspace-manager/twenty-standard-application/utils/page-layout/standard-flat-page-layout-builders.constant';
export type BuildStandardFlatPageLayoutMetadataMapsArgs = Omit<
CreateStandardPageLayoutArgs,
'context'
>;
export const buildStandardFlatPageLayoutMetadataMaps = (
args: BuildStandardFlatPageLayoutMetadataMapsArgs,
): FlatEntityMaps<FlatPageLayout> => {
const allPageLayoutMetadatas: FlatPageLayout[] = Object.values(
STANDARD_FLAT_PAGE_LAYOUT_BUILDERS_BY_LAYOUT_NAME,
).map((builder) => builder(args));
let flatPageLayoutMaps = createEmptyFlatEntityMaps();
for (const pageLayoutMetadata of allPageLayoutMetadatas) {
flatPageLayoutMaps = addFlatEntityToFlatEntityMapsOrThrow({
flatEntity: pageLayoutMetadata,
flatEntityMaps: flatPageLayoutMaps,
});
}
return flatPageLayoutMaps;
};
@@ -0,0 +1,45 @@
import { type FlatPageLayout } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout.type';
import { type PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
import { STANDARD_PAGE_LAYOUTS } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-page-layout.constant';
import { type AllStandardPageLayoutName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-page-layout-name.type';
import { type StandardPageLayoutMetadataRelatedEntityIds } from 'src/engine/workspace-manager/twenty-standard-application/utils/get-standard-page-layout-metadata-related-entity-ids.util';
export type CreateStandardPageLayoutContext = {
layoutName: AllStandardPageLayoutName;
name: string;
type: PageLayoutType;
objectMetadataId: string | null;
};
export type CreateStandardPageLayoutArgs = {
now: string;
workspaceId: string;
twentyStandardApplicationId: string;
standardPageLayoutMetadataRelatedEntityIds: StandardPageLayoutMetadataRelatedEntityIds;
context: CreateStandardPageLayoutContext;
};
export const createStandardPageLayoutFlatMetadata = ({
context: { layoutName, name, type, objectMetadataId },
workspaceId,
twentyStandardApplicationId,
standardPageLayoutMetadataRelatedEntityIds,
now,
}: CreateStandardPageLayoutArgs): FlatPageLayout => {
const universalIdentifier =
STANDARD_PAGE_LAYOUTS[layoutName].universalIdentifier;
return {
id: standardPageLayoutMetadataRelatedEntityIds[layoutName].id,
universalIdentifier,
applicationId: twentyStandardApplicationId,
workspaceId,
name,
type,
objectMetadataId,
tabIds: [],
createdAt: now,
updatedAt: now,
deletedAt: null,
};
};
@@ -0,0 +1,24 @@
import { type FlatPageLayout } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout.type';
import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
import { type AllStandardPageLayoutName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-page-layout-name.type';
import {
type CreateStandardPageLayoutArgs,
createStandardPageLayoutFlatMetadata,
} from 'src/engine/workspace-manager/twenty-standard-application/utils/page-layout/create-standard-page-layout-flat-metadata.util';
export const STANDARD_FLAT_PAGE_LAYOUT_BUILDERS_BY_LAYOUT_NAME = {
myFirstDashboard: (args: Omit<CreateStandardPageLayoutArgs, 'context'>) =>
createStandardPageLayoutFlatMetadata({
...args,
context: {
layoutName: 'myFirstDashboard',
name: 'My First Dashboard',
type: PageLayoutType.DASHBOARD,
objectMetadataId: null,
},
}),
} satisfies {
[P in AllStandardPageLayoutName]: (
args: Omit<CreateStandardPageLayoutArgs, 'context'>,
) => FlatPageLayout;
};
@@ -1,12 +1,16 @@
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
import { type TwentyStandardAllFlatEntityMaps } from 'src/engine/workspace-manager/twenty-standard-application/types/twenty-standard-all-flat-entity-maps.type';
import { buildStandardFlatAgentMetadataMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/agent-metadata/build-standard-flat-agent-metadata-maps.util';
import { buildStandardFlatSkillMetadataMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/skill-metadata/build-standard-flat-skill-metadata-maps.util';
import { buildStandardFlatFieldMetadataMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/build-standard-flat-field-metadata-maps.util';
import { getStandardObjectMetadataRelatedEntityIds } from 'src/engine/workspace-manager/twenty-standard-application/utils/get-standard-object-metadata-related-entity-ids.util';
import { getStandardPageLayoutMetadataRelatedEntityIds } from 'src/engine/workspace-manager/twenty-standard-application/utils/get-standard-page-layout-metadata-related-entity-ids.util';
import { buildStandardFlatIndexMetadataMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/index/build-standard-flat-index-metadata-maps.util';
import { buildStandardFlatObjectMetadataMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/object-metadata/build-standard-flat-object-metadata-maps.util';
import { buildStandardFlatPageLayoutTabMetadataMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/page-layout-tab/build-standard-flat-page-layout-tab-metadata-maps.util';
import { buildStandardFlatPageLayoutWidgetMetadataMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/page-layout-widget/build-standard-flat-page-layout-widget-metadata-maps.util';
import { buildStandardFlatPageLayoutMetadataMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/page-layout/build-standard-flat-page-layout-metadata-maps.util';
import { buildStandardFlatRoleMetadataMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/role-metadata/build-standard-flat-role-metadata-maps.util';
import { buildStandardFlatSkillMetadataMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/skill-metadata/build-standard-flat-skill-metadata-maps.util';
import { buildStandardFlatViewFieldMetadataMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/view-field/build-standard-flat-view-field-metadata-maps.util';
import { buildStandardFlatViewFilterMetadataMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/view-filter/build-standard-flat-view-filter-metadata-maps.util';
import { buildStandardFlatViewGroupMetadataMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/view-group/build-standard-flat-view-group-metadata-maps.util';
@@ -131,6 +135,32 @@ export const computeTwentyStandardApplicationAllFlatEntityMaps = ({
dependencyFlatEntityMaps: undefined,
});
const standardPageLayoutMetadataRelatedEntityIds =
getStandardPageLayoutMetadataRelatedEntityIds();
const flatPageLayoutMaps = buildStandardFlatPageLayoutMetadataMaps({
now,
workspaceId,
twentyStandardApplicationId,
standardPageLayoutMetadataRelatedEntityIds,
});
const flatPageLayoutTabMaps = buildStandardFlatPageLayoutTabMetadataMaps({
now,
workspaceId,
twentyStandardApplicationId,
standardPageLayoutMetadataRelatedEntityIds,
});
const flatPageLayoutWidgetMaps =
buildStandardFlatPageLayoutWidgetMetadataMaps({
now,
workspaceId,
twentyStandardApplicationId,
standardObjectMetadataRelatedEntityIds,
standardPageLayoutMetadataRelatedEntityIds,
});
return {
flatViewFieldMaps,
flatViewFilterMaps,
@@ -142,5 +172,8 @@ export const computeTwentyStandardApplicationAllFlatEntityMaps = ({
flatRoleMaps,
flatAgentMaps,
flatSkillMaps,
flatPageLayoutMaps,
flatPageLayoutTabMaps,
flatPageLayoutWidgetMaps,
};
};
@@ -1,14 +1,13 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { DataSource, Repository } from 'typeorm';
import { Repository } from 'typeorm';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
import { ADMIN_ROLE } from 'src/engine/metadata-modules/role/constants/admin-role';
@@ -17,9 +16,6 @@ import { RoleService } from 'src/engine/metadata-modules/role/role.service';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
import { prefillCompanies } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-companies';
import { prefillPeople } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-people';
import { prefillWorkflows } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflows';
import { TwentyStandardApplicationService } from 'src/engine/workspace-manager/twenty-standard-application/services/twenty-standard-application.service';
@Injectable()
@@ -27,8 +23,6 @@ export class WorkspaceManagerService {
private readonly logger = new Logger(WorkspaceManagerService.name);
constructor(
@InjectDataSource()
private readonly coreDataSource: DataSource,
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
private readonly objectMetadataService: ObjectMetadataService,
private readonly dataSourceService: DataSourceService,
@@ -46,7 +40,6 @@ export class WorkspaceManagerService {
@InjectRepository(ServerlessFunctionEntity)
private readonly serverlessFunctionRepository: Repository<ServerlessFunctionEntity>,
private readonly applicationService: ApplicationService,
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
) {}
public async init({
@@ -86,11 +79,6 @@ export class WorkspaceManagerService {
},
);
await this.prefillCreatedWorkspaceRecords({
workspaceId,
schemaName,
});
const dataSourceMetadataCreationEnd = performance.now();
this.logger.log(
@@ -111,56 +99,6 @@ export class WorkspaceManagerService {
});
}
private async prefillCreatedWorkspaceRecords({
workspaceId,
schemaName,
}: {
workspaceId: string;
schemaName: string;
}): Promise<void> {
const { flatObjectMetadataMaps, flatFieldMetadataMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatObjectMetadataMaps', 'flatFieldMetadataMaps'],
},
);
const queryRunner = this.coreDataSource.createQueryRunner();
await queryRunner.connect();
try {
await queryRunner.startTransaction();
await prefillCompanies(queryRunner.manager, schemaName);
await prefillPeople(queryRunner.manager, schemaName);
await prefillWorkflows(
queryRunner.manager,
schemaName,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
);
await queryRunner.commitTransaction();
} catch (error) {
if (queryRunner.isTransactionActive) {
try {
await queryRunner.rollbackTransaction();
} catch (rollbackError) {
this.logger.error(
`Failed to rollback prefill transaction: ${rollbackError.message}`,
);
}
}
throw error;
} finally {
await queryRunner.release();
}
}
/**
* @deprecated Should be removed once AddWorkspaceForeignKeysMigrationCommand has been run successfully in production
* As we will be able to rely on foreignKey delete cascading
@@ -42,9 +42,11 @@ export class FlatPageLayoutWidgetValidatorService {
const isDashboardV2Enabled =
featureFlagsMap[FeatureFlagKey.IS_DASHBOARD_V2_ENABLED] ?? false;
const existingFlatPageLayoutWidget =
optimisticFlatEntityMapsAndRelatedFlatEntityMaps.flatPageLayoutWidgetMaps
.byId[flatEntityId];
const existingFlatPageLayoutWidget = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: flatEntityId,
flatEntityMaps:
optimisticFlatEntityMapsAndRelatedFlatEntityMaps.flatPageLayoutWidgetMaps,
});
const validationResult = getEmptyFlatEntityValidationError({
flatEntityMinimalInformation: {