diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index d9a0383d62..25d12b57fa 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -3768,6 +3768,12 @@ export type RevokeApiKeyInput = { id: Scalars['UUID']; }; +export type RichTextV2Body = { + __typename?: 'RichTextV2Body'; + blocknote?: Maybe; + markdown?: Maybe; +}; + export type Role = { __typename?: 'Role'; agents: Array; @@ -3977,6 +3983,11 @@ export type SignedFile = { token: Scalars['String']; }; +export type StandaloneRichTextConfiguration = { + __typename?: 'StandaloneRichTextConfiguration'; + body: RichTextV2Body; +}; + export type StandardOverrides = { __typename?: 'StandardOverrides'; description?: Maybe; @@ -4702,17 +4713,18 @@ export type Webhook = { workspaceId: Scalars['UUID']; }; -export type WidgetConfiguration = AggregateChartConfiguration | BarChartConfiguration | GaugeChartConfiguration | IframeConfiguration | LineChartConfiguration | PieChartConfiguration; +export type WidgetConfiguration = AggregateChartConfiguration | BarChartConfiguration | GaugeChartConfiguration | IframeConfiguration | LineChartConfiguration | PieChartConfiguration | StandaloneRichTextConfiguration; export enum WidgetType { CALENDAR = 'CALENDAR', EMAILS = 'EMAILS', FIELDS = 'FIELDS', + FIELD_RICH_TEXT = 'FIELD_RICH_TEXT', FILES = 'FILES', GRAPH = 'GRAPH', IFRAME = 'IFRAME', NOTES = 'NOTES', - RICH_TEXT = 'RICH_TEXT', + STANDALONE_RICH_TEXT = 'STANDALONE_RICH_TEXT', TASKS = 'TASKS', TIMELINE = 'TIMELINE', VIEW = 'VIEW', diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index 1d076b5b19..32d76a6937 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -3605,6 +3605,12 @@ export type RevokeApiKeyInput = { id: Scalars['UUID']; }; +export type RichTextV2Body = { + __typename?: 'RichTextV2Body'; + blocknote?: Maybe; + markdown?: Maybe; +}; + export type Role = { __typename?: 'Role'; agents: Array; @@ -3814,6 +3820,11 @@ export type SignedFile = { token: Scalars['String']; }; +export type StandaloneRichTextConfiguration = { + __typename?: 'StandaloneRichTextConfiguration'; + body: RichTextV2Body; +}; + export type StandardOverrides = { __typename?: 'StandardOverrides'; description?: Maybe; @@ -4521,17 +4532,18 @@ export type Webhook = { workspaceId: Scalars['UUID']; }; -export type WidgetConfiguration = AggregateChartConfiguration | BarChartConfiguration | GaugeChartConfiguration | IframeConfiguration | LineChartConfiguration | PieChartConfiguration; +export type WidgetConfiguration = AggregateChartConfiguration | BarChartConfiguration | GaugeChartConfiguration | IframeConfiguration | LineChartConfiguration | PieChartConfiguration | StandaloneRichTextConfiguration; export enum WidgetType { CALENDAR = 'CALENDAR', EMAILS = 'EMAILS', FIELDS = 'FIELDS', + FIELD_RICH_TEXT = 'FIELD_RICH_TEXT', FILES = 'FILES', GRAPH = 'GRAPH', IFRAME = 'IFRAME', NOTES = 'NOTES', - RICH_TEXT = 'RICH_TEXT', + STANDALONE_RICH_TEXT = 'STANDALONE_RICH_TEXT', TASKS = 'TASKS', TIMELINE = 'TIMELINE', VIEW = 'VIEW', @@ -4792,7 +4804,7 @@ export type SearchQueryVariables = Exact<{ export type SearchQuery = { __typename?: 'Query', search: { __typename?: 'SearchResultConnection', edges: Array<{ __typename?: 'SearchResultEdge', cursor: string, node: { __typename?: 'SearchRecord', recordId: any, objectNameSingular: string, label: string, imageUrl?: string | null, tsRankCD: number, tsRank: number } }>, pageInfo: { __typename?: 'SearchResultPageInfo', hasNextPage: boolean, endCursor?: string | null } } }; -export type PageLayoutWidgetFragmentFragment = { __typename?: 'PageLayoutWidget', id: any, title: string, type: WidgetType, objectMetadataId?: any | null, createdAt: string, updatedAt: string, deletedAt?: string | null, pageLayoutTabId: any, gridPosition: { __typename?: 'GridPosition', column: number, columnSpan: number, row: number, rowSpan: number }, configuration?: { __typename?: 'AggregateChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, label?: string | null, displayDataLabel?: boolean | null, format?: string | null, description?: string | null, filter?: any | null, prefix?: string | null, suffix?: string | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'BarChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, groupMode?: BarChartGroupMode | null, isCumulative?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'GaugeChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'IframeConfiguration', url?: string | null } | { __typename?: 'LineChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, isStacked?: boolean | null, isCumulative?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'PieChartConfiguration', graphType: GraphType, groupByFieldMetadataId: any, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, groupBySubFieldName?: string | null, dateGranularity?: ObjectRecordGroupByDateGranularity | null, orderBy?: GraphOrderBy | null, displayDataLabel?: boolean | null, showCenterMetric?: boolean | null, displayLegend?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | null }; +export type PageLayoutWidgetFragmentFragment = { __typename?: 'PageLayoutWidget', id: any, title: string, type: WidgetType, objectMetadataId?: any | null, createdAt: string, updatedAt: string, deletedAt?: string | null, pageLayoutTabId: any, gridPosition: { __typename?: 'GridPosition', column: number, columnSpan: number, row: number, rowSpan: number }, configuration?: { __typename?: 'AggregateChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, label?: string | null, displayDataLabel?: boolean | null, format?: string | null, description?: string | null, filter?: any | null, prefix?: string | null, suffix?: string | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'BarChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, groupMode?: BarChartGroupMode | null, isCumulative?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'GaugeChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'IframeConfiguration', url?: string | null } | { __typename?: 'LineChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, isStacked?: boolean | null, isCumulative?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'PieChartConfiguration', graphType: GraphType, groupByFieldMetadataId: any, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, groupBySubFieldName?: string | null, dateGranularity?: ObjectRecordGroupByDateGranularity | null, orderBy?: GraphOrderBy | null, displayDataLabel?: boolean | null, showCenterMetric?: boolean | null, displayLegend?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'StandaloneRichTextConfiguration', body: { __typename?: 'RichTextV2Body', blocknote?: string | null, markdown?: string | null } } | null }; export type UpdatePageLayoutWithTabsAndWidgetsMutationVariables = Exact<{ id: Scalars['String']; @@ -4800,7 +4812,7 @@ export type UpdatePageLayoutWithTabsAndWidgetsMutationVariables = Exact<{ }>; -export type UpdatePageLayoutWithTabsAndWidgetsMutation = { __typename?: 'Mutation', updatePageLayoutWithTabsAndWidgets: { __typename?: 'PageLayout', id: any, name: string, type: PageLayoutType, objectMetadataId?: any | null, createdAt: string, updatedAt: string, deletedAt?: string | null, tabs?: Array<{ __typename?: 'PageLayoutTab', id: any, title: string, position: number, pageLayoutId: any, createdAt: string, updatedAt: string, widgets?: Array<{ __typename?: 'PageLayoutWidget', id: any, title: string, type: WidgetType, objectMetadataId?: any | null, createdAt: string, updatedAt: string, deletedAt?: string | null, pageLayoutTabId: any, gridPosition: { __typename?: 'GridPosition', column: number, columnSpan: number, row: number, rowSpan: number }, configuration?: { __typename?: 'AggregateChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, label?: string | null, displayDataLabel?: boolean | null, format?: string | null, description?: string | null, filter?: any | null, prefix?: string | null, suffix?: string | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'BarChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, groupMode?: BarChartGroupMode | null, isCumulative?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'GaugeChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'IframeConfiguration', url?: string | null } | { __typename?: 'LineChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, isStacked?: boolean | null, isCumulative?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'PieChartConfiguration', graphType: GraphType, groupByFieldMetadataId: any, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, groupBySubFieldName?: string | null, dateGranularity?: ObjectRecordGroupByDateGranularity | null, orderBy?: GraphOrderBy | null, displayDataLabel?: boolean | null, showCenterMetric?: boolean | null, displayLegend?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | null }> | null }> | null } }; +export type UpdatePageLayoutWithTabsAndWidgetsMutation = { __typename?: 'Mutation', updatePageLayoutWithTabsAndWidgets: { __typename?: 'PageLayout', id: any, name: string, type: PageLayoutType, objectMetadataId?: any | null, createdAt: string, updatedAt: string, deletedAt?: string | null, tabs?: Array<{ __typename?: 'PageLayoutTab', id: any, title: string, position: number, pageLayoutId: any, createdAt: string, updatedAt: string, widgets?: Array<{ __typename?: 'PageLayoutWidget', id: any, title: string, type: WidgetType, objectMetadataId?: any | null, createdAt: string, updatedAt: string, deletedAt?: string | null, pageLayoutTabId: any, gridPosition: { __typename?: 'GridPosition', column: number, columnSpan: number, row: number, rowSpan: number }, configuration?: { __typename?: 'AggregateChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, label?: string | null, displayDataLabel?: boolean | null, format?: string | null, description?: string | null, filter?: any | null, prefix?: string | null, suffix?: string | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'BarChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, groupMode?: BarChartGroupMode | null, isCumulative?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'GaugeChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'IframeConfiguration', url?: string | null } | { __typename?: 'LineChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, isStacked?: boolean | null, isCumulative?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'PieChartConfiguration', graphType: GraphType, groupByFieldMetadataId: any, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, groupBySubFieldName?: string | null, dateGranularity?: ObjectRecordGroupByDateGranularity | null, orderBy?: GraphOrderBy | null, displayDataLabel?: boolean | null, showCenterMetric?: boolean | null, displayLegend?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'StandaloneRichTextConfiguration', body: { __typename?: 'RichTextV2Body', blocknote?: string | null, markdown?: string | null } } | null }> | null }> | null } }; export type OnDbEventSubscriptionVariables = Exact<{ input: OnDbEventInput; @@ -5208,6 +5220,12 @@ export const PageLayoutWidgetFragmentFragmentDoc = gql` ... on IframeConfiguration { url } + ... on StandaloneRichTextConfiguration { + body { + blocknote + markdown + } + } } pageLayoutTabId } diff --git a/packages/twenty-front/src/modules/object-record/record-show/layouts/note-record-layout.ts b/packages/twenty-front/src/modules/object-record/record-show/layouts/note-record-layout.ts index fde98f3a9d..e5e7f424d9 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/layouts/note-record-layout.ts +++ b/packages/twenty-front/src/modules/object-record/record-show/layouts/note-record-layout.ts @@ -7,7 +7,7 @@ export const NOTE_RECORD_LAYOUT: RecordLayout = { title: 'Note', position: 101, icon: 'IconNotes', - cards: [{ type: CardType.RichTextCard }], + cards: [{ type: CardType.FieldRichTextCard }], hide: { ifMobile: false, ifDesktop: false, diff --git a/packages/twenty-front/src/modules/object-record/record-show/layouts/task-record-layout.ts b/packages/twenty-front/src/modules/object-record/record-show/layouts/task-record-layout.ts index 3c0579b934..38f2a99421 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/layouts/task-record-layout.ts +++ b/packages/twenty-front/src/modules/object-record/record-show/layouts/task-record-layout.ts @@ -7,7 +7,7 @@ export const TASK_RECORD_LAYOUT: RecordLayout = { title: 'Note', position: 101, icon: 'IconNotes', - cards: [{ type: CardType.RichTextCard }], + cards: [{ type: CardType.FieldRichTextCard }], hide: { ifMobile: false, ifDesktop: false, diff --git a/packages/twenty-front/src/modules/object-record/record-show/types/CardConfiguration.ts b/packages/twenty-front/src/modules/object-record/record-show/types/CardConfiguration.ts index a61fde86ee..ad5655f789 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/types/CardConfiguration.ts +++ b/packages/twenty-front/src/modules/object-record/record-show/types/CardConfiguration.ts @@ -18,7 +18,7 @@ export type CardTypeToConfiguration = { [CardType.FileCard]: EmptyCardConfiguration; [CardType.EmailCard]: EmptyCardConfiguration; [CardType.CalendarCard]: EmptyCardConfiguration; - [CardType.RichTextCard]: EmptyCardConfiguration; + [CardType.FieldRichTextCard]: EmptyCardConfiguration; [CardType.WorkflowCard]: EmptyCardConfiguration; [CardType.WorkflowVersionCard]: EmptyCardConfiguration; [CardType.WorkflowRunCard]: EmptyCardConfiguration; diff --git a/packages/twenty-front/src/modules/object-record/record-show/types/CardType.ts b/packages/twenty-front/src/modules/object-record/record-show/types/CardType.ts index eba0ffb2b3..a80aa4497d 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/types/CardType.ts +++ b/packages/twenty-front/src/modules/object-record/record-show/types/CardType.ts @@ -9,6 +9,6 @@ export enum CardType { WorkflowCard = 'WorkflowCard', WorkflowVersionCard = 'WorkflowVersionCard', WorkflowRunCard = 'WorkflowRunCard', - RichTextCard = 'RichTextCard', + FieldRichTextCard = 'FieldRichTextCard', DashboardCard = 'DashboardCard', } diff --git a/packages/twenty-front/src/modules/object-record/record-show/utils/getCardComponent.tsx b/packages/twenty-front/src/modules/object-record/record-show/utils/getCardComponent.tsx index c47ded4f59..f7ec0f487d 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/utils/getCardComponent.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/utils/getCardComponent.tsx @@ -12,7 +12,7 @@ import { } from '@/object-record/record-show/types/CardConfiguration'; import { CardType } from '@/object-record/record-show/types/CardType'; import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext'; -import { RichTextCard } from '@/ui/layout/show-page/components/RichTextCard'; +import { FieldRichTextCard } from '@/ui/layout/show-page/components/FieldRichTextCard'; import { WorkflowCard } from '@/workflow/workflow-diagram/components/WorkflowCard'; import { WorkflowRunCard } from '@/workflow/workflow-diagram/components/WorkflowRunCard'; import { WorkflowVersionCard } from '@/workflow/workflow-diagram/components/WorkflowVersionCard'; @@ -58,8 +58,8 @@ export const getCardComponent = ( /> ); - case CardType.RichTextCard: - return ; + case CardType.FieldRichTextCard: + return ; case CardType.TaskCard: return ; diff --git a/packages/twenty-front/src/modules/page-layout/constants/DefaultNoteRecordPageLayout.ts b/packages/twenty-front/src/modules/page-layout/constants/DefaultNoteRecordPageLayout.ts index 54b9fcbcd4..0e7ab61295 100644 --- a/packages/twenty-front/src/modules/page-layout/constants/DefaultNoteRecordPageLayout.ts +++ b/packages/twenty-front/src/modules/page-layout/constants/DefaultNoteRecordPageLayout.ts @@ -48,7 +48,7 @@ export const DEFAULT_NOTE_RECORD_PAGE_LAYOUT: PageLayout = { id: 'note-widget-note', pageLayoutTabId: 'note-tab-fields', title: 'Note', - type: WidgetType.RICH_TEXT, + type: WidgetType.FIELD_RICH_TEXT, objectMetadataId: null, gridPosition: { __typename: 'GridPosition', @@ -87,7 +87,7 @@ export const DEFAULT_NOTE_RECORD_PAGE_LAYOUT: PageLayout = { id: 'note-widget-note', pageLayoutTabId: 'note-tab-note', title: 'Note', - type: WidgetType.RICH_TEXT, + type: WidgetType.FIELD_RICH_TEXT, objectMetadataId: null, gridPosition: { __typename: 'GridPosition', diff --git a/packages/twenty-front/src/modules/page-layout/constants/DefaultTaskRecordPageLayout.ts b/packages/twenty-front/src/modules/page-layout/constants/DefaultTaskRecordPageLayout.ts index 1078b6a5a8..6914ce9d1c 100644 --- a/packages/twenty-front/src/modules/page-layout/constants/DefaultTaskRecordPageLayout.ts +++ b/packages/twenty-front/src/modules/page-layout/constants/DefaultTaskRecordPageLayout.ts @@ -48,7 +48,7 @@ export const DEFAULT_TASK_RECORD_PAGE_LAYOUT: PageLayout = { id: 'task-widget-note', pageLayoutTabId: 'task-tab-fields', title: 'Note', - type: WidgetType.RICH_TEXT, + type: WidgetType.FIELD_RICH_TEXT, objectMetadataId: null, gridPosition: { __typename: 'GridPosition', @@ -87,7 +87,7 @@ export const DEFAULT_TASK_RECORD_PAGE_LAYOUT: PageLayout = { id: 'task-widget-note', pageLayoutTabId: 'task-tab-note', title: 'Note', - type: WidgetType.RICH_TEXT, + type: WidgetType.FIELD_RICH_TEXT, objectMetadataId: null, gridPosition: { __typename: 'GridPosition', diff --git a/packages/twenty-front/src/modules/page-layout/graphql/fragments/pageLayoutWidgetFragment.ts b/packages/twenty-front/src/modules/page-layout/graphql/fragments/pageLayoutWidgetFragment.ts index 45901c715f..5bb63c89fb 100644 --- a/packages/twenty-front/src/modules/page-layout/graphql/fragments/pageLayoutWidgetFragment.ts +++ b/packages/twenty-front/src/modules/page-layout/graphql/fragments/pageLayoutWidgetFragment.ts @@ -113,6 +113,12 @@ export const PAGE_LAYOUT_WIDGET_FRAGMENT = gql` ... on IframeConfiguration { url } + ... on StandaloneRichTextConfiguration { + body { + blocknote + markdown + } + } } pageLayoutTabId } diff --git a/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetContentRenderer.tsx b/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetContentRenderer.tsx index 49be52a072..2d13b3953e 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetContentRenderer.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetContentRenderer.tsx @@ -1,11 +1,11 @@ import { CalendarWidget } from '@/page-layout/widgets/calendar/components/CalendarWidget'; import { EmailWidget } from '@/page-layout/widgets/emails/components/EmailWidget'; +import { FieldRichTextWidget } from '@/page-layout/widgets/field-rich-text/components/FieldRichTextWidget'; import { FieldsWidget } from '@/page-layout/widgets/fields/components/FieldsWidget'; import { FileWidget } from '@/page-layout/widgets/files/components/FileWidget'; import { GraphWidgetRenderer } from '@/page-layout/widgets/graph/components/GraphWidgetRenderer'; import { IframeWidget } from '@/page-layout/widgets/iframe/components/IframeWidget'; import { NoteWidget } from '@/page-layout/widgets/notes/components/NoteWidget'; -import { RichTextWidget } from '@/page-layout/widgets/rich-text/components/RichTextWidget'; import { TaskWidget } from '@/page-layout/widgets/tasks/components/TaskWidget'; import { TimelineWidget } from '@/page-layout/widgets/timeline/components/TimelineWidget'; import { WorkflowRunWidget } from '@/page-layout/widgets/workflow/components/WorkflowRunWidget'; @@ -42,8 +42,8 @@ export const WidgetContentRenderer = ({ case WidgetType.NOTES: return ; - case WidgetType.RICH_TEXT: - return ; + case WidgetType.FIELD_RICH_TEXT: + return ; case WidgetType.FILES: return ; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/rich-text/components/RichTextWidget.tsx b/packages/twenty-front/src/modules/page-layout/widgets/field-rich-text/components/FieldRichTextWidget.tsx similarity index 54% rename from packages/twenty-front/src/modules/page-layout/widgets/rich-text/components/RichTextWidget.tsx rename to packages/twenty-front/src/modules/page-layout/widgets/field-rich-text/components/FieldRichTextWidget.tsx index 63f0056d86..0bd40e4676 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/rich-text/components/RichTextWidget.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/field-rich-text/components/FieldRichTextWidget.tsx @@ -1,4 +1,4 @@ -import { RichTextCard } from '@/ui/layout/show-page/components/RichTextCard'; +import { FieldRichTextCard } from '@/ui/layout/show-page/components/FieldRichTextCard'; import styled from '@emotion/styled'; import { type PageLayoutWidget } from '~/generated/graphql'; @@ -8,14 +8,16 @@ const StyledContainer = styled.div` width: 100%; `; -type RichTextWidgetProps = { +type FieldRichTextWidgetProps = { widget: PageLayoutWidget; }; -export const RichTextWidget = ({ widget: _widget }: RichTextWidgetProps) => { +export const FieldRichTextWidget = ({ + widget: _widget, +}: FieldRichTextWidgetProps) => { return ( - + ); }; diff --git a/packages/twenty-front/src/modules/ui/layout/show-page/components/RichTextCard.tsx b/packages/twenty-front/src/modules/ui/layout/show-page/components/FieldRichTextCard.tsx similarity index 98% rename from packages/twenty-front/src/modules/ui/layout/show-page/components/RichTextCard.tsx rename to packages/twenty-front/src/modules/ui/layout/show-page/components/FieldRichTextCard.tsx index ecb51787de..256f943f17 100644 --- a/packages/twenty-front/src/modules/ui/layout/show-page/components/RichTextCard.tsx +++ b/packages/twenty-front/src/modules/ui/layout/show-page/components/FieldRichTextCard.tsx @@ -46,7 +46,7 @@ const LoadingSkeleton = () => { ); }; -export const RichTextCard = () => { +export const FieldRichTextCard = () => { const targetRecord = useTargetRecord(); const activityBodyV2 = useRecoilValue( recordStoreFamilySelector({ diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/common/1764846384501-renameRichTextToFieldRichTextAndAddStandaloneRichText.ts b/packages/twenty-server/src/database/typeorm/core/migrations/common/1764846384501-renameRichTextToFieldRichTextAndAddStandaloneRichText.ts new file mode 100644 index 0000000000..589344cfdf --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/common/1764846384501-renameRichTextToFieldRichTextAndAddStandaloneRichText.ts @@ -0,0 +1,47 @@ +import { type MigrationInterface, type QueryRunner } from 'typeorm'; + +export class RenameRichTextToFieldRichTextAndAddStandaloneRichText1764846384501 + implements MigrationInterface +{ + name = 'RenameRichTextToFieldRichTextAndAddStandaloneRichText1764846384501'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TYPE "core"."pageLayoutWidget_type_enum" RENAME TO "pageLayoutWidget_type_enum_old"`, + ); + await queryRunner.query( + `CREATE TYPE "core"."pageLayoutWidget_type_enum" AS ENUM('VIEW', 'IFRAME', 'FIELDS', 'GRAPH', 'STANDALONE_RICH_TEXT', 'TIMELINE', 'TASKS', 'NOTES', 'FILES', 'EMAILS', 'CALENDAR', 'FIELD_RICH_TEXT', 'WORKFLOW', 'WORKFLOW_VERSION', 'WORKFLOW_RUN')`, + ); + await queryRunner.query( + `ALTER TABLE "core"."pageLayoutWidget" ALTER COLUMN "type" DROP DEFAULT`, + ); + await queryRunner.query( + `ALTER TABLE "core"."pageLayoutWidget" ALTER COLUMN "type" TYPE "core"."pageLayoutWidget_type_enum" USING "type"::"text"::"core"."pageLayoutWidget_type_enum"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."pageLayoutWidget" ALTER COLUMN "type" SET DEFAULT 'VIEW'`, + ); + await queryRunner.query( + `DROP TYPE "core"."pageLayoutWidget_type_enum_old"`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TYPE "core"."pageLayoutWidget_type_enum_old" AS ENUM('VIEW', 'IFRAME', 'FIELDS', 'GRAPH', 'TIMELINE', 'TASKS', 'NOTES', 'FILES', 'EMAILS', 'CALENDAR', 'RICH_TEXT', 'WORKFLOW', 'WORKFLOW_VERSION', 'WORKFLOW_RUN')`, + ); + await queryRunner.query( + `ALTER TABLE "core"."pageLayoutWidget" ALTER COLUMN "type" DROP DEFAULT`, + ); + await queryRunner.query( + `ALTER TABLE "core"."pageLayoutWidget" ALTER COLUMN "type" TYPE "core"."pageLayoutWidget_type_enum_old" USING "type"::"text"::"core"."pageLayoutWidget_type_enum_old"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."pageLayoutWidget" ALTER COLUMN "type" SET DEFAULT 'VIEW'`, + ); + await queryRunner.query(`DROP TYPE "core"."pageLayoutWidget_type_enum"`); + await queryRunner.query( + `ALTER TYPE "core"."pageLayoutWidget_type_enum_old" RENAME TO "pageLayoutWidget_type_enum"`, + ); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/rich-text-v2-body.dto.ts b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/rich-text-v2-body.dto.ts new file mode 100644 index 0000000000..5696803e16 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/rich-text-v2-body.dto.ts @@ -0,0 +1,17 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +import { IsOptional, IsString } from 'class-validator'; +import { type RichTextV2Metadata } from 'twenty-shared/types'; + +@ObjectType('RichTextV2Body') +export class RichTextV2BodyDTO implements RichTextV2Metadata { + @Field(() => String, { nullable: true }) + @IsString() + @IsOptional() + blocknote?: string | null; + + @Field(() => String, { nullable: true }) + @IsString() + @IsOptional() + markdown: string | null; +} diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/standalone-rich-text-configuration.dto.ts b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/standalone-rich-text-configuration.dto.ts new file mode 100644 index 0000000000..4614c4cd57 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/standalone-rich-text-configuration.dto.ts @@ -0,0 +1,15 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +import { Type } from 'class-transformer'; +import { IsNotEmpty, ValidateNested } from 'class-validator'; + +import { RichTextV2BodyDTO } from 'src/engine/core-modules/page-layout/dtos/rich-text-v2-body.dto'; + +@ObjectType('StandaloneRichTextConfiguration') +export class StandaloneRichTextConfigurationDTO { + @Field(() => RichTextV2BodyDTO) + @ValidateNested() + @Type(() => RichTextV2BodyDTO) + @IsNotEmpty() + body: RichTextV2BodyDTO; +} diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/widget-configuration.interface.ts b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/widget-configuration.interface.ts index 2f2b1c0ad1..a969bf13f2 100644 --- a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/widget-configuration.interface.ts +++ b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/widget-configuration.interface.ts @@ -6,6 +6,7 @@ import { GaugeChartConfigurationDTO } from 'src/engine/core-modules/page-layout/ import { IframeConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos/iframe-configuration.dto'; import { LineChartConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos/line-chart-configuration.dto'; import { PieChartConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos/pie-chart-configuration.dto'; +import { StandaloneRichTextConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos/standalone-rich-text-configuration.dto'; import { GraphType } from 'src/engine/core-modules/page-layout/enums/graph-type.enum'; import { WidgetConfigurationType } from 'src/engine/core-modules/page-layout/enums/widget-configuration-type.enum'; @@ -18,6 +19,7 @@ export const WidgetConfiguration = createUnionType({ AggregateChartConfigurationDTO, GaugeChartConfigurationDTO, IframeConfigurationDTO, + StandaloneRichTextConfigurationDTO, ], resolveType(configuration: Record) { if (!('configurationType' in configuration)) { @@ -52,6 +54,13 @@ export const WidgetConfiguration = createUnionType({ return IframeConfigurationDTO; } + if ( + configuration.configurationType === + WidgetConfigurationType.STANDALONE_RICH_TEXT_CONFIG + ) { + return StandaloneRichTextConfigurationDTO; + } + throw new Error( `Unknown widget configuration type: ${configuration.configurationType}`, ); @@ -64,4 +73,5 @@ export type WidgetConfigurationInterface = | PieChartConfigurationDTO | AggregateChartConfigurationDTO | GaugeChartConfigurationDTO - | IframeConfigurationDTO; + | IframeConfigurationDTO + | StandaloneRichTextConfigurationDTO; diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/enums/widget-configuration-type.enum.ts b/packages/twenty-server/src/engine/core-modules/page-layout/enums/widget-configuration-type.enum.ts index bc8c8ec1e4..e8c7bee55b 100644 --- a/packages/twenty-server/src/engine/core-modules/page-layout/enums/widget-configuration-type.enum.ts +++ b/packages/twenty-server/src/engine/core-modules/page-layout/enums/widget-configuration-type.enum.ts @@ -1,4 +1,5 @@ export enum WidgetConfigurationType { CHART_CONFIG = 'CHART_CONFIG', IFRAME_CONFIG = 'IFRAME_CONFIG', + STANDALONE_RICH_TEXT_CONFIG = 'STANDALONE_RICH_TEXT_CONFIG', } diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/enums/widget-type.enum.ts b/packages/twenty-server/src/engine/core-modules/page-layout/enums/widget-type.enum.ts index bbc50b566c..108dc645a9 100644 --- a/packages/twenty-server/src/engine/core-modules/page-layout/enums/widget-type.enum.ts +++ b/packages/twenty-server/src/engine/core-modules/page-layout/enums/widget-type.enum.ts @@ -3,13 +3,14 @@ export enum WidgetType { IFRAME = 'IFRAME', FIELDS = 'FIELDS', GRAPH = 'GRAPH', + STANDALONE_RICH_TEXT = 'STANDALONE_RICH_TEXT', TIMELINE = 'TIMELINE', TASKS = 'TASKS', NOTES = 'NOTES', FILES = 'FILES', EMAILS = 'EMAILS', CALENDAR = 'CALENDAR', - RICH_TEXT = 'RICH_TEXT', + FIELD_RICH_TEXT = 'FIELD_RICH_TEXT', WORKFLOW = 'WORKFLOW', WORKFLOW_VERSION = 'WORKFLOW_VERSION', WORKFLOW_RUN = 'WORKFLOW_RUN', diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/services/page-layout-widget.service.ts b/packages/twenty-server/src/engine/core-modules/page-layout/services/page-layout-widget.service.ts index 05442277fe..4cc6df5b81 100644 --- a/packages/twenty-server/src/engine/core-modules/page-layout/services/page-layout-widget.service.ts +++ b/packages/twenty-server/src/engine/core-modules/page-layout/services/page-layout-widget.service.ts @@ -142,7 +142,7 @@ export class PageLayoutWidgetService { ); try { - validatedConfig = validateAndTransformWidgetConfiguration({ + validatedConfig = await validateAndTransformWidgetConfiguration({ type: pageLayoutWidgetData.type, configuration: pageLayoutWidgetData.configuration, isDashboardV2Enabled, @@ -246,7 +246,7 @@ export class PageLayoutWidgetService { ); try { - validatedConfig = validateAndTransformWidgetConfiguration({ + validatedConfig = await validateAndTransformWidgetConfiguration({ type: typeForValidation, configuration: updateData.configuration, isDashboardV2Enabled, diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/utils/__tests__/validate-and-transform-widget-configuration.util.spec.ts b/packages/twenty-server/src/engine/core-modules/page-layout/utils/__tests__/validate-and-transform-widget-configuration.util.spec.ts index 5000aaf463..8666f956ff 100644 --- a/packages/twenty-server/src/engine/core-modules/page-layout/utils/__tests__/validate-and-transform-widget-configuration.util.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/page-layout/utils/__tests__/validate-and-transform-widget-configuration.util.spec.ts @@ -4,6 +4,9 @@ import { INVALID_IFRAME_CONFIG_EMPTY_URL, INVALID_NUMBER_CHART_CONFIG_BAD_UUID, INVALID_NUMBER_CHART_CONFIG_MISSING_FIELDS, + INVALID_STANDALONE_RICH_TEXT_CONFIG_BODY_WRONG_TYPE, + INVALID_STANDALONE_RICH_TEXT_CONFIG_INVALID_SUBFIELDS, + INVALID_STANDALONE_RICH_TEXT_CONFIG_MISSING_BODY, INVALID_VERTICAL_BAR_CHART_CONFIG_MISSING_GROUP_BY, TEST_GAUGE_CHART_CONFIG, TEST_HORIZONTAL_BAR_CHART_CONFIG, @@ -13,6 +16,8 @@ import { TEST_NUMBER_CHART_CONFIG, TEST_NUMBER_CHART_CONFIG_MINIMAL, TEST_PIE_CHART_CONFIG, + TEST_STANDALONE_RICH_TEXT_CONFIG, + TEST_STANDALONE_RICH_TEXT_CONFIG_MINIMAL, TEST_VERTICAL_BAR_CHART_CONFIG, TEST_VERTICAL_BAR_CHART_CONFIG_MINIMAL, } from 'test/integration/constants/widget-configuration-test-data.constants'; @@ -20,10 +25,22 @@ import { import { WidgetType } from 'src/engine/core-modules/page-layout/enums/widget-type.enum'; import { validateAndTransformWidgetConfiguration } from 'src/engine/core-modules/page-layout/utils/validate-and-transform-widget-configuration.util'; +jest.mock( + 'src/engine/core-modules/record-transformer/utils/transform-rich-text-v2.util', + () => ({ + transformRichTextV2Value: jest.fn((value) => + Promise.resolve({ + blocknote: value.blocknote ?? null, + markdown: value.markdown ?? null, + }), + ), + }), +); + describe('validateAndTransformWidgetConfiguration', () => { describe('IFRAME widget', () => { - it('should validate and transform valid iframe configuration', () => { - const result = validateAndTransformWidgetConfiguration({ + it('should validate and transform valid iframe configuration', async () => { + const result = await validateAndTransformWidgetConfiguration({ type: WidgetType.IFRAME, configuration: TEST_IFRAME_CONFIG, isDashboardV2Enabled: false, @@ -32,31 +49,86 @@ describe('validateAndTransformWidgetConfiguration', () => { expect(result).toMatchObject(TEST_IFRAME_CONFIG); }); - it('should throw error for invalid URL', () => { - expect(() => + it('should throw error for invalid URL', async () => { + await expect( validateAndTransformWidgetConfiguration({ type: WidgetType.IFRAME, configuration: INVALID_IFRAME_CONFIG_BAD_URL, isDashboardV2Enabled: false, }), - ).toThrow(/url must be a URL address/); + ).rejects.toThrow(/url must be a URL address/); }); - it('should throw error for empty URL', () => { - expect(() => + it('should throw error for empty URL', async () => { + await expect( validateAndTransformWidgetConfiguration({ type: WidgetType.IFRAME, configuration: INVALID_IFRAME_CONFIG_EMPTY_URL, isDashboardV2Enabled: false, }), - ).toThrow(/url must be a URL address/); + ).rejects.toThrow(/url must be a URL address/); + }); + }); + + describe('STANDALONE_RICH_TEXT widget', () => { + it('should validate and transform valid standalone rich text configuration', async () => { + const result = await validateAndTransformWidgetConfiguration({ + type: WidgetType.STANDALONE_RICH_TEXT, + configuration: TEST_STANDALONE_RICH_TEXT_CONFIG, + isDashboardV2Enabled: false, + }); + + expect(result).toMatchObject(TEST_STANDALONE_RICH_TEXT_CONFIG); + }); + + it('should validate minimal standalone rich text configuration', async () => { + const result = await validateAndTransformWidgetConfiguration({ + type: WidgetType.STANDALONE_RICH_TEXT, + configuration: TEST_STANDALONE_RICH_TEXT_CONFIG_MINIMAL, + isDashboardV2Enabled: false, + }); + + expect(result).toMatchObject(TEST_STANDALONE_RICH_TEXT_CONFIG_MINIMAL); + }); + + it('should throw error for missing body', async () => { + await expect( + validateAndTransformWidgetConfiguration({ + type: WidgetType.STANDALONE_RICH_TEXT, + configuration: INVALID_STANDALONE_RICH_TEXT_CONFIG_MISSING_BODY, + isDashboardV2Enabled: false, + }), + ).rejects.toThrow(/body/); + }); + + it('should throw error when body is wrong type', async () => { + await expect( + validateAndTransformWidgetConfiguration({ + type: WidgetType.STANDALONE_RICH_TEXT, + configuration: INVALID_STANDALONE_RICH_TEXT_CONFIG_BODY_WRONG_TYPE, + isDashboardV2Enabled: false, + }), + ).rejects.toThrow(); + }); + + it('should strip invalid subfields from body', async () => { + const result = await validateAndTransformWidgetConfiguration({ + type: WidgetType.STANDALONE_RICH_TEXT, + configuration: INVALID_STANDALONE_RICH_TEXT_CONFIG_INVALID_SUBFIELDS, + isDashboardV2Enabled: false, + }); + + expect(result).toBeDefined(); + expect((result as any).body.blocknote).toBeDefined(); + expect((result as any).body.markdown).toBe('valid'); + expect((result as any).body.invalidField).toBeUndefined(); }); }); describe('GRAPH widget', () => { describe('NUMBER graph', () => { - it('should validate full number graph configuration', () => { - const result = validateAndTransformWidgetConfiguration({ + it('should validate full number graph configuration', async () => { + const result = await validateAndTransformWidgetConfiguration({ type: WidgetType.GRAPH, configuration: TEST_NUMBER_CHART_CONFIG, isDashboardV2Enabled: false, @@ -65,8 +137,8 @@ describe('validateAndTransformWidgetConfiguration', () => { expect(result).toMatchObject(TEST_NUMBER_CHART_CONFIG); }); - it('should validate minimal number graph configuration', () => { - const result = validateAndTransformWidgetConfiguration({ + it('should validate minimal number graph configuration', async () => { + const result = await validateAndTransformWidgetConfiguration({ type: WidgetType.GRAPH, configuration: TEST_NUMBER_CHART_CONFIG_MINIMAL, isDashboardV2Enabled: false, @@ -75,30 +147,30 @@ describe('validateAndTransformWidgetConfiguration', () => { expect(result).toMatchObject(TEST_NUMBER_CHART_CONFIG_MINIMAL); }); - it('should throw error for partial number graph configuration with missing required fields', () => { - expect(() => + it('should throw error for partial number graph configuration with missing required fields', async () => { + await expect( validateAndTransformWidgetConfiguration({ type: WidgetType.GRAPH, configuration: INVALID_NUMBER_CHART_CONFIG_MISSING_FIELDS, isDashboardV2Enabled: false, }), - ).toThrow(/aggregateFieldMetadataId.*aggregateOperation/); + ).rejects.toThrow(/aggregateFieldMetadataId.*aggregateOperation/); }); - it('should throw error for invalid UUID', () => { - expect(() => + it('should throw error for invalid UUID', async () => { + await expect( validateAndTransformWidgetConfiguration({ type: WidgetType.GRAPH, configuration: INVALID_NUMBER_CHART_CONFIG_BAD_UUID, isDashboardV2Enabled: false, }), - ).toThrow(/aggregateFieldMetadataId must be a UUID/); + ).rejects.toThrow(/aggregateFieldMetadataId must be a UUID/); }); }); describe('VERTICAL_BAR graph', () => { - it('should validate full vertical bar graph configuration', () => { - const result = validateAndTransformWidgetConfiguration({ + it('should validate full vertical bar graph configuration', async () => { + const result = await validateAndTransformWidgetConfiguration({ type: WidgetType.GRAPH, configuration: TEST_VERTICAL_BAR_CHART_CONFIG, isDashboardV2Enabled: false, @@ -107,8 +179,8 @@ describe('validateAndTransformWidgetConfiguration', () => { expect(result).toMatchObject(TEST_VERTICAL_BAR_CHART_CONFIG); }); - it('should validate minimal vertical bar graph configuration', () => { - const result = validateAndTransformWidgetConfiguration({ + it('should validate minimal vertical bar graph configuration', async () => { + const result = await validateAndTransformWidgetConfiguration({ type: WidgetType.GRAPH, configuration: TEST_VERTICAL_BAR_CHART_CONFIG_MINIMAL, isDashboardV2Enabled: false, @@ -117,20 +189,20 @@ describe('validateAndTransformWidgetConfiguration', () => { expect(result).toMatchObject(TEST_VERTICAL_BAR_CHART_CONFIG_MINIMAL); }); - it('should throw error for partial vertical bar graph configuration with missing required fields', () => { - expect(() => + it('should throw error for partial vertical bar graph configuration with missing required fields', async () => { + await expect( validateAndTransformWidgetConfiguration({ type: WidgetType.GRAPH, configuration: INVALID_VERTICAL_BAR_CHART_CONFIG_MISSING_GROUP_BY, isDashboardV2Enabled: false, }), - ).toThrow(/primaryAxisGroupByFieldMetadataId/); + ).rejects.toThrow(/primaryAxisGroupByFieldMetadataId/); }); }); describe('HORIZONTAL_BAR graph', () => { - it('should validate full horizontal bar graph configuration', () => { - const result = validateAndTransformWidgetConfiguration({ + it('should validate full horizontal bar graph configuration', async () => { + const result = await validateAndTransformWidgetConfiguration({ type: WidgetType.GRAPH, configuration: TEST_HORIZONTAL_BAR_CHART_CONFIG, isDashboardV2Enabled: false, @@ -139,8 +211,8 @@ describe('validateAndTransformWidgetConfiguration', () => { expect(result).toMatchObject(TEST_HORIZONTAL_BAR_CHART_CONFIG); }); - it('should validate minimal horizontal bar graph configuration', () => { - const result = validateAndTransformWidgetConfiguration({ + it('should validate minimal horizontal bar graph configuration', async () => { + const result = await validateAndTransformWidgetConfiguration({ type: WidgetType.GRAPH, configuration: TEST_HORIZONTAL_BAR_CHART_CONFIG_MINIMAL, isDashboardV2Enabled: false, @@ -149,24 +221,24 @@ describe('validateAndTransformWidgetConfiguration', () => { expect(result).toMatchObject(TEST_HORIZONTAL_BAR_CHART_CONFIG_MINIMAL); }); - it('should throw error for partial horizontal bar graph configuration with missing required fields', () => { - expect(() => + it('should throw error for partial horizontal bar graph configuration with missing required fields', async () => { + await expect( validateAndTransformWidgetConfiguration({ type: WidgetType.GRAPH, configuration: INVALID_HORIZONTAL_BAR_CHART_CONFIG_MISSING_GROUP_BY, isDashboardV2Enabled: false, }), - ).toThrow(/primaryAxisGroupByFieldMetadataId/); + ).rejects.toThrow(/primaryAxisGroupByFieldMetadataId/); }); }); - it('should return null for unsupported graph type', () => { + it('should return null for unsupported graph type', async () => { const configuration = { graphType: 'UNSUPPORTED', viewId: '550e8400-e29b-41d4-a716-446655440000', }; - const result = validateAndTransformWidgetConfiguration({ + const result = await validateAndTransformWidgetConfiguration({ type: WidgetType.GRAPH, configuration: configuration, isDashboardV2Enabled: false, @@ -175,12 +247,12 @@ describe('validateAndTransformWidgetConfiguration', () => { expect(result).toBeNull(); }); - it('should return null for missing graph type', () => { + it('should return null for missing graph type', async () => { const configuration = { viewId: '550e8400-e29b-41d4-a716-446655440000', }; - const result = validateAndTransformWidgetConfiguration({ + const result = await validateAndTransformWidgetConfiguration({ type: WidgetType.GRAPH, configuration: configuration, isDashboardV2Enabled: false, @@ -191,40 +263,40 @@ describe('validateAndTransformWidgetConfiguration', () => { }); describe('Edge cases', () => { - it('should throw error for null configuration', () => { - expect(() => + it('should throw error for null configuration', async () => { + await expect( validateAndTransformWidgetConfiguration({ type: WidgetType.IFRAME, configuration: null, isDashboardV2Enabled: false, }), - ).toThrow('Invalid configuration: not an object'); + ).rejects.toThrow('Invalid configuration: not an object'); }); - it('should throw error for undefined configuration', () => { - expect(() => + it('should throw error for undefined configuration', async () => { + await expect( validateAndTransformWidgetConfiguration({ type: WidgetType.IFRAME, configuration: undefined, isDashboardV2Enabled: false, }), - ).toThrow('Invalid configuration: not an object'); + ).rejects.toThrow('Invalid configuration: not an object'); }); - it('should throw error for non-object configuration', () => { - expect(() => + it('should throw error for non-object configuration', async () => { + await expect( validateAndTransformWidgetConfiguration({ type: WidgetType.IFRAME, configuration: 'string', isDashboardV2Enabled: false, }), - ).toThrow('Invalid configuration: not an object'); + ).rejects.toThrow('Invalid configuration: not an object'); }); - it('should return null for unsupported widget type', () => { + it('should return null for unsupported widget type', async () => { const configuration = { someField: 'value' }; - const result = validateAndTransformWidgetConfiguration({ + const result = await validateAndTransformWidgetConfiguration({ type: 'UNSUPPORTED' as WidgetType, configuration: configuration, isDashboardV2Enabled: false, @@ -235,72 +307,72 @@ describe('validateAndTransformWidgetConfiguration', () => { }); describe('Error messages', () => { - it('should include validation details in error message', () => { - expect(() => + it('should include validation details in error message', async () => { + await expect( validateAndTransformWidgetConfiguration({ type: WidgetType.GRAPH, configuration: INVALID_NUMBER_CHART_CONFIG_BAD_UUID, isDashboardV2Enabled: false, }), - ).toThrow(/aggregateFieldMetadataId must be a UUID/); + ).rejects.toThrow(/aggregateFieldMetadataId must be a UUID/); }); }); describe('Feature flags', () => { - it('should throw error for GAUGE chart type when IS_DASHBOARD_V2_ENABLED is false', () => { - expect(() => + it('should throw error for GAUGE chart type when IS_DASHBOARD_V2_ENABLED is false', async () => { + await expect( validateAndTransformWidgetConfiguration({ type: WidgetType.GRAPH, configuration: TEST_GAUGE_CHART_CONFIG, isDashboardV2Enabled: false, }), - ).toThrow(/IS_DASHBOARD_V2_ENABLED feature flag/); + ).rejects.toThrow(/IS_DASHBOARD_V2_ENABLED feature flag/); }); - it('should not throw error for GAUGE chart type when IS_DASHBOARD_V2_ENABLED is true', () => { - expect(() => + it('should not throw error for GAUGE chart type when IS_DASHBOARD_V2_ENABLED is true', async () => { + await expect( validateAndTransformWidgetConfiguration({ type: WidgetType.GRAPH, configuration: TEST_GAUGE_CHART_CONFIG, isDashboardV2Enabled: true, }), - ).not.toThrow(); + ).resolves.not.toThrow(); }); - it('should not throw error for PIE chart type regardless of IS_DASHBOARD_V2_ENABLED', () => { - expect(() => + it('should not throw error for PIE chart type regardless of IS_DASHBOARD_V2_ENABLED', async () => { + await expect( validateAndTransformWidgetConfiguration({ type: WidgetType.GRAPH, configuration: TEST_PIE_CHART_CONFIG, isDashboardV2Enabled: false, }), - ).not.toThrow(); + ).resolves.not.toThrow(); - expect(() => + await expect( validateAndTransformWidgetConfiguration({ type: WidgetType.GRAPH, configuration: TEST_PIE_CHART_CONFIG, isDashboardV2Enabled: true, }), - ).not.toThrow(); + ).resolves.not.toThrow(); }); - it('should not throw error for LINE chart type regardless of IS_DASHBOARD_V2_ENABLED', () => { - expect(() => + it('should not throw error for LINE chart type regardless of IS_DASHBOARD_V2_ENABLED', async () => { + await expect( validateAndTransformWidgetConfiguration({ type: WidgetType.GRAPH, configuration: TEST_LINE_CHART_CONFIG, isDashboardV2Enabled: false, }), - ).not.toThrow(); + ).resolves.not.toThrow(); - expect(() => + await expect( validateAndTransformWidgetConfiguration({ type: WidgetType.GRAPH, configuration: TEST_LINE_CHART_CONFIG, isDashboardV2Enabled: true, }), - ).not.toThrow(); + ).resolves.not.toThrow(); }); }); }); diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/utils/inject-widget-configuration-discriminator.util.ts b/packages/twenty-server/src/engine/core-modules/page-layout/utils/inject-widget-configuration-discriminator.util.ts index d6ee0f2712..67b78e395a 100644 --- a/packages/twenty-server/src/engine/core-modules/page-layout/utils/inject-widget-configuration-discriminator.util.ts +++ b/packages/twenty-server/src/engine/core-modules/page-layout/utils/inject-widget-configuration-discriminator.util.ts @@ -18,14 +18,21 @@ export const injectWidgetConfigurationDiscriminator = ( return { ...configuration, configurationType: WidgetConfigurationType.IFRAME_CONFIG, - } as ConfigurationWithDiscriminator; + } satisfies ConfigurationWithDiscriminator; } if (widgetType === WidgetType.GRAPH && 'graphType' in configuration) { return { ...configuration, configurationType: WidgetConfigurationType.CHART_CONFIG, - } as ConfigurationWithDiscriminator; + } satisfies ConfigurationWithDiscriminator; + } + + if (widgetType === WidgetType.STANDALONE_RICH_TEXT) { + return { + ...configuration, + configurationType: WidgetConfigurationType.STANDALONE_RICH_TEXT_CONFIG, + } satisfies ConfigurationWithDiscriminator; } return configuration as ConfigurationWithDiscriminator; diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/utils/validate-and-transform-widget-configuration.util.ts b/packages/twenty-server/src/engine/core-modules/page-layout/utils/validate-and-transform-widget-configuration.util.ts index 68f8b17a28..bdfbd07747 100644 --- a/packages/twenty-server/src/engine/core-modules/page-layout/utils/validate-and-transform-widget-configuration.util.ts +++ b/packages/twenty-server/src/engine/core-modules/page-layout/utils/validate-and-transform-widget-configuration.util.ts @@ -3,11 +3,13 @@ import { validateSync, type ValidationError } from 'class-validator'; import { isDefined } from 'twenty-shared/utils'; import { AggregateChartConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos/aggregate-chart-configuration.dto'; +import { transformRichTextV2Value } from 'src/engine/core-modules/record-transformer/utils/transform-rich-text-v2.util'; import { BarChartConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos/bar-chart-configuration.dto'; import { GaugeChartConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos/gauge-chart-configuration.dto'; import { IframeConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos/iframe-configuration.dto'; import { LineChartConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos/line-chart-configuration.dto'; import { PieChartConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos/pie-chart-configuration.dto'; +import { StandaloneRichTextConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos/standalone-rich-text-configuration.dto'; import { type WidgetConfigurationInterface } from 'src/engine/core-modules/page-layout/dtos/widget-configuration.interface'; import { BarChartGroupMode } from 'src/engine/core-modules/page-layout/enums/bar-chart-group-mode.enum'; import { GraphType } from 'src/engine/core-modules/page-layout/enums/graph-type.enum'; @@ -161,7 +163,31 @@ const validateIframeConfiguration = ( return instance; }; -export const validateAndTransformWidgetConfiguration = ({ +const validateStandaloneRichTextConfiguration = async ( + configuration: unknown, +): Promise => { + const instance = plainToInstance( + StandaloneRichTextConfigurationDTO, + configuration, + ); + + const errors = validateSync(instance, { + whitelist: true, + forbidUnknownValues: true, + }); + + if (errors.length > 0) { + throw errors; + } + + if (instance.body) { + instance.body = await transformRichTextV2Value(instance.body); + } + + return instance; +}; + +export const validateAndTransformWidgetConfiguration = async ({ type, configuration, isDashboardV2Enabled, @@ -169,7 +195,7 @@ export const validateAndTransformWidgetConfiguration = ({ type: WidgetType; configuration: unknown; isDashboardV2Enabled: boolean; -}): WidgetConfigurationInterface | null => { +}): Promise => { if (!configuration || typeof configuration !== 'object') { throw new Error('Invalid configuration: not an object'); } @@ -183,6 +209,8 @@ export const validateAndTransformWidgetConfiguration = ({ }); case WidgetType.IFRAME: return validateIframeConfiguration(configuration); + case WidgetType.STANDALONE_RICH_TEXT: + return await validateStandaloneRichTextConfiguration(configuration); default: return null; } diff --git a/packages/twenty-server/test/integration/constants/widget-configuration-test-data.constants.ts b/packages/twenty-server/test/integration/constants/widget-configuration-test-data.constants.ts index 17fd6ca071..ac648dae32 100644 --- a/packages/twenty-server/test/integration/constants/widget-configuration-test-data.constants.ts +++ b/packages/twenty-server/test/integration/constants/widget-configuration-test-data.constants.ts @@ -12,6 +12,34 @@ export const TEST_IFRAME_CONFIG = { url: 'https://example.com/dashboard', }; +export const TEST_STANDALONE_RICH_TEXT_CONFIG = { + body: { + blocknote: + '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"Hello world"}]}]}', + markdown: '# Hello world', + }, +}; + +export const TEST_STANDALONE_RICH_TEXT_CONFIG_MINIMAL = { + body: { + markdown: 'Simple text', + }, +}; + +export const INVALID_STANDALONE_RICH_TEXT_CONFIG_MISSING_BODY = {}; + +export const INVALID_STANDALONE_RICH_TEXT_CONFIG_BODY_WRONG_TYPE = { + body: 'not an object', +}; + +export const INVALID_STANDALONE_RICH_TEXT_CONFIG_INVALID_SUBFIELDS = { + body: { + blocknote: 'valid', + markdown: 'valid', + invalidField: 'should not be here', + }, +}; + export const TEST_IFRAME_CONFIG_ALTERNATIVE = { url: 'https://app.twenty.com/analytics', }; @@ -263,6 +291,8 @@ export function getValidConfigForWidgetType(widgetType: string): any { return configs[Math.floor(Math.random() * configs.length)]; } + case 'STANDALONE_RICH_TEXT': + return TEST_STANDALONE_RICH_TEXT_CONFIG; case 'VIEW': case 'FIELDS': case 'TIMELINE': @@ -271,6 +301,7 @@ export function getValidConfigForWidgetType(widgetType: string): any { case 'FILES': case 'EMAILS': case 'CALENDAR': + case 'FIELD_RICH_TEXT': return null; default: return null;