Create save dashboard record action (#14665)

Closes https://github.com/twentyhq/core-team-issues/issues/1502

- Created the save action
- Removed the temporary data attribute from the page layout widget since
we will connect widgets to real data soon



https://github.com/user-attachments/assets/2b46b511-f2c2-4e11-873c-a76fd31704e5
This commit is contained in:
Raphaël Bosi
2025-09-24 14:03:27 +02:00
committed by GitHub
parent a067d961f7
commit 66a6190e3c
43 changed files with 389 additions and 316 deletions
+2
View File
@@ -19,6 +19,8 @@ module.exports = {
'./src/modules/prefetch/graphql/**/*.{ts,tsx}',
'./src/modules/subscription/graphql/**/*.{ts,tsx}',
'./src/modules/page-layout/graphql/**/*.{ts,tsx}',
'!./src/**/*.test.{ts,tsx}',
'!./src/**/*.stories.{ts,tsx}',
'!./src/**/__mocks__/*.ts',
@@ -3684,7 +3684,7 @@ export type UpdatePageLayoutWidgetWithIdInput = {
export type UpdatePageLayoutWithTabsInput = {
name: Scalars['String'];
objectMetadataId: Scalars['UUID'];
objectMetadataId?: InputMaybe<Scalars['UUID']>;
tabs: Array<UpdatePageLayoutTabWithWidgetsInput>;
type: PageLayoutType;
};
+73 -1
View File
@@ -3530,7 +3530,7 @@ export type UpdatePageLayoutWidgetWithIdInput = {
export type UpdatePageLayoutWithTabsInput = {
name: Scalars['String'];
objectMetadataId: Scalars['UUID'];
objectMetadataId?: InputMaybe<Scalars['UUID']>;
tabs: Array<UpdatePageLayoutTabWithWidgetsInput>;
type: PageLayoutType;
};
@@ -4069,6 +4069,14 @@ 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 UpdatePageLayoutWithTabsAndWidgetsMutationVariables = Exact<{
id: Scalars['String'];
input: UpdatePageLayoutWithTabsInput;
}>;
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, pageLayoutTabId: any, objectMetadataId?: any | null, configuration?: any | null, createdAt: string, updatedAt: string, gridPosition: { __typename?: 'GridPosition', row: number, column: number, rowSpan: number, columnSpan: number } }> | null }> | null } };
export type OnDbEventSubscriptionVariables = Exact<{
input: OnDbEventInput;
}>;
@@ -4496,6 +4504,70 @@ export function useSearchLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<Sea
export type SearchQueryHookResult = ReturnType<typeof useSearchQuery>;
export type SearchLazyQueryHookResult = ReturnType<typeof useSearchLazyQuery>;
export type SearchQueryResult = Apollo.QueryResult<SearchQuery, SearchQueryVariables>;
export const UpdatePageLayoutWithTabsAndWidgetsDocument = gql`
mutation UpdatePageLayoutWithTabsAndWidgets($id: String!, $input: UpdatePageLayoutWithTabsInput!) {
updatePageLayoutWithTabsAndWidgets(id: $id, input: $input) {
id
name
type
objectMetadataId
createdAt
updatedAt
deletedAt
tabs {
id
title
position
pageLayoutId
widgets {
id
title
type
pageLayoutTabId
objectMetadataId
gridPosition {
row
column
rowSpan
columnSpan
}
configuration
createdAt
updatedAt
}
createdAt
updatedAt
}
}
}
`;
export type UpdatePageLayoutWithTabsAndWidgetsMutationFn = Apollo.MutationFunction<UpdatePageLayoutWithTabsAndWidgetsMutation, UpdatePageLayoutWithTabsAndWidgetsMutationVariables>;
/**
* __useUpdatePageLayoutWithTabsAndWidgetsMutation__
*
* To run a mutation, you first call `useUpdatePageLayoutWithTabsAndWidgetsMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useUpdatePageLayoutWithTabsAndWidgetsMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [updatePageLayoutWithTabsAndWidgetsMutation, { data, loading, error }] = useUpdatePageLayoutWithTabsAndWidgetsMutation({
* variables: {
* id: // value for 'id'
* input: // value for 'input'
* },
* });
*/
export function useUpdatePageLayoutWithTabsAndWidgetsMutation(baseOptions?: Apollo.MutationHookOptions<UpdatePageLayoutWithTabsAndWidgetsMutation, UpdatePageLayoutWithTabsAndWidgetsMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<UpdatePageLayoutWithTabsAndWidgetsMutation, UpdatePageLayoutWithTabsAndWidgetsMutationVariables>(UpdatePageLayoutWithTabsAndWidgetsDocument, options);
}
export type UpdatePageLayoutWithTabsAndWidgetsMutationHookResult = ReturnType<typeof useUpdatePageLayoutWithTabsAndWidgetsMutation>;
export type UpdatePageLayoutWithTabsAndWidgetsMutationResult = Apollo.MutationResult<UpdatePageLayoutWithTabsAndWidgetsMutation>;
export type UpdatePageLayoutWithTabsAndWidgetsMutationOptions = Apollo.BaseMutationOptions<UpdatePageLayoutWithTabsAndWidgetsMutation, UpdatePageLayoutWithTabsAndWidgetsMutationVariables>;
export const OnDbEventDocument = gql`
subscription OnDbEvent($input: OnDbEventInput!) {
onDbEvent(input: $input) {
@@ -1,5 +1,6 @@
import { CancelDashboardSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/dashboard-actions/components/CancelDashboardSingleRecordAction';
import { EditDashboardSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/dashboard-actions/components/EditDashboardSingleRecordAction';
import { SaveDashboardSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/dashboard-actions/components/SaveDashboardSingleRecordAction';
import { DashboardSingleRecordActionKeys } from '@/action-menu/actions/record-actions/single-record/dashboard-actions/types/DashboardSingleRecordActionKeys';
import { SingleRecordActionKeys } from '@/action-menu/actions/record-actions/single-record/types/SingleRecordActionsKey';
import { inheritActionsFromDefaultConfig } from '@/action-menu/actions/record-actions/utils/inheritActionsFromDefaultConfig';
@@ -8,7 +9,7 @@ import { ActionType } from '@/action-menu/actions/types/ActionType';
import { ActionViewType } from '@/action-menu/actions/types/ActionViewType';
import { msg } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { IconPencil, IconX } from 'twenty-ui/display';
import { IconDeviceFloppy, IconPencil, IconX } from 'twenty-ui/display';
export const DASHBOARD_ACTIONS_CONFIG = inheritActionsFromDefaultConfig({
config: {
@@ -17,7 +18,7 @@ export const DASHBOARD_ACTIONS_CONFIG = inheritActionsFromDefaultConfig({
label: msg`Edit Dashboard`,
shortLabel: msg`Edit`,
isPinned: true,
position: 1,
position: 0,
Icon: IconPencil,
type: ActionType.Standard,
scope: ActionScope.RecordSelection,
@@ -29,6 +30,23 @@ export const DASHBOARD_ACTIONS_CONFIG = inheritActionsFromDefaultConfig({
availableOn: [ActionViewType.SHOW_PAGE],
component: <EditDashboardSingleRecordAction />,
},
[DashboardSingleRecordActionKeys.SAVE_DASHBOARD]: {
key: DashboardSingleRecordActionKeys.SAVE_DASHBOARD,
label: msg`Save Dashboard`,
shortLabel: msg`Save`,
isPinned: true,
position: 1,
Icon: IconDeviceFloppy,
type: ActionType.Standard,
scope: ActionScope.RecordSelection,
shouldBeRegistered: ({ selectedRecord }) =>
isDefined(selectedRecord) &&
!selectedRecord?.isRemote &&
!isDefined(selectedRecord?.deletedAt) &&
isDefined(selectedRecord?.pageLayoutId),
availableOn: [ActionViewType.SHOW_PAGE],
component: <SaveDashboardSingleRecordAction />,
},
[DashboardSingleRecordActionKeys.CANCEL_DASHBOARD_EDITION]: {
key: DashboardSingleRecordActionKeys.CANCEL_DASHBOARD_EDITION,
label: msg`Cancel Edition`,
@@ -0,0 +1,26 @@
import { Action } from '@/action-menu/actions/components/Action';
import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow';
import { useSetIsDashboardInEditMode } from '@/dashboards/hooks/useSetDashboardInEditMode';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { useSavePageLayout } from '@/page-layout/hooks/useSavePageLayout';
import { useRecoilValue } from 'recoil';
export const SaveDashboardSingleRecordAction = () => {
const recordId = useSelectedRecordIdOrThrow();
const selectedRecord = useRecoilValue(recordStoreFamilyState(recordId));
const pageLayoutId = selectedRecord?.pageLayoutId;
const { savePageLayout } = useSavePageLayout(pageLayoutId);
const { setIsDashboardInEditMode } =
useSetIsDashboardInEditMode(pageLayoutId);
const handleClick = () => {
savePageLayout();
setIsDashboardInEditMode(false);
};
return <Action onClick={handleClick} />;
};
@@ -1,4 +1,5 @@
export enum DashboardSingleRecordActionKeys {
EDIT_DASHBOARD = 'edit-dashboard-single-record',
SAVE_DASHBOARD = 'save-dashboard-single-record',
CANCEL_DASHBOARD_EDITION = 'cancel-dashboard-edition-single-record',
}
@@ -1,10 +1,8 @@
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
import { createState } from 'twenty-ui/utilities';
export const forceRegisteredActionsByKeyComponentState = createComponentState<
export const forceRegisteredActionsByKeyState = createState<
Record<string, boolean | undefined>
>({
key: 'forceRegisteredActionsByKeyComponentState',
defaultValue: {},
componentInstanceContext: ActionMenuComponentInstanceContext,
});
@@ -1,4 +1,4 @@
import { forceRegisteredActionsByKeyComponentState } from '@/action-menu/actions/states/forceRegisteredActionsMapComponentState';
import { forceRegisteredActionsByKeyState } from '@/action-menu/actions/states/forceRegisteredActionsMapComponentState';
import { type ShouldBeRegisteredFunctionParams } from '@/action-menu/actions/types/ShouldBeRegisteredFunctionParams';
import { getActionViewType } from '@/action-menu/actions/utils/getActionViewType';
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
@@ -101,8 +101,8 @@ export const useShouldActionBeRegisteredParams = ({
[],
);
const forceRegisteredActionsByKey = useRecoilComponentValue(
forceRegisteredActionsByKeyComponentState,
const forceRegisteredActionsByKey = useRecoilValue(
forceRegisteredActionsByKeyState,
);
return {
@@ -1,6 +1,6 @@
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { useCreatePageLayoutWidget } from '@/page-layout/hooks/useCreatePageLayoutWidget';
import { useCreatePageLayoutGraphWidget } from '@/page-layout/hooks/useCreatePageLayoutGraphWidget';
import { GraphType, WidgetType } from '@/page-layout/mocks/mockWidgets';
import styled from '@emotion/styled';
@@ -61,7 +61,8 @@ export const CommandMenuPageLayoutGraphTypeSelect = () => {
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
const { createPageLayoutWidget } = useCreatePageLayoutWidget(pageLayoutId);
const { createPageLayoutWidget } =
useCreatePageLayoutGraphWidget(pageLayoutId);
const handleSelectGraphType = (graphType: GraphType) => {
createPageLayoutWidget(WidgetType.GRAPH, graphType);
@@ -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 PageLayoutWithData } from '@/page-layout/types/pageLayoutTypes';
import { type PageLayout } from '@/page-layout/types/pageLayoutTypes';
import { isPageLayoutEmpty } from '@/page-layout/utils/isPageLayoutEmpty';
type DashboardContentRendererProps = {
@@ -16,7 +16,7 @@ export const DashboardContentRenderer = ({
const { setIsDashboardInEditMode } =
useSetIsDashboardInEditMode(pageLayoutId);
const onInitialized = (pageLayout: PageLayoutWithData) => {
const onInitialized = (pageLayout: PageLayout) => {
if (isPageLayoutEmpty(pageLayout)) {
setIsDashboardInEditMode(true);
} else {
@@ -1,5 +1,5 @@
import { DashboardSingleRecordActionKeys } from '@/action-menu/actions/record-actions/single-record/dashboard-actions/types/DashboardSingleRecordActionKeys';
import { forceRegisteredActionsByKeyComponentState } from '@/action-menu/actions/states/forceRegisteredActionsMapComponentState';
import { forceRegisteredActionsByKeyState } from '@/action-menu/actions/states/forceRegisteredActionsMapComponentState';
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
@@ -17,10 +17,6 @@ export const useSetIsDashboardInEditMode = (pageLayoutIdFromProps: string) => {
pageLayoutId,
);
const forceRegisteredActionsByKeyState = useRecoilComponentCallbackState(
forceRegisteredActionsByKeyComponentState,
);
const setIsDashboardInEditMode = useRecoilCallback(
({ set }) =>
(value: boolean) => {
@@ -28,10 +24,11 @@ export const useSetIsDashboardInEditMode = (pageLayoutIdFromProps: string) => {
set(forceRegisteredActionsByKeyState, (prev) => ({
...prev,
[DashboardSingleRecordActionKeys.EDIT_DASHBOARD]: !value,
[DashboardSingleRecordActionKeys.SAVE_DASHBOARD]: value,
[DashboardSingleRecordActionKeys.CANCEL_DASHBOARD_EDITION]: value,
}));
},
[forceRegisteredActionsByKeyState, isPageLayoutInEditModeState],
[isPageLayoutInEditModeState],
);
return { setIsDashboardInEditMode };
@@ -2,7 +2,7 @@ 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 PageLayoutWithData } from '@/page-layout/types/pageLayoutTypes';
import { type PageLayout } from '@/page-layout/types/pageLayoutTypes';
import { convertPageLayoutToTabLayouts } from '@/page-layout/utils/convertPageLayoutToTabLayouts';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
@@ -14,7 +14,7 @@ import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
type PageLayoutInitializationQueryEffectProps = {
pageLayoutId: string;
onInitialized: (pageLayout: PageLayoutWithData) => void;
onInitialized: (pageLayout: PageLayout) => void;
};
export const PageLayoutInitializationQueryEffect = ({
@@ -29,7 +29,7 @@ export const PageLayoutInitializationQueryEffect = ({
},
});
const pageLayout: PageLayoutWithData | undefined = data?.getPageLayout;
const pageLayout: PageLayout | undefined = data?.getPageLayout;
const pageLayoutPersistedComponentCallbackState =
useRecoilComponentCallbackState(pageLayoutPersistedComponentState);
@@ -43,7 +43,7 @@ export const PageLayoutInitializationQueryEffect = ({
const initializePageLayout = useRecoilCallback(
({ set, snapshot }) =>
(layout: PageLayoutWithData) => {
(layout: PageLayout) => {
const currentPersisted = getSnapshotValue(
snapshot,
pageLayoutPersistedComponentCallbackState,
@@ -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 PageLayoutWithData } from '@/page-layout/types/pageLayoutTypes';
import { type PageLayout } from '@/page-layout/types/pageLayoutTypes';
import { getTabListInstanceIdFromPageLayoutId } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutId';
import { TabListComponentInstanceContext } from '@/ui/layout/tab-list/states/contexts/TabListComponentInstanceContext';
import 'react-grid-layout/css/styles.css';
@@ -10,7 +10,7 @@ import 'react-resizable/css/styles.css';
type PageLayoutRendererProps = {
pageLayoutId: string;
onInitialized: (pageLayout: PageLayoutWithData) => void;
onInitialized: (pageLayout: PageLayout) => void;
};
export const PageLayoutRenderer = ({
@@ -7,8 +7,7 @@ import { FIND_ONE_PAGE_LAYOUT } from '@/dashboards/graphql/queries/findOnePageLa
import { PageLayoutRenderer } from '@/page-layout/components/PageLayoutRenderer';
import { GraphType, WidgetType } from '@/page-layout/mocks/mockWidgets';
import { RecoilRoot } from 'recoil';
import { PageLayoutType } from '~/generated/graphql';
import { type PageLayoutWidgetWithData } from '../../types/pageLayoutTypes';
import { PageLayoutType, type PageLayoutWidget } from '~/generated/graphql';
const validatePageLayoutContent = async (canvasElement: HTMLElement) => {
const canvas = within(canvasElement);
@@ -56,14 +55,10 @@ const mixedGraphsPageLayoutMocks = {
configuration: {
graphType: GraphType.NUMBER,
},
data: {
value: '$125,000',
trendPercentage: 8.3,
},
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
deletedAt: null,
} as PageLayoutWidgetWithData,
} as PageLayoutWidget,
{
__typename: 'PageLayoutWidget',
id: 'gauge-widget',
@@ -81,16 +76,10 @@ const mixedGraphsPageLayoutMocks = {
configuration: {
graphType: GraphType.GAUGE,
},
data: {
value: 0.75,
min: 0,
max: 1,
label: 'Goal Progress',
},
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
deletedAt: null,
} as PageLayoutWidgetWithData,
} as PageLayoutWidget,
{
__typename: 'PageLayoutWidget',
id: 'pie-widget',
@@ -108,17 +97,10 @@ const mixedGraphsPageLayoutMocks = {
configuration: {
graphType: GraphType.PIE,
},
data: {
items: [
{ id: 'product', value: 60, label: 'Product Sales' },
{ id: 'services', value: 30, label: 'Services' },
{ id: 'support', value: 10, label: 'Support' },
],
},
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
deletedAt: null,
} as PageLayoutWidgetWithData,
} as PageLayoutWidget,
{
__typename: 'PageLayoutWidget',
id: 'bar-widget',
@@ -136,25 +118,10 @@ const mixedGraphsPageLayoutMocks = {
configuration: {
graphType: GraphType.BAR,
},
data: {
items: [
{ quarter: 'Q1', revenue: 100000, expenses: 80000 },
{ quarter: 'Q2', revenue: 125000, expenses: 90000 },
{ quarter: 'Q3', revenue: 150000, expenses: 95000 },
{ quarter: 'Q4', revenue: 180000, expenses: 100000 },
],
indexBy: 'quarter',
keys: ['revenue', 'expenses'],
layout: 'vertical',
seriesLabels: {
revenue: 'Revenue',
expenses: 'Expenses',
},
},
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
deletedAt: null,
} as PageLayoutWidgetWithData,
} as PageLayoutWidget,
],
},
],
@@ -0,0 +1,42 @@
import { gql } from '@apollo/client';
export const UPDATE_PAGE_LAYOUT_WITH_TABS_AND_WIDGETS = gql`
mutation UpdatePageLayoutWithTabsAndWidgets(
$id: String!
$input: UpdatePageLayoutWithTabsInput!
) {
updatePageLayoutWithTabsAndWidgets(id: $id, input: $input) {
id
name
type
objectMetadataId
createdAt
updatedAt
deletedAt
tabs {
id
title
position
pageLayoutId
widgets {
id
title
type
pageLayoutTabId
objectMetadataId
gridPosition {
row
column
rowSpan
columnSpan
}
configuration
createdAt
updatedAt
}
createdAt
updatedAt
}
}
}
`;
@@ -1,3 +1,4 @@
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';
@@ -7,7 +8,6 @@ import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state
import { act, renderHook } from '@testing-library/react';
import { useSetRecoilState } from 'recoil';
import { PageLayoutType } from '~/generated/graphql';
import { useCreatePageLayoutWidget } from '../useCreatePageLayoutWidget';
import {
PAGE_LAYOUT_TEST_INSTANCE_ID,
PageLayoutTestWrapper,
@@ -17,7 +17,7 @@ jest.mock('uuid', () => ({
v4: jest.fn(() => 'mock-uuid'),
}));
describe('useCreatePageLayoutWidget', () => {
describe('useCreatePageLayoutGraphWidget', () => {
beforeEach(() => {
jest.clearAllMocks();
});
@@ -43,7 +43,7 @@ describe('useCreatePageLayoutWidget', () => {
pageLayoutCurrentLayoutsComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
);
const createWidget = useCreatePageLayoutWidget(
const createWidget = useCreatePageLayoutGraphWidget(
PAGE_LAYOUT_TEST_INSTANCE_ID,
);
return {
@@ -119,7 +119,7 @@ describe('useCreatePageLayoutWidget', () => {
pageLayoutCurrentLayoutsComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
);
const createWidget = useCreatePageLayoutWidget(
const createWidget = useCreatePageLayoutGraphWidget(
PAGE_LAYOUT_TEST_INSTANCE_ID,
);
return {
@@ -181,7 +181,7 @@ describe('useCreatePageLayoutWidget', () => {
expect(widget.type).toBe(WidgetType.GRAPH);
expect(widget.pageLayoutTabId).toBe('tab-1');
expect(widget.configuration.graphType).toBe(graphType);
expect(widget.id).toBe('widget-mock-uuid');
expect(widget.id).toBe('mock-uuid');
});
expect(result.current.pageLayoutCurrentLayouts['tab-1']).toBeDefined();
@@ -207,7 +207,7 @@ describe('useCreatePageLayoutWidget', () => {
pageLayoutCurrentLayoutsComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
);
const createWidget = useCreatePageLayoutWidget(
const createWidget = useCreatePageLayoutGraphWidget(
PAGE_LAYOUT_TEST_INSTANCE_ID,
);
return { allWidgets, pageLayoutCurrentLayouts, createWidget };
@@ -96,7 +96,6 @@ describe('usePageLayoutDraftState', () => {
type: WidgetType.GRAPH,
gridPosition: { row: 2, column: 2, rowSpan: 2, columnSpan: 2 },
configuration: { graphType: GraphType.BAR },
data: {},
objectMetadataId: null,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
@@ -3,10 +3,8 @@ import { PageLayoutComponentInstanceContext } from '@/page-layout/states/context
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLayoutDraggedAreaComponentState';
import { type PageLayoutWidgetWithData } from '@/page-layout/types/pageLayoutTypes';
import { addWidgetToTab } from '@/page-layout/utils/addWidgetToTab';
import {
getDefaultWidgetData,
getWidgetSize,
getWidgetTitle,
} from '@/page-layout/utils/getDefaultWidgetData';
@@ -18,9 +16,11 @@ import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/com
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { useRecoilCallback } from 'recoil';
import { v4 as uuidv4 } from 'uuid';
import { type WidgetType } from '~/generated/graphql';
import { type PageLayoutWidget, type WidgetType } from '~/generated/graphql';
export const useCreatePageLayoutWidget = (pageLayoutIdFromProps?: string) => {
export const useCreatePageLayoutGraphWidget = (
pageLayoutIdFromProps?: string,
) => {
const pageLayoutId = useAvailableComponentInstanceIdOrThrow(
PageLayoutComponentInstanceContext,
pageLayoutIdFromProps,
@@ -57,8 +57,6 @@ export const useCreatePageLayoutWidget = (pageLayoutIdFromProps?: string) => {
return;
}
const widgetData = getDefaultWidgetData(graphType);
const pageLayoutDraft = snapshot
.getLoadable(pageLayoutDraftState)
.getValue();
@@ -77,7 +75,7 @@ export const useCreatePageLayoutWidget = (pageLayoutIdFromProps?: string) => {
w.type === widgetType && w.configuration.graphType === graphType,
).length;
const title = getWidgetTitle(graphType, existingWidgetCount);
const widgetId = `widget-${uuidv4()}`;
const widgetId = uuidv4();
const defaultSize = getWidgetSize(graphType);
const position = getDefaultWidgetPosition(
@@ -85,7 +83,7 @@ export const useCreatePageLayoutWidget = (pageLayoutIdFromProps?: string) => {
defaultSize,
);
const newWidget: PageLayoutWidgetWithData = {
const newWidget: PageLayoutWidget = {
id: widgetId,
pageLayoutTabId: activeTabId,
title,
@@ -99,7 +97,6 @@ export const useCreatePageLayoutWidget = (pageLayoutIdFromProps?: string) => {
configuration: {
graphType,
},
data: widgetData as Record<string, unknown>,
objectMetadataId: null,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
@@ -2,7 +2,6 @@ import { PageLayoutComponentInstanceContext } from '@/page-layout/states/context
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLayoutDraggedAreaComponentState';
import { type PageLayoutWidgetWithData } from '@/page-layout/types/pageLayoutTypes';
import { addWidgetToTab } from '@/page-layout/utils/addWidgetToTab';
import { getDefaultWidgetPosition } from '@/page-layout/utils/getDefaultWidgetPosition';
import { getTabListInstanceIdFromPageLayoutId } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutId';
@@ -13,7 +12,7 @@ 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 { WidgetType } from '~/generated/graphql';
import { type PageLayoutWidget, WidgetType } from '~/generated/graphql';
export const useCreatePageLayoutIframeWidget = (
pageLayoutIdFromProps?: string,
@@ -58,14 +57,14 @@ export const useCreatePageLayoutIframeWidget = (
return;
}
const widgetId = `widget-${uuidv4()}`;
const widgetId = uuidv4();
const defaultSize = { w: 6, h: 6 };
const position = getDefaultWidgetPosition(
pageLayoutDraggedArea,
defaultSize,
);
const newWidget: PageLayoutWidgetWithData = {
const newWidget: PageLayoutWidget = {
id: widgetId,
pageLayoutTabId: activeTabId,
title,
@@ -79,7 +78,6 @@ export const useCreatePageLayoutIframeWidget = (
configuration: {
url,
},
data: {},
objectMetadataId: null,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
@@ -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 PageLayoutTabWithData } from '../types/pageLayoutTypes';
import { type PageLayoutTab } from '../types/pageLayoutTypes';
import { getEmptyTabLayout } from '../utils/getEmptyTabLayout';
export const useCreatePageLayoutTab = (pageLayoutIdFromProps?: string) => {
@@ -33,7 +33,7 @@ export const useCreatePageLayoutTab = (pageLayoutIdFromProps?: string) => {
const newTabId = `tab-${uuidv4()}`;
const tabsLength = pageLayoutDraft.tabs.length;
const newTab: PageLayoutTabWithData = {
const newTab: PageLayoutTab = {
id: newTabId,
title: title || `Tab ${tabsLength + 1}`,
position: tabsLength,
@@ -81,7 +81,6 @@ export const usePageLayoutHandleLayoutChange = (
objectMetadataId: null,
gridPosition: widget.gridPosition,
configuration: widget.configuration || undefined,
data: widget.data,
createdAt:
tab.widgets.find((w) => w.id === widget.id)?.createdAt ||
new Date().toISOString(),
@@ -0,0 +1,98 @@
import { usePageLayoutDraftState } from '@/page-layout/hooks/usePageLayoutDraftState';
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 { convertPageLayoutDraftToUpdateInput } from '@/page-layout/utils/convertPageLayoutDraftToUpdateInput';
import { convertPageLayoutToTabLayouts } from '@/page-layout/utils/convertPageLayoutToTabLayouts';
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 { isDefined } from 'twenty-shared/utils';
import { useUpdatePageLayoutWithTabsAndWidgetsMutation } from '~/generated/graphql';
export const useSavePageLayout = (pageLayoutIdFromProps: string) => {
const pageLayoutId = useAvailableComponentInstanceIdOrThrow(
PageLayoutComponentInstanceContext,
pageLayoutIdFromProps,
);
const pageLayoutPersistedCallbackState = useRecoilComponentCallbackState(
pageLayoutPersistedComponentState,
pageLayoutId,
);
const pageLayoutCurrentLayoutsCallbackState = useRecoilComponentCallbackState(
pageLayoutCurrentLayoutsComponentState,
pageLayoutId,
);
const { pageLayoutDraft } = usePageLayoutDraftState(pageLayoutId);
const [updatePageLayoutWithTabsAndWidgets] =
useUpdatePageLayoutWithTabsAndWidgetsMutation();
const savePageLayout = useRecoilCallback(
({ set }) =>
async () => {
const updateInput =
convertPageLayoutDraftToUpdateInput(pageLayoutDraft);
const { data } = await updatePageLayoutWithTabsAndWidgets({
variables: {
id: pageLayoutId,
input: updateInput,
},
});
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,
};
set(pageLayoutPersistedCallbackState, pageLayoutToPersist);
set(
pageLayoutCurrentLayoutsCallbackState,
convertPageLayoutToTabLayouts(pageLayoutToPersist),
);
}
},
[
pageLayoutCurrentLayoutsCallbackState,
pageLayoutDraft,
pageLayoutId,
pageLayoutPersistedCallbackState,
updatePageLayoutWithTabsAndWidgets,
],
);
return { savePageLayout };
};
@@ -1,9 +1,9 @@
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { type PageLayoutWidgetWithData } from '@/page-layout/types/pageLayoutTypes';
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(
@@ -18,7 +18,7 @@ export const useUpdatePageLayoutWidget = (pageLayoutIdFromProps?: string) => {
const updatePageLayoutWidget = useRecoilCallback(
({ set }) =>
(widgetId: string, updates: Partial<PageLayoutWidgetWithData>) => {
(widgetId: string, updates: Partial<PageLayoutWidget>) => {
set(pageLayoutDraftState, (prev) => ({
...prev,
tabs: prev.tabs.map((tab) => ({
@@ -1,5 +1,5 @@
import { type Layouts } from 'react-grid-layout';
import { type PageLayoutWidgetWithData } from '../types/pageLayoutTypes';
import { type PageLayoutWidget } from '~/generated/graphql';
export enum WidgetType {
VIEW = 'VIEW',
@@ -16,7 +16,7 @@ export enum GraphType {
LINE = 'LINE',
}
export const mockPageLayoutWidgets: PageLayoutWidgetWithData[] = [
export const mockPageLayoutWidgets: PageLayoutWidget[] = [
{
id: 'widget-1',
pageLayoutTabId: 'tab-overview',
@@ -32,10 +32,6 @@ export const mockPageLayoutWidgets: PageLayoutWidgetWithData[] = [
configuration: {
graphType: GraphType.NUMBER,
},
data: {
value: '1,234',
trendPercentage: 12.5,
},
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
deletedAt: null,
@@ -55,12 +51,6 @@ export const mockPageLayoutWidgets: PageLayoutWidgetWithData[] = [
configuration: {
graphType: GraphType.GAUGE,
},
data: {
value: 0.5,
min: 0,
max: 1,
label: 'Conversion rate',
},
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
deletedAt: null,
@@ -80,40 +70,6 @@ export const mockPageLayoutWidgets: PageLayoutWidgetWithData[] = [
configuration: {
graphType: GraphType.PIE,
},
data: {
items: [
{
id: 'qualified',
value: 35,
label: 'Qualified',
to: '/leads/qualified',
},
{
id: 'contacted',
value: 25,
label: 'Contacted',
to: '/leads/contacted',
},
{
id: 'unqualified',
value: 20,
label: 'Unqualified',
to: '/leads/unqualified',
},
{
id: 'proposal',
value: 15,
label: 'Proposal',
to: '/leads/proposal',
},
{
id: 'negotiation',
value: 5,
label: 'Negotiation',
to: '/leads/negotiation',
},
],
},
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
deletedAt: null,
@@ -133,52 +89,6 @@ export const mockPageLayoutWidgets: PageLayoutWidgetWithData[] = [
configuration: {
graphType: GraphType.BAR,
},
data: {
items: [
{
month: 'Jan',
sales: 120,
leads: 45,
conversions: 12,
to: '/metrics/january',
},
{
month: 'Feb',
sales: 150,
leads: 52,
conversions: 15,
to: '/metrics/february',
},
{
month: 'Mar',
sales: 180,
leads: 48,
conversions: 18,
to: '/metrics/march',
},
{
month: 'Apr',
sales: 140,
leads: 60,
conversions: 14,
to: '/metrics/april',
},
{
month: 'May',
sales: 200,
leads: 55,
conversions: 20,
to: '/metrics/may',
},
],
indexBy: 'month',
keys: ['sales', 'leads', 'conversions'],
showLegend: true,
showGrid: true,
displayType: 'number',
xAxisLabel: 'Month',
yAxisLabel: 'Count',
},
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
deletedAt: null,
@@ -1,10 +1,10 @@
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
import { type PageLayoutWithData } from '../types/pageLayoutTypes';
import { type PageLayout } from '../types/pageLayoutTypes';
import { PageLayoutComponentInstanceContext } from './contexts/PageLayoutComponentInstanceContext';
export const pageLayoutPersistedComponentState = createComponentState<
PageLayoutWithData | undefined
PageLayout | undefined
>({
key: 'pageLayoutPersistedComponentState',
defaultValue: undefined,
@@ -1,10 +1,10 @@
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
import { type PageLayoutWithData } from '../types/pageLayoutTypes';
import { type PageLayout } from '../types/pageLayoutTypes';
import { PageLayoutComponentInstanceContext } from './contexts/PageLayoutComponentInstanceContext';
export const savedPageLayoutsComponentState = createComponentState<
PageLayoutWithData[]
PageLayout[]
>({
key: 'savedPageLayoutsComponentState',
defaultValue: [],
@@ -1,6 +1,6 @@
import { type PageLayoutWithData } from './pageLayoutTypes';
import { type PageLayout } from './pageLayoutTypes';
export type DraftPageLayout = Omit<
PageLayoutWithData,
PageLayout,
'createdAt' | 'updatedAt' | 'deletedAt'
>;
@@ -1,18 +1,13 @@
import {
type PageLayout,
type PageLayoutTab,
type PageLayout as PageLayoutGenerated,
type PageLayoutTab as PageLayoutTabGenerated,
type PageLayoutWidget,
} from '~/generated/graphql';
// TODO: Remove this once we query the data from the database
export type PageLayoutWidgetWithData = PageLayoutWidget & {
data?: Record<string, any>;
export type PageLayoutTab = Omit<PageLayoutTabGenerated, 'widgets'> & {
widgets: PageLayoutWidget[];
};
export type PageLayoutTabWithData = Omit<PageLayoutTab, 'widgets'> & {
widgets: PageLayoutWidgetWithData[];
};
export type PageLayoutWithData = Omit<PageLayout, 'tabs'> & {
tabs: PageLayoutTabWithData[];
export type PageLayout = Omit<PageLayoutGenerated, 'tabs'> & {
tabs: PageLayoutTab[];
};
@@ -1,12 +1,10 @@
import { type PageLayoutWidget } from '~/generated/graphql';
import { WidgetType } from '../../mocks/mockWidgets';
import {
type PageLayoutTabWithData,
type PageLayoutWidgetWithData,
} from '../../types/pageLayoutTypes';
import { type PageLayoutTab } from '../../types/pageLayoutTypes';
import { addWidgetToTab } from '../addWidgetToTab';
describe('addWidgetToTab', () => {
const mockWidget: PageLayoutWidgetWithData = {
const mockWidget: PageLayoutWidget = {
id: 'widget-1',
pageLayoutTabId: 'tab-1',
title: 'Test Widget',
@@ -18,7 +16,7 @@ describe('addWidgetToTab', () => {
deletedAt: null,
};
const mockTabs: PageLayoutTabWithData[] = [
const mockTabs: PageLayoutTab[] = [
{
id: 'tab-1',
title: 'Tab 1',
@@ -45,7 +43,7 @@ describe('addWidgetToTab', () => {
const result = addWidgetToTab(mockTabs, 'tab-1', mockWidget);
expect(result[0].widgets).toHaveLength(1);
expect(result[0].widgets[0]).toEqual(mockWidget);
expect(result[0].widgets?.[0]).toEqual(mockWidget);
expect(result[1].widgets).toHaveLength(0);
});
@@ -65,7 +63,7 @@ describe('addWidgetToTab', () => {
});
it('should add multiple widgets to the same tab', () => {
const secondWidget: PageLayoutWidgetWithData = {
const secondWidget: PageLayoutWidget = {
...mockWidget,
id: 'widget-2',
title: 'Second Widget',
@@ -75,8 +73,8 @@ describe('addWidgetToTab', () => {
result = addWidgetToTab(result, 'tab-1', secondWidget);
expect(result[0].widgets).toHaveLength(2);
expect(result[0].widgets[0]).toEqual(mockWidget);
expect(result[0].widgets[1]).toEqual(secondWidget);
expect(result[0].widgets?.[0]).toEqual(mockWidget);
expect(result[0].widgets?.[1]).toEqual(secondWidget);
});
it('should return a new array without mutating the original', () => {
@@ -1,9 +1,9 @@
import { type PageLayoutWidget } from '~/generated/graphql';
import { GraphType, WidgetType } from '../../mocks/mockWidgets';
import { type PageLayoutWidgetWithData } from '../../types/pageLayoutTypes';
import { convertLayoutsToWidgets } from '../convertLayoutsToWidgets';
describe('convertLayoutsToWidgets', () => {
const mockWidgets: PageLayoutWidgetWithData[] = [
const mockWidgets: PageLayoutWidget[] = [
{
id: 'widget-1',
pageLayoutTabId: 'tab-1',
@@ -19,7 +19,6 @@ describe('convertLayoutsToWidgets', () => {
configuration: {
graphType: GraphType.NUMBER,
},
data: { value: 100 },
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
deletedAt: null,
@@ -39,7 +38,6 @@ describe('convertLayoutsToWidgets', () => {
configuration: {
graphType: GraphType.PIE,
},
data: { items: [] },
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
deletedAt: null,
@@ -1,10 +1,10 @@
import { type PageLayoutWithData } from '@/page-layout/types/pageLayoutTypes';
import { type PageLayout } from '@/page-layout/types/pageLayoutTypes';
import { convertPageLayoutToTabLayouts } from '@/page-layout/utils/convertPageLayoutToTabLayouts';
import { PageLayoutType, WidgetType } from '~/generated/graphql';
describe('convertPageLayoutToTabLayouts', () => {
it('should convert page layout to tab layouts', () => {
const pageLayout: PageLayoutWithData = {
const pageLayout: PageLayout = {
id: 'page-layout-1',
name: 'Page Layout 1',
type: PageLayoutType.RECORD_PAGE,
@@ -1,9 +1,9 @@
import { WidgetType } from '../../mocks/mockWidgets';
import { type PageLayoutTabWithData } from '../../types/pageLayoutTypes';
import { type PageLayoutTab } from '../../types/pageLayoutTypes';
import { removeWidgetFromTab } from '../removeWidgetFromTab';
describe('removeWidgetFromTab', () => {
const mockTabs: PageLayoutTabWithData[] = [
const mockTabs: PageLayoutTab[] = [
{
id: 'tab-1',
title: 'Tab 1',
@@ -1,18 +1,16 @@
import {
type PageLayoutTabWithData,
type PageLayoutWidgetWithData,
} from '../types/pageLayoutTypes';
import { type PageLayoutTab } from '@/page-layout/types/pageLayoutTypes';
import { type PageLayoutWidget } from '~/generated/graphql';
export const addWidgetToTab = (
tabs: PageLayoutTabWithData[],
tabs: PageLayoutTab[],
activeTabId: string,
newWidget: PageLayoutWidgetWithData,
): PageLayoutTabWithData[] => {
newWidget: PageLayoutWidget,
): PageLayoutTab[] => {
return tabs.map((tab) => {
if (tab.id === activeTabId) {
return {
...tab,
widgets: [...tab.widgets, newWidget],
widgets: [...(tab?.widgets ?? []), newWidget],
};
}
return tab;
@@ -1,10 +1,10 @@
import { type PageLayoutWidgetWithData } from '@/page-layout/types/pageLayoutTypes';
import { type Layouts } from 'react-grid-layout';
import { type PageLayoutWidget } from '~/generated/graphql';
export const convertLayoutsToWidgets = (
widgets: PageLayoutWidgetWithData[],
widgets: PageLayoutWidget[],
layouts: Layouts,
): PageLayoutWidgetWithData[] => {
): PageLayoutWidget[] => {
const activeLayouts = layouts.desktop || layouts.mobile || [];
return widgets.map((widget) => {
@@ -0,0 +1,31 @@
import { type DraftPageLayout } from '@/page-layout/types/draft-page-layout';
import { type UpdatePageLayoutWithTabsInput } from '~/generated/graphql';
export const convertPageLayoutDraftToUpdateInput = (
pageLayoutDraft: DraftPageLayout,
): UpdatePageLayoutWithTabsInput => {
return {
name: pageLayoutDraft.name,
type: pageLayoutDraft.type,
objectMetadataId: pageLayoutDraft.objectMetadataId ?? null,
tabs: pageLayoutDraft.tabs.map((tab) => ({
id: tab.id,
title: tab.title,
position: tab.position,
widgets: tab.widgets.map((widget) => ({
id: widget.id,
pageLayoutTabId: widget.pageLayoutTabId,
title: widget.title,
type: widget.type,
objectMetadataId: widget.objectMetadataId ?? null,
gridPosition: {
row: widget.gridPosition.row,
column: widget.gridPosition.column,
rowSpan: widget.gridPosition.rowSpan,
columnSpan: widget.gridPosition.columnSpan,
},
configuration: widget.configuration ?? null,
})),
})),
};
};
@@ -1,8 +1,8 @@
import { type PageLayoutWithData } from '@/page-layout/types/pageLayoutTypes';
import { type PageLayout } from '@/page-layout/types/pageLayoutTypes';
import { type TabLayouts } from '@/page-layout/types/tab-layouts';
export const convertPageLayoutToTabLayouts = (
pageLayout: PageLayoutWithData,
pageLayout: PageLayout,
): TabLayouts => {
if (pageLayout.tabs.length === 0) {
return {};
@@ -1,6 +1,6 @@
import { type PageLayoutWithData } from '~/modules/page-layout/types/pageLayoutTypes';
import { type PageLayout } from '~/modules/page-layout/types/pageLayoutTypes';
export const isPageLayoutEmpty = (pageLayout: PageLayoutWithData): boolean => {
export const isPageLayoutEmpty = (pageLayout: PageLayout): boolean => {
return (
pageLayout.tabs.length === 1 && pageLayout.tabs[0].widgets.length === 0
);
@@ -1,10 +1,10 @@
import { type PageLayoutTabWithData } from '../types/pageLayoutTypes';
import { type PageLayoutTab } from '../types/pageLayoutTypes';
export const removeWidgetFromTab = (
tabs: PageLayoutTabWithData[],
tabs: PageLayoutTab[],
tabId: string,
widgetId: string,
): PageLayoutTabWithData[] => {
): PageLayoutTab[] => {
return tabs.map((tab) => {
if (tab.id === tabId) {
return {
@@ -46,10 +46,6 @@ export const WithNumberChart: Story = {
pageLayoutTabId: 'tab-overview',
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
data: {
value: '1,234',
trendPercentage: 12.5,
},
},
},
render: (args) => (
@@ -67,12 +63,6 @@ export const WithGaugeChart: Story = {
configuration: {
graphType: GraphType.GAUGE,
},
data: {
value: 0.5,
min: 0,
max: 1,
label: 'Conversion rate',
},
createdAt: '2024-01-01T00:00:00Z',
gridPosition: {
row: 0,
@@ -100,40 +90,6 @@ export const WithPieChart: Story = {
configuration: {
graphType: GraphType.PIE,
},
data: {
items: [
{
id: 'qualified',
value: 35,
label: 'Qualified',
to: '/leads/qualified',
},
{
id: 'contacted',
value: 25,
label: 'Contacted',
to: '/leads/contacted',
},
{
id: 'unqualified',
value: 20,
label: 'Unqualified',
to: '/leads/unqualified',
},
{
id: 'proposal',
value: 15,
label: 'Proposal',
to: '/leads/proposal',
},
{
id: 'negotiation',
value: 5,
label: 'Negotiation',
to: '/leads/negotiation',
},
],
},
createdAt: '2024-01-01T00:00:00Z',
gridPosition: {
row: 0,
@@ -161,10 +117,6 @@ export const SmallWidget: Story = {
configuration: {
graphType: GraphType.NUMBER,
},
data: {
value: '42',
trendPercentage: 5,
},
createdAt: '2024-01-01T00:00:00Z',
gridPosition: {
row: 0,
@@ -199,12 +151,6 @@ export const MediumWidget: Story = {
configuration: {
graphType: GraphType.GAUGE,
},
data: {
value: 0.75,
min: 0,
max: 1,
label: 'Progress',
},
createdAt: '2024-01-01T00:00:00Z',
gridPosition: {
row: 0,
@@ -239,14 +185,6 @@ export const LargeWidget: Story = {
configuration: {
graphType: GraphType.PIE,
},
data: {
items: [
{ id: 'a', value: 40, label: 'Category A', to: '/a' },
{ id: 'b', value: 30, label: 'Category B', to: '/b' },
{ id: 'c', value: 20, label: 'Category C', to: '/c' },
{ id: 'd', value: 10, label: 'Category D', to: '/d' },
],
},
createdAt: '2024-01-01T00:00:00Z',
gridPosition: {
row: 0,
@@ -281,10 +219,6 @@ export const WideWidget: Story = {
configuration: {
graphType: GraphType.NUMBER,
},
data: {
value: '1,234,567',
trendPercentage: 23.4,
},
createdAt: '2024-01-01T00:00:00Z',
gridPosition: {
row: 0,
@@ -319,12 +253,6 @@ export const TallWidget: Story = {
configuration: {
graphType: GraphType.GAUGE,
},
data: {
value: 0.33,
min: 0,
max: 1,
label: 'Utilization',
},
createdAt: '2024-01-01T00:00:00Z',
gridPosition: {
row: 0,
@@ -48,7 +48,7 @@ export const GraphWidgetRenderer = ({ widget }: GraphWidgetRendererProps) => {
return null;
}
const data = widget.data ?? getDefaultWidgetData(graphType);
const data: any = getDefaultWidgetData(graphType);
if (!data) {
return null;
@@ -1,8 +1,7 @@
import { type GraphType } from '@/page-layout/mocks/mockWidgets';
import { type PageLayoutWidgetWithData } from '@/page-layout/types/pageLayoutTypes';
import { type WidgetType } from '~/generated/graphql';
import { type PageLayoutWidget, type WidgetType } from '~/generated/graphql';
export type GraphWidget = PageLayoutWidgetWithData & {
export type GraphWidget = PageLayoutWidget & {
type: WidgetType.GRAPH;
configuration: {
graphType: GraphType;
@@ -1,7 +1,6 @@
import { type PageLayoutWidgetWithData } from '@/page-layout/types/pageLayoutTypes';
import { type WidgetType } from '~/generated/graphql';
import { type PageLayoutWidget, type WidgetType } from '~/generated/graphql';
export type IframeWidget = PageLayoutWidgetWithData & {
export type IframeWidget = PageLayoutWidget & {
type: WidgetType.IFRAME;
configuration: {
url: string;
@@ -6,6 +6,7 @@ import {
IsArray,
IsEnum,
IsNotEmpty,
IsOptional,
IsString,
IsUUID,
ValidateNested,
@@ -27,8 +28,9 @@ export class UpdatePageLayoutWithTabsInput {
@IsNotEmpty()
type: PageLayoutType;
@Field(() => UUIDScalarType)
@Field(() => UUIDScalarType, { nullable: true })
@IsUUID()
@IsOptional()
objectMetadataId: string | null;
@Field(() => [UpdatePageLayoutTabWithWidgetsInput])