[Dashboards] Server: Rich Text Widget (#16296)

closes https://github.com/twentyhq/core-team-issues/issues/1893

sync in the transformation layer - 
with just the blocknote format -

<img width="1532" height="1058" alt="CleanShot 2025-12-04 at 16 43 27"
src="https://github.com/user-attachments/assets/081483fb-0bd0-4f14-81fc-8ea2261367a2"
/>

with just the mardown format - 
<img width="1534" height="1056" alt="CleanShot 2025-12-04 at 16 45 03"
src="https://github.com/user-attachments/assets/4eae513b-423d-4597-b79c-97d1f33ab994"
/>

attaching response since in screen shot its getting cut - 
```
{
  "id": "7189a10a-177c-4cc2-8946-950b8c125913",
  "pageLayoutTabId": "82ac56dd-4351-43ad-bf3d-49624c057e17",
  "workspaceId": "20202020-1c25-4d02-bf25-6aeccf7ea419",
  "title": "awdawd awdawdawd",
  "type": "STANDALONE_RICH_TEXT",
  "objectMetadataId": null,
  "gridPosition": {
    "row": 1,
    "column": 1,
    "rowSpan": 1,
    "columnSpan": 1
  },
  "configuration": {
    "body": {
      "markdown": "# Hello World",
      "blocknote": "[{\"id\":\"2e744f21-7c71-414d-b87f-fb48400c593c\",\"type\":\"heading\",\"props\":{\"textColor\":\"default\",\"backgroundColor\":\"default\",\"textAlignment\":\"left\",\"level\":1},\"content\":[{\"type\":\"text\",\"text\":\"Hello World\",\"styles\":{}}],\"children\":[]}]"
    }
  },
  "createdAt": "2025-12-04T11:14:52.212Z",
  "updatedAt": "2025-12-04T11:14:52.212Z",
  "deletedAt": null
}
```
This commit is contained in:
nitin
2025-12-04 18:28:33 +05:30
committed by GitHub
parent 193be72289
commit 2f36c2e82e
24 changed files with 366 additions and 99 deletions
@@ -3768,6 +3768,12 @@ export type RevokeApiKeyInput = {
id: Scalars['UUID'];
};
export type RichTextV2Body = {
__typename?: 'RichTextV2Body';
blocknote?: Maybe<Scalars['String']>;
markdown?: Maybe<Scalars['String']>;
};
export type Role = {
__typename?: 'Role';
agents: Array<Agent>;
@@ -3977,6 +3983,11 @@ export type SignedFile = {
token: Scalars['String'];
};
export type StandaloneRichTextConfiguration = {
__typename?: 'StandaloneRichTextConfiguration';
body: RichTextV2Body;
};
export type StandardOverrides = {
__typename?: 'StandardOverrides';
description?: Maybe<Scalars['String']>;
@@ -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',
+22 -4
View File
@@ -3605,6 +3605,12 @@ export type RevokeApiKeyInput = {
id: Scalars['UUID'];
};
export type RichTextV2Body = {
__typename?: 'RichTextV2Body';
blocknote?: Maybe<Scalars['String']>;
markdown?: Maybe<Scalars['String']>;
};
export type Role = {
__typename?: 'Role';
agents: Array<Agent>;
@@ -3814,6 +3820,11 @@ export type SignedFile = {
token: Scalars['String'];
};
export type StandaloneRichTextConfiguration = {
__typename?: 'StandaloneRichTextConfiguration';
body: RichTextV2Body;
};
export type StandardOverrides = {
__typename?: 'StandardOverrides';
description?: Maybe<Scalars['String']>;
@@ -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
}
@@ -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,
@@ -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,
@@ -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;
@@ -9,6 +9,6 @@ export enum CardType {
WorkflowCard = 'WorkflowCard',
WorkflowVersionCard = 'WorkflowVersionCard',
WorkflowRunCard = 'WorkflowRunCard',
RichTextCard = 'RichTextCard',
FieldRichTextCard = 'FieldRichTextCard',
DashboardCard = 'DashboardCard',
}
@@ -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 = <T extends CardType>(
/>
);
case CardType.RichTextCard:
return <CardRenderer Component={RichTextCard} />;
case CardType.FieldRichTextCard:
return <CardRenderer Component={FieldRichTextCard} />;
case CardType.TaskCard:
return <CardRenderer Component={TasksCard} />;
@@ -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',
@@ -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',
@@ -113,6 +113,12 @@ export const PAGE_LAYOUT_WIDGET_FRAGMENT = gql`
... on IframeConfiguration {
url
}
... on StandaloneRichTextConfiguration {
body {
blocknote
markdown
}
}
}
pageLayoutTabId
}
@@ -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 <NoteWidget widget={widget} />;
case WidgetType.RICH_TEXT:
return <RichTextWidget widget={widget} />;
case WidgetType.FIELD_RICH_TEXT:
return <FieldRichTextWidget widget={widget} />;
case WidgetType.FILES:
return <FileWidget widget={widget} />;
@@ -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 (
<StyledContainer>
<RichTextCard />
<FieldRichTextCard />
</StyledContainer>
);
};
@@ -46,7 +46,7 @@ const LoadingSkeleton = () => {
);
};
export const RichTextCard = () => {
export const FieldRichTextCard = () => {
const targetRecord = useTargetRecord();
const activityBodyV2 = useRecoilValue(
recordStoreFamilySelector({
@@ -0,0 +1,47 @@
import { type MigrationInterface, type QueryRunner } from 'typeorm';
export class RenameRichTextToFieldRichTextAndAddStandaloneRichText1764846384501
implements MigrationInterface
{
name = 'RenameRichTextToFieldRichTextAndAddStandaloneRichText1764846384501';
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
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"`,
);
}
}
@@ -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;
}
@@ -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;
}
@@ -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<string, unknown>) {
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;
@@ -1,4 +1,5 @@
export enum WidgetConfigurationType {
CHART_CONFIG = 'CHART_CONFIG',
IFRAME_CONFIG = 'IFRAME_CONFIG',
STANDALONE_RICH_TEXT_CONFIG = 'STANDALONE_RICH_TEXT_CONFIG',
}
@@ -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',
@@ -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,
@@ -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();
});
});
});
@@ -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;
@@ -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<WidgetConfigurationInterface | null> => {
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<WidgetConfigurationInterface | null> => {
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;
}
@@ -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;