[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:
@@ -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',
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+1
-1
@@ -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',
|
||||
}
|
||||
|
||||
+3
-3
@@ -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} />;
|
||||
|
||||
+2
-2
@@ -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',
|
||||
|
||||
+2
-2
@@ -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',
|
||||
|
||||
+6
@@ -113,6 +113,12 @@ export const PAGE_LAYOUT_WIDGET_FRAGMENT = gql`
|
||||
... on IframeConfiguration {
|
||||
url
|
||||
}
|
||||
... on StandaloneRichTextConfiguration {
|
||||
body {
|
||||
blocknote
|
||||
markdown
|
||||
}
|
||||
}
|
||||
}
|
||||
pageLayoutTabId
|
||||
}
|
||||
|
||||
+3
-3
@@ -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} />;
|
||||
|
||||
+6
-4
@@ -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>
|
||||
);
|
||||
};
|
||||
+1
-1
@@ -46,7 +46,7 @@ const LoadingSkeleton = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const RichTextCard = () => {
|
||||
export const FieldRichTextCard = () => {
|
||||
const targetRecord = useTargetRecord();
|
||||
const activityBodyV2 = useRecoilValue(
|
||||
recordStoreFamilySelector({
|
||||
+47
@@ -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"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+17
@@ -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;
|
||||
}
|
||||
+15
@@ -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;
|
||||
}
|
||||
+11
-1
@@ -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
@@ -1,4 +1,5 @@
|
||||
export enum WidgetConfigurationType {
|
||||
CHART_CONFIG = 'CHART_CONFIG',
|
||||
IFRAME_CONFIG = 'IFRAME_CONFIG',
|
||||
STANDALONE_RICH_TEXT_CONFIG = 'STANDALONE_RICH_TEXT_CONFIG',
|
||||
}
|
||||
|
||||
+2
-1
@@ -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',
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+138
-66
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+9
-2
@@ -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;
|
||||
|
||||
+30
-2
@@ -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;
|
||||
}
|
||||
|
||||
+31
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user