Add configuration types for widgets (#14717)

closes https://github.com/twentyhq/core-team-issues/issues/1532
This commit is contained in:
nitin
2025-09-29 17:11:26 +05:30
committed by GitHub
parent 2685f4a5b9
commit ea144c434b
88 changed files with 2829 additions and 1487 deletions
@@ -1,7 +1,7 @@
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { useCreatePageLayoutGraphWidget } from '@/page-layout/hooks/useCreatePageLayoutGraphWidget';
import { GraphType, WidgetType } from '@/page-layout/mocks/mockWidgets';
import { GraphType, WidgetType } from '~/generated-metadata/graphql';
import styled from '@emotion/styled';
import {
@@ -62,7 +62,10 @@ export const CommandMenuPageLayoutIframeConfig = () => {
const [title, setTitle] = useState(editingWidget?.title || '');
const configUrl = editingWidget?.configuration?.url;
const configUrl =
editingWidget?.configuration && 'url' in editingWidget.configuration
? editingWidget.configuration.url
: undefined;
const [url, setUrl] = useState(isString(configUrl) ? configUrl : '');
@@ -1,12 +1,12 @@
import { useNavigatePageLayoutCommandMenu } from '@/command-menu/pages/page-layout/hooks/useNavigatePageLayoutCommandMenu';
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { WidgetType } from '@/page-layout/mocks/mockWidgets';
import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLayoutDraggedAreaComponentState';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import styled from '@emotion/styled';
import { IconChartPie, IconFrame, IconList } from 'twenty-ui/display';
import { MenuItem } from 'twenty-ui/navigation';
import { WidgetType } from '~/generated-metadata/graphql';
const StyledContainer = styled.div`
display: flex;
@@ -1,7 +1,7 @@
import { type Dashboard } from '@/dashboards/components/types/Dashboard';
import { useSetIsDashboardInEditMode } from '@/dashboards/hooks/useSetDashboardInEditMode';
import { PageLayoutRenderer } from '@/page-layout/components/PageLayoutRenderer';
import { type PageLayout } from '@/page-layout/types/pageLayoutTypes';
import { type PageLayout } from '@/page-layout/types/PageLayout';
import { isPageLayoutEmpty } from '@/page-layout/utils/isPageLayoutEmpty';
type DashboardContentRendererProps = {
@@ -1,18 +1 @@
import { gql } from '@apollo/client';
export const PAGE_LAYOUT_WIDGET_FRAGMENT = gql`
fragment PageLayoutWidgetFragment on PageLayoutWidget {
id
title
type
objectMetadataId
gridPosition {
column
columnSpan
row
rowSpan
}
configuration
pageLayoutTabId
}
`;
export { PAGE_LAYOUT_WIDGET_FRAGMENT } from '@/page-layout/graphql/fragments/pageLayoutWidgetFragment';
@@ -14,7 +14,6 @@ import { pageLayoutCurrentBreakpointComponentState } from '@/page-layout/states/
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
import { WidgetPlaceholder } from '@/page-layout/widgets/components/WidgetPlaceholder';
import { WidgetRenderer } from '@/page-layout/widgets/components/WidgetRenderer';
import { type Widget } from '@/page-layout/widgets/types/Widget';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
@@ -84,7 +83,7 @@ export const PageLayoutGridLayout = () => {
return null;
}
const activeTabWidgets = currentPageLayout?.tabs.find(
const activeTabWidgets = currentPageLayout.tabs.find(
(tab) => tab.id === activeTabId,
)?.widgets;
@@ -141,7 +140,7 @@ export const PageLayoutGridLayout = () => {
) : (
activeTabWidgets?.map((widget) => (
<div key={widget.id} data-select-disable="true">
<WidgetRenderer widget={widget as Widget} />
<WidgetRenderer widget={widget} />
</div>
))
)}
@@ -2,8 +2,9 @@ import { FIND_ONE_PAGE_LAYOUT } from '@/dashboards/graphql/queries/findOnePageLa
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
import { type PageLayout } from '@/page-layout/types/pageLayoutTypes';
import { type PageLayout } from '@/page-layout/types/PageLayout';
import { convertPageLayoutToTabLayouts } from '@/page-layout/utils/convertPageLayoutToTabLayouts';
import { transformPageLayout } from '@/page-layout/utils/transformPageLayout';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
import { useQuery } from '@apollo/client';
@@ -29,7 +30,9 @@ export const PageLayoutInitializationQueryEffect = ({
},
});
const pageLayout: PageLayout | undefined = data?.getPageLayout;
const pageLayout: PageLayout | undefined = data?.getPageLayout
? transformPageLayout(data.getPageLayout)
: undefined;
const pageLayoutPersistedComponentCallbackState =
useRecoilComponentCallbackState(pageLayoutPersistedComponentState);
@@ -2,7 +2,7 @@ import { PageLayoutInitializationQueryEffect } from '@/page-layout/components/Pa
import { PageLayoutRendererContent } from '@/page-layout/components/PageLayoutRendererContent';
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
import { type PageLayout } from '@/page-layout/types/pageLayoutTypes';
import { type PageLayout } from '@/page-layout/types/PageLayout';
import { getTabListInstanceIdFromPageLayoutId } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutId';
import { TabListComponentInstanceContext } from '@/ui/layout/tab-list/states/contexts/TabListComponentInstanceContext';
import 'react-grid-layout/css/styles.css';
@@ -4,8 +4,13 @@ import { expect, within } from '@storybook/test';
import { MemoryRouter } from 'react-router-dom';
import { FIND_ONE_PAGE_LAYOUT } from '@/dashboards/graphql/queries/findOnePageLayout';
import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations';
import { PageLayoutRenderer } from '@/page-layout/components/PageLayoutRenderer';
import { GraphType, WidgetType } from '@/page-layout/mocks/mockWidgets';
import {
GraphOrderBy,
GraphType,
WidgetType,
} from '~/generated-metadata/graphql';
import { RecoilRoot } from 'recoil';
import { PageLayoutType, type PageLayoutWidget } from '~/generated/graphql';
@@ -53,7 +58,10 @@ const mixedGraphsPageLayoutMocks = {
columnSpan: 3,
},
configuration: {
__typename: 'NumberChartConfiguration',
graphType: GraphType.NUMBER,
aggregateOperation: AggregateOperations.COUNT,
aggregateFieldMetadataId: 'id',
},
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
@@ -74,7 +82,12 @@ const mixedGraphsPageLayoutMocks = {
columnSpan: 3,
},
configuration: {
__typename: 'GaugeChartConfiguration',
graphType: GraphType.GAUGE,
aggregateOperation: AggregateOperations.COUNT,
aggregateFieldMetadataId: 'id',
aggregateOperationTotal: AggregateOperations.COUNT,
aggregateFieldMetadataIdTotal: 'id',
},
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
@@ -95,7 +108,12 @@ const mixedGraphsPageLayoutMocks = {
columnSpan: 3,
},
configuration: {
__typename: 'PieChartConfiguration',
graphType: GraphType.PIE,
aggregateOperation: AggregateOperations.COUNT,
aggregateFieldMetadataId: 'id',
groupByFieldMetadataId: 'createdAt',
orderBy: GraphOrderBy.VALUE_DESC,
},
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
@@ -116,7 +134,12 @@ const mixedGraphsPageLayoutMocks = {
columnSpan: 6,
},
configuration: {
__typename: 'BarChartConfiguration',
graphType: GraphType.BAR,
aggregateOperation: AggregateOperations.COUNT,
aggregateFieldMetadataId: 'id',
groupByFieldMetadataIdX: 'createdAt',
orderByX: GraphOrderBy.FIELD_ASC,
},
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
@@ -0,0 +1,74 @@
import { gql } from '@apollo/client';
export const PAGE_LAYOUT_WIDGET_FRAGMENT = gql`
fragment PageLayoutWidgetFragment on PageLayoutWidget {
id
title
type
objectMetadataId
createdAt
updatedAt
deletedAt
gridPosition {
column
columnSpan
row
rowSpan
}
configuration {
... on BarChartConfiguration {
graphType
aggregateFieldMetadataId
aggregateOperation
groupByFieldMetadataIdX
orderByX
groupByFieldMetadataIdY
orderByY
omitNullValues
xAxisName
yAxisName
rangeMin
rangeMax
filter
}
... on LineChartConfiguration {
graphType
aggregateFieldMetadataId
aggregateOperation
groupByFieldMetadataIdX
orderByX
groupByFieldMetadataIdY
orderByY
filter
}
... on PieChartConfiguration {
graphType
groupByFieldMetadataId
aggregateFieldMetadataId
aggregateOperation
orderBy
filter
}
... on NumberChartConfiguration {
graphType
aggregateFieldMetadataId
aggregateOperation
description
filter
}
... on GaugeChartConfiguration {
graphType
aggregateFieldMetadataId
aggregateOperation
aggregateOperationTotal
aggregateFieldMetadataIdTotal
description
filter
}
... on IframeConfiguration {
url
}
}
pageLayoutTabId
}
`;
@@ -1,6 +1,9 @@
import { gql } from '@apollo/client';
import { PAGE_LAYOUT_WIDGET_FRAGMENT } from '../fragments/pageLayoutWidgetFragment';
export const UPDATE_PAGE_LAYOUT_WITH_TABS_AND_WIDGETS = gql`
${PAGE_LAYOUT_WIDGET_FRAGMENT}
mutation UpdatePageLayoutWithTabsAndWidgets(
$id: String!
$input: UpdatePageLayoutWithTabsInput!
@@ -19,20 +22,7 @@ export const UPDATE_PAGE_LAYOUT_WITH_TABS_AND_WIDGETS = gql`
position
pageLayoutId
widgets {
id
title
type
pageLayoutTabId
objectMetadataId
gridPosition {
row
column
rowSpan
columnSpan
}
configuration
createdAt
updatedAt
...PageLayoutWidgetFragment
}
createdAt
updatedAt
@@ -1,7 +1,7 @@
import { useCreatePageLayoutGraphWidget } from '@/page-layout/hooks/useCreatePageLayoutGraphWidget';
import { GraphType, WidgetType } from '@/page-layout/mocks/mockWidgets';
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { GraphType, WidgetType } from '~/generated-metadata/graphql';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
@@ -180,7 +180,11 @@ describe('useCreatePageLayoutGraphWidget', () => {
const widget = result.current.allWidgets[index];
expect(widget.type).toBe(WidgetType.GRAPH);
expect(widget.pageLayoutTabId).toBe('tab-1');
expect(widget.configuration.graphType).toBe(graphType);
expect(
widget.configuration && 'graphType' in widget.configuration
? widget.configuration.graphType
: null,
).toBe(graphType);
expect(widget.id).toBe('mock-uuid');
});
@@ -1,4 +1,9 @@
import { GraphType, WidgetType } from '@/page-layout/mocks/mockWidgets';
import {
GraphOrderBy,
GraphType,
WidgetType,
} from '~/generated-metadata/graphql';
import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations';
import { act, renderHook } from '@testing-library/react';
import { PageLayoutType } from '~/generated/graphql';
import { usePageLayoutDraftState } from '../usePageLayoutDraftState';
@@ -95,7 +100,13 @@ describe('usePageLayoutDraftState', () => {
title: 'New Widget',
type: WidgetType.GRAPH,
gridPosition: { row: 2, column: 2, rowSpan: 2, columnSpan: 2 },
configuration: { graphType: GraphType.BAR },
configuration: {
graphType: GraphType.BAR,
aggregateOperation: AggregateOperations.COUNT,
aggregateFieldMetadataId: 'id',
groupByFieldMetadataIdX: 'createdAt',
orderByX: GraphOrderBy.FIELD_ASC,
},
objectMetadataId: null,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
@@ -1,9 +1,9 @@
import { type GraphType } from '@/page-layout/mocks/mockWidgets';
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLayoutDraggedAreaComponentState';
import { addWidgetToTab } from '@/page-layout/utils/addWidgetToTab';
import { createDefaultGraphWidget } from '@/page-layout/utils/createDefaultGraphWidget';
import {
getWidgetSize,
getWidgetTitle,
@@ -11,12 +11,13 @@ import {
import { getDefaultWidgetPosition } from '@/page-layout/utils/getDefaultWidgetPosition';
import { getTabListInstanceIdFromPageLayoutId } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutId';
import { getUpdatedTabLayouts } from '@/page-layout/utils/getUpdatedTabLayouts';
import { type GraphType } from '~/generated-metadata/graphql';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { useRecoilCallback } from 'recoil';
import { v4 as uuidv4 } from 'uuid';
import { type PageLayoutWidget, type WidgetType } from '~/generated/graphql';
import { type WidgetType } from '~/generated/graphql';
export const useCreatePageLayoutGraphWidget = (
pageLayoutIdFromProps?: string,
@@ -72,7 +73,10 @@ export const useCreatePageLayoutGraphWidget = (
const allWidgets = pageLayoutDraft.tabs.flatMap((tab) => tab.widgets);
const existingWidgetCount = allWidgets.filter(
(w) =>
w.type === widgetType && w.configuration.graphType === graphType,
w.type === widgetType &&
w.configuration &&
'graphType' in w.configuration &&
w.configuration.graphType === graphType,
).length;
const title = getWidgetTitle(graphType, existingWidgetCount);
const widgetId = uuidv4();
@@ -83,25 +87,18 @@ export const useCreatePageLayoutGraphWidget = (
defaultSize,
);
const newWidget: PageLayoutWidget = {
id: widgetId,
pageLayoutTabId: activeTabId,
const newWidget = createDefaultGraphWidget(
widgetId,
activeTabId,
title,
type: widgetType,
gridPosition: {
graphType,
{
row: position.y,
column: position.x,
rowSpan: position.h,
columnSpan: position.w,
},
configuration: {
graphType,
},
objectMetadataId: null,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
deletedAt: null,
};
);
const newLayout = {
i: widgetId,
@@ -3,6 +3,7 @@ import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pag
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLayoutDraggedAreaComponentState';
import { addWidgetToTab } from '@/page-layout/utils/addWidgetToTab';
import { createDefaultIframeWidget } from '@/page-layout/utils/createDefaultIframeWidget';
import { getDefaultWidgetPosition } from '@/page-layout/utils/getDefaultWidgetPosition';
import { getTabListInstanceIdFromPageLayoutId } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutId';
import { getUpdatedTabLayouts } from '@/page-layout/utils/getUpdatedTabLayouts';
@@ -12,7 +13,6 @@ import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useRecoilCallback } from 'recoil';
import { v4 as uuidv4 } from 'uuid';
import { type PageLayoutWidget, WidgetType } from '~/generated/graphql';
export const useCreatePageLayoutIframeWidget = (
pageLayoutIdFromProps?: string,
@@ -64,25 +64,18 @@ export const useCreatePageLayoutIframeWidget = (
defaultSize,
);
const newWidget: PageLayoutWidget = {
id: widgetId,
pageLayoutTabId: activeTabId,
const newWidget = createDefaultIframeWidget(
widgetId,
activeTabId,
title,
type: WidgetType.IFRAME,
gridPosition: {
url,
{
row: position.y,
column: position.x,
rowSpan: position.h,
columnSpan: position.w,
},
configuration: {
url,
},
objectMetadataId: null,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
deletedAt: null,
};
);
const newLayout = {
i: widgetId,
@@ -5,7 +5,7 @@ import { useRecoilCallback } from 'recoil';
import { v4 as uuidv4 } from 'uuid';
import { pageLayoutCurrentLayoutsComponentState } from '../states/pageLayoutCurrentLayoutsComponentState';
import { pageLayoutDraftComponentState } from '../states/pageLayoutDraftComponentState';
import { type PageLayoutTab } from '../types/pageLayoutTypes';
import { type PageLayoutTab } from '../types/PageLayoutTab';
import { getEmptyTabLayout } from '../utils/getEmptyTabLayout';
export const useCreatePageLayoutTab = (pageLayoutIdFromProps?: string) => {
@@ -71,22 +71,9 @@ export const usePageLayoutHandleLayoutChange = (
...prev,
tabs: prev.tabs.map((tab) => {
if (tab.id === activeTabId) {
const tabWidgets = updatedWidgets
.filter((w) => w.pageLayoutTabId === activeTabId)
.map((widget) => ({
id: widget.id,
pageLayoutTabId: widget.pageLayoutTabId || activeTabId,
title: widget.title,
type: widget.type,
objectMetadataId: null,
gridPosition: widget.gridPosition,
configuration: widget.configuration || undefined,
createdAt:
tab.widgets.find((w) => w.id === widget.id)?.createdAt ||
new Date().toISOString(),
updatedAt: new Date().toISOString(),
deletedAt: null,
}));
const tabWidgets = updatedWidgets.filter(
(w) => w.pageLayoutTabId === activeTabId,
);
return {
...tab,
widgets: tabWidgets,
@@ -2,9 +2,10 @@ import { usePageLayoutDraftState } from '@/page-layout/hooks/usePageLayoutDraftS
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
import { type PageLayout } from '@/page-layout/types/pageLayoutTypes';
import { type PageLayout } from '@/page-layout/types/PageLayout';
import { convertPageLayoutDraftToUpdateInput } from '@/page-layout/utils/convertPageLayoutDraftToUpdateInput';
import { convertPageLayoutToTabLayouts } from '@/page-layout/utils/convertPageLayoutToTabLayouts';
import { transformPageLayout } from '@/page-layout/utils/transformPageLayout';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { useRecoilCallback } from 'recoil';
@@ -48,35 +49,8 @@ export const useSavePageLayout = (pageLayoutIdFromProps: string) => {
const updatedPageLayout = data?.updatePageLayoutWithTabsAndWidgets;
if (isDefined(updatedPageLayout)) {
const pageLayoutToPersist: PageLayout = {
id: updatedPageLayout.id,
name: updatedPageLayout.name,
type: updatedPageLayout.type,
objectMetadataId: updatedPageLayout.objectMetadataId,
tabs:
updatedPageLayout.tabs?.map((tab) => ({
id: tab.id,
title: tab.title,
position: tab.position,
pageLayoutId: tab.pageLayoutId,
createdAt: tab.createdAt,
updatedAt: tab.updatedAt,
widgets:
tab.widgets?.map((widget) => ({
id: widget.id,
title: widget.title,
type: widget.type,
pageLayoutTabId: widget.pageLayoutTabId,
objectMetadataId: widget.objectMetadataId,
configuration: widget.configuration,
gridPosition: widget.gridPosition,
createdAt: widget.createdAt,
updatedAt: widget.updatedAt,
})) ?? [],
})) ?? [],
createdAt: updatedPageLayout.createdAt,
updatedAt: updatedPageLayout.updatedAt,
};
const pageLayoutToPersist: PageLayout =
transformPageLayout(updatedPageLayout);
set(pageLayoutPersistedCallbackState, pageLayoutToPersist);
set(
@@ -1,9 +1,9 @@
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { type PageLayoutWidget } from '~/generated/graphql';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { useRecoilCallback } from 'recoil';
import { type PageLayoutWidget } from '~/generated/graphql';
export const useUpdatePageLayoutWidget = (pageLayoutIdFromProps?: string) => {
const pageLayoutId = useAvailableComponentInstanceIdOrThrow(
@@ -1,159 +0,0 @@
import { type Layouts } from 'react-grid-layout';
import { type PageLayoutWidget } from '~/generated/graphql';
export enum WidgetType {
VIEW = 'VIEW',
IFRAME = 'IFRAME',
FIELDS = 'FIELDS',
GRAPH = 'GRAPH',
}
export enum GraphType {
NUMBER = 'NUMBER',
GAUGE = 'GAUGE',
PIE = 'PIE',
BAR = 'BAR',
LINE = 'LINE',
}
export const mockPageLayoutWidgets: PageLayoutWidget[] = [
{
id: 'widget-1',
pageLayoutTabId: 'tab-overview',
type: WidgetType.GRAPH,
title: 'Sales Pipeline',
objectMetadataId: null,
gridPosition: {
row: 0,
column: 0,
rowSpan: 2,
columnSpan: 3,
},
configuration: {
graphType: GraphType.NUMBER,
},
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
deletedAt: null,
},
{
id: 'widget-2',
pageLayoutTabId: 'tab-overview',
type: WidgetType.GRAPH,
title: 'Conversion Rate',
objectMetadataId: null,
gridPosition: {
row: 0,
column: 6,
rowSpan: 5,
columnSpan: 3,
},
configuration: {
graphType: GraphType.GAUGE,
},
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
deletedAt: null,
},
{
id: 'widget-3',
pageLayoutTabId: 'tab-analytics',
type: WidgetType.GRAPH,
title: 'Lead Distribution',
objectMetadataId: null,
gridPosition: {
row: 2,
column: 0,
rowSpan: 5,
columnSpan: 6,
},
configuration: {
graphType: GraphType.PIE,
},
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
deletedAt: null,
},
{
id: 'widget-4',
pageLayoutTabId: 'tab-reports',
type: WidgetType.GRAPH,
title: 'Monthly Performance',
objectMetadataId: null,
gridPosition: {
row: 0,
column: 9,
rowSpan: 8,
columnSpan: 4,
},
configuration: {
graphType: GraphType.BAR,
},
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
deletedAt: null,
},
];
export const mockLayouts: Layouts = {
desktop: [
{
i: 'widget-1',
x: 0,
y: 0,
w: 3,
h: 2,
},
{
i: 'widget-2',
x: 6,
y: 0,
w: 3,
h: 5,
},
{
i: 'widget-3',
x: 0,
y: 2,
w: 6,
h: 5,
},
{
i: 'widget-4',
x: 9,
y: 0,
w: 4,
h: 8,
},
],
mobile: [
{
i: 'widget-1',
x: 0,
y: 0,
w: 1,
h: 2,
},
{
i: 'widget-2',
x: 0,
y: 2,
w: 1,
h: 5,
},
{
i: 'widget-3',
x: 0,
y: 7,
w: 1,
h: 5,
},
{
i: 'widget-4',
x: 0,
y: 12,
w: 1,
h: 5,
},
],
};
@@ -1,6 +1,6 @@
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
import { type PageLayout } from '../types/pageLayoutTypes';
import { type PageLayout } from '@/page-layout/types/PageLayout';
import { PageLayoutComponentInstanceContext } from './contexts/PageLayoutComponentInstanceContext';
export const pageLayoutPersistedComponentState = createComponentState<
@@ -1,5 +1,5 @@
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
import { type PageLayout } from '../types/pageLayoutTypes';
import { type PageLayout } from '../types/PageLayout';
import { PageLayoutComponentInstanceContext } from './contexts/PageLayoutComponentInstanceContext';
@@ -0,0 +1,6 @@
import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab';
import { type PageLayout as PageLayoutGenerated } from '~/generated/graphql';
export type PageLayout = Omit<PageLayoutGenerated, 'tabs'> & {
tabs: PageLayoutTab[];
};
@@ -1,5 +1,4 @@
import {
type PageLayout as PageLayoutGenerated,
type PageLayoutTab as PageLayoutTabGenerated,
type PageLayoutWidget,
} from '~/generated/graphql';
@@ -7,7 +6,3 @@ import {
export type PageLayoutTab = Omit<PageLayoutTabGenerated, 'widgets'> & {
widgets: PageLayoutWidget[];
};
export type PageLayout = Omit<PageLayoutGenerated, 'tabs'> & {
tabs: PageLayoutTab[];
};
@@ -1,4 +1,4 @@
import { type PageLayout } from './pageLayoutTypes';
import { type PageLayout } from '@/page-layout/types/PageLayout';
export type DraftPageLayout = Omit<
PageLayout,
@@ -1,14 +1,21 @@
import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations';
import { GraphType, WidgetType } from '~/generated-metadata/graphql';
import { type PageLayoutTab } from '../../types/PageLayoutTab';
import { type PageLayoutWidget } from '~/generated/graphql';
import { WidgetType } from '../../mocks/mockWidgets';
import { type PageLayoutTab } from '../../types/pageLayoutTypes';
import { addWidgetToTab } from '../addWidgetToTab';
describe('addWidgetToTab', () => {
const mockWidget: PageLayoutWidget = {
__typename: 'PageLayoutWidget',
id: 'widget-1',
pageLayoutTabId: 'tab-1',
title: 'Test Widget',
type: WidgetType.GRAPH,
configuration: {
graphType: GraphType.NUMBER,
aggregateOperation: AggregateOperations.COUNT,
aggregateFieldMetadataId: 'id',
},
gridPosition: { row: 0, column: 0, rowSpan: 2, columnSpan: 2 },
objectMetadataId: null,
createdAt: '2024-01-01T00:00:00Z',
@@ -1,5 +1,10 @@
import { type PageLayoutWidget } from '~/generated/graphql';
import { GraphType, WidgetType } from '../../mocks/mockWidgets';
import {
GraphOrderBy,
GraphType,
WidgetType,
type PageLayoutWidget,
} from '~/generated-metadata/graphql';
import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations';
import { convertLayoutsToWidgets } from '../convertLayoutsToWidgets';
describe('convertLayoutsToWidgets', () => {
@@ -18,6 +23,8 @@ describe('convertLayoutsToWidgets', () => {
},
configuration: {
graphType: GraphType.NUMBER,
aggregateOperation: AggregateOperations.COUNT,
aggregateFieldMetadataId: 'id',
},
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
@@ -37,6 +44,10 @@ describe('convertLayoutsToWidgets', () => {
},
configuration: {
graphType: GraphType.PIE,
aggregateOperation: AggregateOperations.COUNT,
aggregateFieldMetadataId: 'id',
groupByFieldMetadataId: 'status',
orderBy: GraphOrderBy.VALUE_DESC,
},
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
@@ -1,6 +1,12 @@
import { type PageLayout } from '@/page-layout/types/pageLayoutTypes';
import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations';
import { type PageLayout } from '@/page-layout/types/PageLayout';
import { convertPageLayoutToTabLayouts } from '@/page-layout/utils/convertPageLayoutToTabLayouts';
import { PageLayoutType, WidgetType } from '~/generated/graphql';
import {
GraphOrderBy,
GraphType,
PageLayoutType,
WidgetType,
} from '~/generated-metadata/graphql';
describe('convertPageLayoutToTabLayouts', () => {
it('should convert page layout to tab layouts', () => {
@@ -17,10 +23,16 @@ describe('convertPageLayoutToTabLayouts', () => {
pageLayoutId: 'page-layout-1',
widgets: [
{
__typename: 'PageLayoutWidget',
id: 'widget-1',
pageLayoutTabId: 'tab-1',
title: 'Widget 1',
type: WidgetType.GRAPH,
configuration: {
graphType: GraphType.NUMBER,
aggregateOperation: AggregateOperations.COUNT,
aggregateFieldMetadataId: 'id',
},
gridPosition: { row: 0, column: 0, rowSpan: 2, columnSpan: 2 },
objectMetadataId: 'object-metadata-1',
createdAt: '2025-01-01T00:00:00.000Z',
@@ -28,10 +40,18 @@ describe('convertPageLayoutToTabLayouts', () => {
deletedAt: null,
},
{
__typename: 'PageLayoutWidget',
id: 'widget-2',
pageLayoutTabId: 'tab-1',
title: 'Widget 2',
type: WidgetType.GRAPH,
configuration: {
graphType: GraphType.PIE,
aggregateOperation: AggregateOperations.COUNT,
aggregateFieldMetadataId: 'id',
groupByFieldMetadataId: 'status',
orderBy: GraphOrderBy.VALUE_DESC,
},
gridPosition: { row: 2, column: 0, rowSpan: 2, columnSpan: 2 },
objectMetadataId: 'object-metadata-1',
createdAt: '2025-01-01T00:00:00.000Z',
@@ -1,5 +1,10 @@
import { WidgetType } from '../../mocks/mockWidgets';
import { type PageLayoutTab } from '../../types/pageLayoutTypes';
import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations';
import {
GraphOrderBy,
GraphType,
WidgetType,
} from '~/generated-metadata/graphql';
import { type PageLayoutTab } from '../../types/PageLayoutTab';
import { removeWidgetFromTab } from '../removeWidgetFromTab';
describe('removeWidgetFromTab', () => {
@@ -11,10 +16,16 @@ describe('removeWidgetFromTab', () => {
pageLayoutId: 'layout-1',
widgets: [
{
__typename: 'PageLayoutWidget' as const,
id: 'widget-1',
pageLayoutTabId: 'tab-1',
title: 'Widget 1',
type: WidgetType.GRAPH,
configuration: {
graphType: GraphType.NUMBER,
aggregateOperation: AggregateOperations.COUNT,
aggregateFieldMetadataId: 'id',
},
gridPosition: { row: 0, column: 0, rowSpan: 2, columnSpan: 2 },
objectMetadataId: null,
createdAt: '2024-01-01T00:00:00Z',
@@ -22,10 +33,18 @@ describe('removeWidgetFromTab', () => {
deletedAt: null,
},
{
__typename: 'PageLayoutWidget' as const,
id: 'widget-2',
pageLayoutTabId: 'tab-1',
title: 'Widget 2',
type: WidgetType.GRAPH,
configuration: {
graphType: GraphType.PIE,
aggregateOperation: AggregateOperations.COUNT,
aggregateFieldMetadataId: 'id',
groupByFieldMetadataId: 'status',
orderBy: GraphOrderBy.VALUE_DESC,
},
gridPosition: { row: 2, column: 0, rowSpan: 2, columnSpan: 2 },
objectMetadataId: null,
createdAt: '2024-01-01T00:00:00Z',
@@ -44,10 +63,14 @@ describe('removeWidgetFromTab', () => {
pageLayoutId: 'layout-1',
widgets: [
{
__typename: 'PageLayoutWidget' as const,
id: 'widget-3',
pageLayoutTabId: 'tab-2',
title: 'Widget 3',
type: WidgetType.IFRAME,
configuration: {
url: 'https://example.com',
},
gridPosition: { row: 0, column: 0, rowSpan: 2, columnSpan: 2 },
objectMetadataId: null,
createdAt: '2024-01-01T00:00:00Z',
@@ -1,4 +1,4 @@
import { type PageLayoutTab } from '@/page-layout/types/pageLayoutTypes';
import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab';
import { type PageLayoutWidget } from '~/generated/graphql';
export const addWidgetToTab = (
@@ -1,5 +1,5 @@
import { type Layouts } from 'react-grid-layout';
import { type PageLayoutWidget } from '~/generated/graphql';
import { type Layouts } from 'react-grid-layout';
export const convertLayoutsToWidgets = (
widgets: PageLayoutWidget[],
@@ -1,4 +1,4 @@
import { type PageLayout } from '@/page-layout/types/pageLayoutTypes';
import { type PageLayout } from '@/page-layout/types/PageLayout';
import { type TabLayouts } from '@/page-layout/types/tab-layouts';
export const convertPageLayoutToTabLayouts = (
@@ -0,0 +1,91 @@
import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations';
import { v4 as uuidv4 } from 'uuid';
import { GraphOrderBy, GraphType } from '~/generated-metadata/graphql';
import {
type GridPosition,
type PageLayoutWidget,
type WidgetConfiguration,
WidgetType,
} from '~/generated/graphql';
const createDefaultGraphConfiguration = (
graphType: GraphType,
): WidgetConfiguration => {
const placeholderFieldId1 = uuidv4();
const placeholderFieldId2 = uuidv4();
switch (graphType) {
case GraphType.NUMBER:
return {
graphType: GraphType.NUMBER,
aggregateOperation: AggregateOperations.COUNT,
aggregateFieldMetadataId: placeholderFieldId1,
};
case GraphType.PIE:
return {
graphType: GraphType.PIE,
aggregateOperation: AggregateOperations.COUNT,
aggregateFieldMetadataId: placeholderFieldId1,
groupByFieldMetadataId: placeholderFieldId2,
orderBy: GraphOrderBy.VALUE_DESC,
};
case GraphType.BAR:
return {
graphType: GraphType.BAR,
aggregateOperation: AggregateOperations.COUNT,
aggregateFieldMetadataId: placeholderFieldId1,
groupByFieldMetadataIdX: placeholderFieldId2,
orderByX: GraphOrderBy.FIELD_ASC,
};
case GraphType.LINE:
return {
graphType: GraphType.LINE,
aggregateOperation: AggregateOperations.COUNT,
aggregateFieldMetadataId: placeholderFieldId1,
groupByFieldMetadataIdX: placeholderFieldId2,
orderByX: GraphOrderBy.FIELD_ASC,
};
case GraphType.GAUGE:
return {
graphType: GraphType.GAUGE,
aggregateOperation: AggregateOperations.COUNT,
aggregateFieldMetadataId: placeholderFieldId1,
aggregateOperationTotal: AggregateOperations.COUNT,
aggregateFieldMetadataIdTotal: placeholderFieldId2,
};
default:
return {
graphType: GraphType.NUMBER,
aggregateOperation: AggregateOperations.COUNT,
aggregateFieldMetadataId: placeholderFieldId1,
};
}
};
export const createDefaultGraphWidget = (
id: string,
pageLayoutTabId: string,
title: string,
graphType: GraphType,
gridPosition: GridPosition,
objectMetadataId?: string | null,
): PageLayoutWidget => {
return {
__typename: 'PageLayoutWidget',
id,
pageLayoutTabId,
title,
type: WidgetType.GRAPH,
configuration: createDefaultGraphConfiguration(graphType),
gridPosition,
objectMetadataId: objectMetadataId ?? null,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
deletedAt: null,
};
};
@@ -0,0 +1,30 @@
import {
type GridPosition,
type PageLayoutWidget,
WidgetType,
} from '~/generated/graphql';
export const createDefaultIframeWidget = (
id: string,
pageLayoutTabId: string,
title: string,
url: string,
gridPosition: GridPosition,
objectMetadataId?: string | null,
): PageLayoutWidget => {
return {
__typename: 'PageLayoutWidget',
id,
pageLayoutTabId,
title,
type: WidgetType.IFRAME,
configuration: {
url,
},
gridPosition,
objectMetadataId: objectMetadataId ?? null,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
deletedAt: null,
};
};
@@ -1,4 +1,4 @@
import { GraphType } from '../mocks/mockWidgets';
import { GraphType } from '~/generated-metadata/graphql';
export const getDefaultWidgetData = (graphType: GraphType) => {
switch (graphType) {
@@ -1,4 +1,4 @@
import { type PageLayout } from '~/modules/page-layout/types/pageLayoutTypes';
import { type PageLayout } from '@/page-layout/types/PageLayout';
export const isPageLayoutEmpty = (pageLayout: PageLayout): boolean => {
return (
@@ -1,4 +1,4 @@
import { type PageLayoutTab } from '../types/pageLayoutTypes';
import { type PageLayoutTab } from '../types/PageLayoutTab';
export const removeWidgetFromTab = (
tabs: PageLayoutTab[],
@@ -0,0 +1,17 @@
import { type PageLayout } from '@/page-layout/types/PageLayout';
import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab';
import { type PageLayout as PageLayoutGenerated } from '~/generated/graphql';
export const transformPageLayout = (
pageLayout: PageLayoutGenerated,
): PageLayout => {
return {
...pageLayout,
tabs: (pageLayout.tabs ?? []).map(
(tab): PageLayoutTab => ({
...tab,
widgets: tab.widgets ?? [],
}),
),
};
};
@@ -1,8 +1,13 @@
import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import styled from '@emotion/styled';
import { type ReactNode } from 'react';
import { isDefined } from 'twenty-shared/utils';
const StyledContainer = styled.div<{ onClick?: () => void }>`
const StyledContainer = styled.div<{
onClick?: () => void;
showHover?: boolean;
}>`
background: ${({ theme }) => theme.background.secondary};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: ${({ theme }) => theme.border.radius.md};
@@ -14,21 +19,36 @@ const StyledContainer = styled.div<{ onClick?: () => void }>`
position: relative;
padding: ${({ theme }) => theme.spacing(4)};
&:hover {
cursor: ${({ onClick }) => (isDefined(onClick) ? 'pointer' : 'default')};
border: 1px solid ${({ theme }) => theme.color.blue};
background: ${({ theme }) => theme.background.primary};
}
${({ showHover, onClick, theme }) =>
showHover &&
`
&:hover {
cursor: ${isDefined(onClick) ? 'pointer' : 'default'};
border: 1px solid ${theme.color.blue};
background: ${theme.background.primary};
}
`}
`;
type WidgetContainerProps = {
children?: ReactNode;
onClick?: () => void;
isRestricted?: boolean;
};
export const WidgetContainer = ({
children,
onClick,
isRestricted = false,
}: WidgetContainerProps) => {
return <StyledContainer onClick={onClick}>{children}</StyledContainer>;
const isPageLayoutInEditModeComponent = useRecoilComponentValue(
isPageLayoutInEditModeComponentState,
);
const showHover = !isRestricted || isPageLayoutInEditModeComponent;
return (
<StyledContainer onClick={onClick} showHover={showHover}>
{children}
</StyledContainer>
);
};
@@ -1,11 +1,9 @@
import { GraphWidgetRenderer } from '@/page-layout/widgets/graph/components/GraphWidgetRenderer';
import { IframeWidget } from '@/page-layout/widgets/iframe/components/IframeWidget';
import { type Widget } from '@/page-layout/widgets/types/Widget';
import { isString } from '@sniptt/guards';
import { WidgetType } from '~/generated/graphql';
import { type PageLayoutWidget, WidgetType } from '~/generated/graphql';
type WidgetContentRendererProps = {
widget: Widget;
widget: PageLayoutWidget;
};
export const WidgetContentRenderer = ({
@@ -15,12 +13,9 @@ export const WidgetContentRenderer = ({
case WidgetType.GRAPH:
return <GraphWidgetRenderer widget={widget} />;
case WidgetType.IFRAME: {
const url = widget.configuration?.url;
return (
<IframeWidget url={isString(url) ? url : ''} title={widget.title} />
);
}
case WidgetType.IFRAME:
return <IframeWidget widget={widget} />;
default:
return null;
}
@@ -1,15 +1,17 @@
import { ForbiddenFieldDisplay } from '@/object-record/record-field/ui/meta-types/display/components/ForbiddenFieldDisplay';
import { useDeletePageLayoutWidget } from '@/page-layout/hooks/useDeletePageLayoutWidget';
import { useEditPageLayoutWidget } from '@/page-layout/hooks/useEditPageLayoutWidget';
import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
import { WidgetContainer } from '@/page-layout/widgets/components/WidgetContainer';
import { WidgetContentRenderer } from '@/page-layout/widgets/components/WidgetContentRenderer';
import { WidgetHeader } from '@/page-layout/widgets/components/WidgetHeader';
import { type Widget as WidgetType } from '@/page-layout/widgets/types/Widget';
import { useWidgetObjectPermissions } from '@/page-layout/widgets/hooks/useWidgetObjectPermissions';
import { type PageLayoutWidget } from '~/generated/graphql';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import styled from '@emotion/styled';
type WidgetRendererProps = {
widget: WidgetType;
widget: PageLayoutWidget;
};
const StyledContent = styled.div`
@@ -23,13 +25,14 @@ const StyledContent = styled.div`
export const WidgetRenderer = ({ widget }: WidgetRendererProps) => {
const { deletePageLayoutWidget } = useDeletePageLayoutWidget();
const { handleEditWidget } = useEditPageLayoutWidget();
const { haveAccessToWidgetsObject } = useWidgetObjectPermissions(widget);
const isPageLayoutInEditMode = useRecoilComponentValue(
isPageLayoutInEditModeComponentState,
);
return (
<WidgetContainer>
<WidgetContainer isRestricted={!haveAccessToWidgetsObject}>
<WidgetHeader
isInEditMode={isPageLayoutInEditMode}
title={widget.title}
@@ -39,7 +42,11 @@ export const WidgetRenderer = ({ widget }: WidgetRendererProps) => {
onRemove={() => deletePageLayoutWidget(widget.id)}
/>
<StyledContent>
<WidgetContentRenderer widget={widget} />
{!haveAccessToWidgetsObject ? (
<ForbiddenFieldDisplay />
) : (
<WidgetContentRenderer widget={widget} />
)}
</StyledContent>
</WidgetContainer>
);
@@ -1,3 +1,4 @@
import { PageLayoutTestWrapper } from '@/page-layout/hooks/__tests__/PageLayoutTestWrapper';
import { WidgetPlaceholder } from '@/page-layout/widgets/components/WidgetPlaceholder';
import { type Meta, type StoryObj } from '@storybook/react';
import { ComponentDecorator } from 'twenty-ui/testing';
@@ -5,7 +6,14 @@ import { ComponentDecorator } from 'twenty-ui/testing';
const meta: Meta<typeof WidgetPlaceholder> = {
title: 'Modules/PageLayout/Widgets/WidgetPlaceholder',
component: WidgetPlaceholder,
decorators: [ComponentDecorator],
decorators: [
(Story) => (
<PageLayoutTestWrapper>
<Story />
</PageLayoutTestWrapper>
),
ComponentDecorator,
],
parameters: {
layout: 'centered',
docs: {
@@ -1,9 +1,9 @@
import { PageLayoutTestWrapper } from '@/page-layout/hooks/__tests__/PageLayoutTestWrapper';
import { GraphType } from '@/page-layout/mocks/mockWidgets';
import { createDefaultGraphWidget } from '@/page-layout/utils/createDefaultGraphWidget';
import { WidgetRenderer } from '@/page-layout/widgets/components/WidgetRenderer';
import { GraphType } from '~/generated-metadata/graphql';
import { type Meta, type StoryObj } from '@storybook/react';
import { ComponentDecorator } from 'twenty-ui/testing';
import { WidgetType } from '~/generated/graphql';
const meta: Meta<typeof WidgetRenderer> = {
title: 'Modules/PageLayout/Widgets/WidgetRenderer',
@@ -30,23 +30,18 @@ type Story = StoryObj<typeof WidgetRenderer>;
export const WithNumberChart: Story = {
args: {
widget: {
title: 'Sales Pipeline',
type: WidgetType.GRAPH,
configuration: {
graphType: GraphType.NUMBER,
},
gridPosition: {
widget: createDefaultGraphWidget(
'widget-1',
'tab-overview',
'Sales Pipeline',
GraphType.NUMBER,
{
row: 0,
column: 0,
rowSpan: 2,
columnSpan: 3,
},
id: 'widget-1',
pageLayoutTabId: 'tab-overview',
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
},
),
},
render: (args) => (
<div style={{ width: '300px', height: '100px' }}>
@@ -57,23 +52,18 @@ export const WithNumberChart: Story = {
export const WithGaugeChart: Story = {
args: {
widget: {
title: 'Conversion Rate',
type: WidgetType.GRAPH,
configuration: {
graphType: GraphType.GAUGE,
},
createdAt: '2024-01-01T00:00:00Z',
gridPosition: {
widget: createDefaultGraphWidget(
'widget-1',
'tab-overview',
'Conversion Rate',
GraphType.GAUGE,
{
row: 0,
column: 0,
rowSpan: 5,
columnSpan: 3,
},
id: 'widget-1',
pageLayoutTabId: 'tab-overview',
updatedAt: '2024-01-01T00:00:00Z',
},
),
},
render: (args) => (
<div style={{ width: '300px', height: '400px' }}>
@@ -84,23 +74,18 @@ export const WithGaugeChart: Story = {
export const WithPieChart: Story = {
args: {
widget: {
title: 'Lead Distribution',
type: WidgetType.GRAPH,
configuration: {
graphType: GraphType.PIE,
},
createdAt: '2024-01-01T00:00:00Z',
gridPosition: {
widget: createDefaultGraphWidget(
'widget-1',
'tab-overview',
'Lead Distribution',
GraphType.PIE,
{
row: 0,
column: 0,
rowSpan: 5,
columnSpan: 3,
},
id: 'widget-1',
pageLayoutTabId: 'tab-overview',
updatedAt: '2024-01-01T00:00:00Z',
},
),
},
render: (args) => (
<div style={{ width: '300px', height: '500px' }}>
@@ -111,23 +96,18 @@ export const WithPieChart: Story = {
export const SmallWidget: Story = {
args: {
widget: {
title: 'Small Widget (2x2 grid)',
type: WidgetType.GRAPH,
configuration: {
graphType: GraphType.NUMBER,
},
createdAt: '2024-01-01T00:00:00Z',
gridPosition: {
widget: createDefaultGraphWidget(
'widget-1',
'tab-overview',
'Small Widget (2x2 grid)',
GraphType.NUMBER,
{
row: 0,
column: 0,
rowSpan: 2,
columnSpan: 2,
},
id: 'widget-1',
pageLayoutTabId: 'tab-overview',
updatedAt: '2024-01-01T00:00:00Z',
},
),
},
parameters: {
docs: {
@@ -145,23 +125,18 @@ export const SmallWidget: Story = {
export const MediumWidget: Story = {
args: {
widget: {
title: 'Medium Widget (4x3 grid)',
type: WidgetType.GRAPH,
configuration: {
graphType: GraphType.GAUGE,
},
createdAt: '2024-01-01T00:00:00Z',
gridPosition: {
widget: createDefaultGraphWidget(
'widget-1',
'tab-overview',
'Medium Widget (4x3 grid)',
GraphType.GAUGE,
{
row: 0,
column: 0,
rowSpan: 3,
columnSpan: 4,
},
id: 'widget-1',
pageLayoutTabId: 'tab-overview',
updatedAt: '2024-01-01T00:00:00Z',
},
),
},
parameters: {
docs: {
@@ -179,23 +154,18 @@ export const MediumWidget: Story = {
export const LargeWidget: Story = {
args: {
widget: {
title: 'Large Widget (6x4 grid)',
type: WidgetType.GRAPH,
configuration: {
graphType: GraphType.PIE,
},
createdAt: '2024-01-01T00:00:00Z',
gridPosition: {
widget: createDefaultGraphWidget(
'widget-1',
'tab-overview',
'Large Widget (6x4 grid)',
GraphType.PIE,
{
row: 0,
column: 0,
rowSpan: 4,
columnSpan: 6,
},
id: 'widget-1',
pageLayoutTabId: 'tab-overview',
updatedAt: '2024-01-01T00:00:00Z',
},
),
},
parameters: {
docs: {
@@ -213,23 +183,18 @@ export const LargeWidget: Story = {
export const WideWidget: Story = {
args: {
widget: {
title: 'Wide Widget (8x2 grid)',
type: WidgetType.GRAPH,
configuration: {
graphType: GraphType.NUMBER,
},
createdAt: '2024-01-01T00:00:00Z',
gridPosition: {
widget: createDefaultGraphWidget(
'widget-1',
'tab-overview',
'Wide Widget (8x2 grid)',
GraphType.NUMBER,
{
row: 0,
column: 0,
rowSpan: 2,
columnSpan: 8,
},
id: 'widget-1',
pageLayoutTabId: 'tab-overview',
updatedAt: '2024-01-01T00:00:00Z',
},
),
},
parameters: {
docs: {
@@ -247,23 +212,18 @@ export const WideWidget: Story = {
export const TallWidget: Story = {
args: {
widget: {
title: 'Tall Widget (3x6 grid)',
type: WidgetType.GRAPH,
configuration: {
graphType: GraphType.GAUGE,
},
createdAt: '2024-01-01T00:00:00Z',
gridPosition: {
widget: createDefaultGraphWidget(
'widget-1',
'tab-overview',
'Tall Widget (3x6 grid)',
GraphType.GAUGE,
{
row: 0,
column: 0,
rowSpan: 6,
columnSpan: 3,
},
id: 'widget-1',
pageLayoutTabId: 'tab-overview',
updatedAt: '2024-01-01T00:00:00Z',
},
),
},
parameters: {
docs: {
@@ -1,9 +1,8 @@
import { GraphType } from '@/page-layout/mocks/mockWidgets';
import { getDefaultWidgetData } from '@/page-layout/utils/getDefaultWidgetData';
import { ChartSkeletonLoader } from '@/page-layout/widgets/graph/components/ChartSkeletonLoader';
import { GraphWidgetNumberChart } from '@/page-layout/widgets/graph/graphWidgetNumberChart/components/GraphWidgetNumberChart';
import { type GraphWidget } from '@/page-layout/widgets/graph/types/GraphWidget';
import { lazy, Suspense } from 'react';
import { GraphType, type PageLayoutWidget } from '~/generated-metadata/graphql';
const GraphWidgetBarChart = lazy(() =>
import(
@@ -38,14 +37,22 @@ const GraphWidgetGaugeChart = lazy(() =>
);
type GraphWidgetRendererProps = {
widget: GraphWidget;
widget: PageLayoutWidget;
};
export const GraphWidgetRenderer = ({ widget }: GraphWidgetRendererProps) => {
const graphType = widget.configuration?.graphType;
if (!widget.configuration || !('graphType' in widget.configuration)) {
throw new Error(
`Invalid configuration for widget ${widget.id}: missing graphType`,
);
}
const graphType = widget.configuration.graphType;
if (!Object.values(GraphType).includes(graphType)) {
return null;
throw new Error(
`Unsupported graph type ${graphType} for widget ${widget.id}`,
);
}
const data: any = getDefaultWidgetData(graphType);
@@ -54,7 +61,7 @@ export const GraphWidgetRenderer = ({ widget }: GraphWidgetRendererProps) => {
return null;
}
switch (graphType as GraphType) {
switch (graphType) {
case GraphType.NUMBER:
return (
<GraphWidgetNumberChart
@@ -1,9 +0,0 @@
import { type GraphType } from '@/page-layout/mocks/mockWidgets';
import { type PageLayoutWidget, type WidgetType } from '~/generated/graphql';
export type GraphWidget = PageLayoutWidget & {
type: WidgetType.GRAPH;
configuration: {
graphType: GraphType;
};
};
@@ -0,0 +1,29 @@
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { isDefined } from 'twenty-shared/utils';
import { type PageLayoutWidget } from '~/generated-metadata/graphql';
type UseWidgetObjectPermissionsReturn = {
haveAccessToWidgetsObject: boolean;
};
export const useWidgetObjectPermissions = (
widget: PageLayoutWidget,
): UseWidgetObjectPermissionsReturn => {
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
if (!isDefined(widget.objectMetadataId)) {
return {
haveAccessToWidgetsObject: true,
};
}
const objectPermissions = getObjectPermissionsForObject(
objectPermissionsByObjectMetadataId,
widget.objectMetadataId,
);
return {
haveAccessToWidgetsObject: objectPermissions.canReadObjectRecords,
};
};
@@ -1,4 +1,5 @@
import { ChartSkeletonLoader } from '@/page-layout/widgets/graph/components/ChartSkeletonLoader';
import { type PageLayoutWidget } from '~/generated-metadata/graphql';
import styled from '@emotion/styled';
import { useState } from 'react';
@@ -57,14 +58,21 @@ const StyledErrorUrl = styled.div`
`;
export type IframeWidgetProps = {
url: string;
title?: string;
widget: PageLayoutWidget;
};
export const IframeWidget = ({
url,
title = 'Embedded Content',
}: IframeWidgetProps) => {
export const IframeWidget = ({ widget }: IframeWidgetProps) => {
const configuration = widget.configuration;
if (!configuration || !('url' in configuration)) {
throw new Error(
`Invalid configuration for widget ${widget.id}: missing url`,
);
}
const url = configuration.url;
const title = widget.title;
const [isLoading, setIsLoading] = useState(true);
const [hasError, setHasError] = useState(false);
@@ -1,8 +0,0 @@
import { type PageLayoutWidget, type WidgetType } from '~/generated/graphql';
export type IframeWidget = PageLayoutWidget & {
type: WidgetType.IFRAME;
configuration: {
url: string;
};
};
@@ -1,4 +0,0 @@
import { type GraphWidget } from '@/page-layout/widgets/graph/types/GraphWidget';
import { type IframeWidget } from '@/page-layout/widgets/iframe/types/IframeWidget';
export type Widget = GraphWidget | IframeWidget;