Widget cards (#15387)

figma -
https://www.figma.com/design/xt8O9mFeLl46C5InWwoMrN/Twenty?node-id=79795-372071&t=0Rg3h23G8F7v80cs-0

for the pinned tab column or the left column -- there is only one state
change! -- which, after talks with @Bonapara -- we should try to keep
the same if possible! -- right now, there is no way to test it -- so I
did not add the same

closes -
https://discord.com/channels/1130383047699738754/1430825706643918942 and
https://discord.com/channels/1130383047699738754/1430599763807436870

story QA - 
<img width="1535" height="1239" alt="Screenshot 2025-10-28 at 00 46 23"
src="https://github.com/user-attachments/assets/9293983e-2d08-427b-971f-731e6f1517aa"
/>
This commit is contained in:
nitin
2025-10-28 20:25:52 +05:30
committed by GitHub
parent da0e5ba342
commit 0fdf76560d
15 changed files with 563 additions and 144 deletions
@@ -11,6 +11,7 @@ import { usePageLayoutHandleLayoutChange } from '@/page-layout/hooks/usePageLayo
import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
import { pageLayoutCurrentBreakpointComponentState } from '@/page-layout/states/pageLayoutCurrentBreakpointComponentState';
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
import { pageLayoutDraggingWidgetIdComponentState } from '@/page-layout/states/pageLayoutDraggingWidgetIdComponentState';
import { WidgetPlaceholder } from '@/page-layout/widgets/components/WidgetPlaceholder';
import { WidgetRenderer } from '@/page-layout/widgets/components/WidgetRenderer';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
@@ -39,12 +40,16 @@ const StyledGridContainer = styled.div`
.react-grid-placeholder {
background: ${({ theme }) => theme.adaptiveColors.blue3} !important;
border-radius: ${({ theme }) => theme.border.radius.sm};
border-radius: ${({ theme }) => theme.border.radius.md};
}
.react-grid-item:not(.react-draggable-dragging) {
user-select: auto;
}
.react-grid-item:hover .widget-card-resize-handle {
display: block !important;
}
`;
type ExtendedResponsiveProps = ResponsiveProps & {
@@ -75,6 +80,10 @@ export const PageLayoutGridLayout = ({ tabId }: PageLayoutGridLayoutProps) => {
pageLayoutCurrentBreakpointComponentState,
);
const setDraggingWidgetId = useSetRecoilComponentState(
pageLayoutDraggingWidgetIdComponentState,
);
const { handleLayoutChange } = usePageLayoutHandleLayoutChange();
const gridContainerRef = useRef<HTMLDivElement>(null);
@@ -153,6 +162,12 @@ export const PageLayoutGridLayout = ({ tabId }: PageLayoutGridLayoutProps) => {
) : undefined
}
resizeHandles={['se']}
onDragStart={(_layout, _oldItem, newItem) => {
setDraggingWidgetId(newItem.i);
}}
onDragStop={() => {
setDraggingWidgetId(null);
}}
onLayoutChange={handleLayoutChange}
onBreakpointChange={(newBreakpoint) =>
setPageLayoutCurrentBreakpoint(
@@ -18,6 +18,7 @@ const StyledIcon = styled(IconRadiusBottomRight)`
position: absolute;
right: ${({ theme }) => theme.spacing(1)};
color: ${({ theme }) => theme.border.color.strong};
display: none;
:hover {
color: ${({ theme }) => theme.font.color.tertiary};
@@ -49,7 +50,11 @@ export const PageLayoutGridResizeHandle = forwardRef<
className={className}
style={style}
>
<StyledIcon size={theme.spacing(4)} stroke={theme.spacing(1)} />
<StyledIcon
size={theme.spacing(4)}
stroke={theme.spacing(1)}
className="widget-card-resize-handle"
/>
</div>
);
});
@@ -1,4 +1,6 @@
import { pageLayoutDraggingWidgetIdComponentState } from '@/page-layout/states/pageLayoutDraggingWidgetIdComponentState';
import { WidgetRenderer } from '@/page-layout/widgets/components/WidgetRenderer';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import styled from '@emotion/styled';
import {
DragDropContext,
@@ -33,8 +35,20 @@ export const PageLayoutVerticalListEditor = ({
}: PageLayoutVerticalListEditorProps) => {
const droppableId = `page-layout-vertical-list-${useId()}`;
const setDraggingWidgetId = useSetRecoilComponentState(
pageLayoutDraggingWidgetIdComponentState,
);
return (
<DragDropContext onDragEnd={onReorder}>
<DragDropContext
onDragStart={(result) => {
setDraggingWidgetId(result.draggableId);
}}
onDragEnd={(result) => {
setDraggingWidgetId(null);
onReorder(result);
}}
>
<Droppable droppableId={droppableId}>
{(provided) => (
<StyledVerticalListContainer
@@ -53,7 +67,10 @@ export const PageLayoutVerticalListEditor = ({
>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<div {...provided.dragHandleProps}>
<WidgetRenderer widget={widget} />
<WidgetRenderer
widget={widget}
widgetCardContext="recordPage"
/>
</div>
</StyledDraggableWrapper>
)}
@@ -19,7 +19,7 @@ export const PageLayoutVerticalListViewer = ({
<StyledVerticalListContainer>
{widgets.map((widget) => (
<div key={widget.id}>
<WidgetRenderer widget={widget} />
<WidgetRenderer widget={widget} widgetCardContext="recordPage" />
</div>
))}
</StyledVerticalListContainer>
@@ -0,0 +1,11 @@
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
import { PageLayoutComponentInstanceContext } from './contexts/PageLayoutComponentInstanceContext';
export const pageLayoutDraggingWidgetIdComponentState = createComponentState<
string | null
>({
key: 'pageLayoutDraggingWidgetIdComponentState',
defaultValue: null,
componentInstanceContext: PageLayoutComponentInstanceContext,
});
@@ -1,54 +0,0 @@
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;
isPageLayoutInEditMode?: boolean;
}>`
background: ${({ theme }) => theme.background.secondary};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: ${({ theme }) => theme.border.radius.md};
box-sizing: border-box;
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
position: relative;
padding: ${({ theme }) => theme.spacing(4)};
${({ onClick, theme, isPageLayoutInEditMode }) =>
isPageLayoutInEditMode &&
`
&:hover {
cursor: ${isDefined(onClick) ? 'pointer' : 'default'};
border: 1px solid ${theme.color.blue};
background: ${theme.background.primary};
}
`}
`;
type WidgetContainerProps = {
children?: ReactNode;
onClick?: () => void;
};
export const WidgetContainer = ({
children,
onClick,
}: WidgetContainerProps) => {
const isPageLayoutInEditMode = useRecoilComponentValue(
isPageLayoutInEditModeComponentState,
);
return (
<StyledContainer
onClick={onClick}
isPageLayoutInEditMode={isPageLayoutInEditMode}
>
{children}
</StyledContainer>
);
};
@@ -1,55 +0,0 @@
import styled from '@emotion/styled';
import { IconX } from 'twenty-ui/display';
import { IconButton } from 'twenty-ui/input';
import { WidgetGrip } from './WidgetGrip';
const StyledHeader = styled.div`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
height: ${({ theme }) => theme.spacing(6)};
flex-shrink: 0;
`;
const StyledTitle = styled.span`
color: ${({ theme }) => theme.font.color.primary};
flex: 1;
font-size: ${({ theme }) => theme.font.size.sm};
font-weight: ${({ theme }) => theme.font.weight.medium};
user-select: none;
`;
type WidgetHeaderProps = {
isInEditMode: boolean;
isEmpty?: boolean;
title: string;
onRemove?: (e?: React.MouseEvent) => void;
};
export const WidgetHeader = ({
isEmpty = false,
isInEditMode = false,
title,
onRemove,
}: WidgetHeaderProps) => {
return (
<StyledHeader>
{!isEmpty && isInEditMode && (
<WidgetGrip
className="drag-handle"
onClick={(e) => e.stopPropagation()}
/>
)}
<StyledTitle>{isEmpty ? 'Add Widget' : title}</StyledTitle>
{!isEmpty && isInEditMode && onRemove && (
<IconButton
onClick={onRemove}
Icon={IconX}
variant="tertiary"
size="small"
/>
)}
</StyledHeader>
);
};
@@ -3,8 +3,8 @@ import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
import { WidgetContainer } from '@/page-layout/widgets/components/WidgetContainer';
import { WidgetHeader } from '@/page-layout/widgets/components/WidgetHeader';
import { WidgetCard } from '@/page-layout/widgets/widget-card/components/WidgetCard';
import { WidgetCardHeader } from '@/page-layout/widgets/widget-card/components/WidgetCardHeader';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { t } from '@lingui/core/macro';
@@ -42,8 +42,13 @@ export const WidgetPlaceholder = () => {
};
return (
<WidgetContainer onClick={handleClick}>
<WidgetHeader
<WidgetCard
onClick={handleClick}
widgetCardContext="dashboard"
isEditing={false}
isDragging={false}
>
<WidgetCardHeader
isInEditMode={isPageLayoutInEditMode}
title={t`Add Widget`}
isEmpty
@@ -62,6 +67,6 @@ export const WidgetPlaceholder = () => {
</AnimatedPlaceholderEmptySubTitle>
</AnimatedPlaceholderEmptyTextContainer>
</AnimatedPlaceholderEmptyContainer>
</WidgetContainer>
</WidgetCard>
);
};
@@ -1,29 +1,28 @@
import { useDeletePageLayoutWidget } from '@/page-layout/hooks/useDeletePageLayoutWidget';
import { useEditPageLayoutWidget } from '@/page-layout/hooks/useEditPageLayoutWidget';
import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
import { pageLayoutDraggingWidgetIdComponentState } from '@/page-layout/states/pageLayoutDraggingWidgetIdComponentState';
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
import { PageLayoutWidgetForbiddenDisplay } from '@/page-layout/widgets/components/PageLayoutWidgetForbiddenDisplay';
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 { useWidgetPermissions } from '@/page-layout/widgets/hooks/useWidgetPermissions';
import { WidgetCard } from '@/page-layout/widgets/widget-card/components/WidgetCard';
import { WidgetCardContent } from '@/page-layout/widgets/widget-card/components/WidgetCardContent';
import { WidgetCardHeader } from '@/page-layout/widgets/widget-card/components/WidgetCardHeader';
import { type WidgetCardContext } from '@/page-layout/widgets/widget-card/types/WidgetCardContext';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import styled from '@emotion/styled';
import { type MouseEvent } from 'react';
import { type PageLayoutWidget } from '~/generated/graphql';
type WidgetRendererProps = {
widget: PageLayoutWidget;
widgetCardContext?: WidgetCardContext;
};
const StyledContent = styled.div`
align-items: center;
display: flex;
height: 100%;
width: 100%;
justify-content: center;
`;
export const WidgetRenderer = ({ widget }: WidgetRendererProps) => {
export const WidgetRenderer = ({
widget,
widgetCardContext = 'dashboard',
}: WidgetRendererProps) => {
const { deletePageLayoutWidget } = useDeletePageLayoutWidget();
const { handleEditWidget } = useEditPageLayoutWidget();
@@ -31,6 +30,18 @@ export const WidgetRenderer = ({ widget }: WidgetRendererProps) => {
isPageLayoutInEditModeComponentState,
);
const draggingWidgetId = useRecoilComponentValue(
pageLayoutDraggingWidgetIdComponentState,
);
const currentlyEditingWidgetId = useRecoilComponentValue(
pageLayoutEditingWidgetIdComponentState,
);
const isEditing = currentlyEditingWidgetId === widget.id;
const isDragging = draggingWidgetId === widget.id;
const { hasAccess, restriction } = useWidgetPermissions(widget);
const handleClick = () => {
@@ -46,22 +57,28 @@ export const WidgetRenderer = ({ widget }: WidgetRendererProps) => {
};
return (
<WidgetContainer onClick={isPageLayoutInEditMode ? handleClick : undefined}>
<WidgetHeader
<WidgetCard
onClick={isPageLayoutInEditMode ? handleClick : undefined}
isDragging={isDragging}
widgetCardContext={widgetCardContext}
isEditing={isEditing}
>
<WidgetCardHeader
isInEditMode={isPageLayoutInEditMode}
title={widget.title}
onRemove={handleRemove}
forbiddenDisplay={
!hasAccess && (
<PageLayoutWidgetForbiddenDisplay
widgetId={widget.id}
restriction={restriction}
/>
)
}
/>
<StyledContent>
{!hasAccess ? (
<PageLayoutWidgetForbiddenDisplay
widgetId={widget.id}
restriction={restriction}
/>
) : (
<WidgetContentRenderer widget={widget} />
)}
</StyledContent>
</WidgetContainer>
<WidgetCardContent widgetCardContext={widgetCardContext}>
{hasAccess && <WidgetContentRenderer widget={widget} />}
</WidgetCardContent>
</WidgetCard>
);
};
@@ -0,0 +1,152 @@
import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { css } from '@emotion/react';
import styled from '@emotion/styled';
import { type ReactNode } from 'react';
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
import { type WidgetCardContext } from '../types/WidgetCardContext';
export type WidgetCardProps = {
children?: ReactNode;
widgetCardContext: WidgetCardContext;
onClick?: () => void;
isEditing: boolean;
isDragging: boolean;
className?: string;
};
const StyledWidgetCard = styled.div<{
onClick?: () => void;
widgetCardContext: WidgetCardContext;
isPageLayoutInEditMode: boolean;
isEditing: boolean;
isDragging: boolean;
}>`
box-sizing: border-box;
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
position: relative;
${({
theme,
widgetCardContext,
isPageLayoutInEditMode,
isEditing,
isDragging,
onClick,
}) => {
switch (widgetCardContext) {
case 'dashboard':
return css`
background: ${theme.background.secondary};
border: 1px solid ${theme.border.color.light};
border-radius: ${theme.border.radius.md};
padding: ${theme.spacing(2)};
gap: ${theme.spacing(2)};
${isPageLayoutInEditMode &&
!isDragging &&
!isEditing &&
css`
&:hover {
cursor: ${isDefined(onClick) ? 'pointer' : 'default'};
border: 1px solid ${theme.border.color.strong};
.widget-card-remove-button {
display: block !important;
}
}
`}
${isEditing &&
!isDragging &&
css`
border: 1px solid ${theme.color.blue} !important;
`}
${isDragging &&
css`
background: linear-gradient(
0deg,
${theme.background.transparent.lighter} 0%,
${theme.background.transparent.lighter} 100%
),
${theme.background.secondary};
border: 1px solid ${theme.color.blue} !important;
`}
`;
case 'recordPage':
return css`
background: ${theme.background.primary};
border: 1px solid transparent;
border-radius: ${theme.border.radius.md};
gap: ${theme.spacing(2)};
padding: ${theme.spacing(2)};
${isPageLayoutInEditMode &&
!isDragging &&
!isEditing &&
css`
&:hover {
cursor: ${isDefined(onClick) ? 'pointer' : 'default'};
border: 1px solid ${theme.border.color.strong};
.widget-card-remove-button {
display: block !important;
}
}
`}
${isEditing &&
!isDragging &&
css`
border: 1px solid ${theme.color.blue} !important;
`}
${isDragging &&
css`
background: linear-gradient(
0deg,
${theme.background.transparent.lighter} 0%,
${theme.background.transparent.lighter} 100%
),
${theme.background.secondary};
border: 1px solid ${theme.color.blue} !important;
`}
`;
default:
return assertUnreachable(widgetCardContext);
}
}}
`;
export const WidgetCard = ({
children,
widgetCardContext,
onClick,
isEditing,
isDragging,
className,
}: WidgetCardProps) => {
const isPageLayoutInEditMode = useRecoilComponentValue(
isPageLayoutInEditModeComponentState,
);
return (
<StyledWidgetCard
onClick={onClick}
widgetCardContext={widgetCardContext}
isPageLayoutInEditMode={isPageLayoutInEditMode}
isEditing={isEditing}
isDragging={isDragging}
className={className}
>
{children}
</StyledWidgetCard>
);
};
@@ -0,0 +1,40 @@
import { css } from '@emotion/react';
import styled from '@emotion/styled';
import { type ReactNode } from 'react';
import { assertUnreachable } from 'twenty-shared/utils';
import { type WidgetCardContext } from '../types/WidgetCardContext';
export type WidgetCardContentProps = {
children?: ReactNode;
widgetCardContext: WidgetCardContext;
className?: string;
};
const StyledWidgetCardContent = styled.div<WidgetCardContentProps>`
align-items: center;
display: flex;
height: 100%;
width: 100%;
justify-content: center;
box-sizing: border-box;
padding: ${({ theme }) => theme.spacing(2)};
${({ theme, widgetCardContext }) => {
switch (widgetCardContext) {
case 'recordPage':
return css`
border: 1px solid ${theme.border.color.medium};
border-radius: ${theme.border.radius.md};
`;
case 'dashboard':
return '';
default:
return assertUnreachable(widgetCardContext);
}
}}
`;
export { StyledWidgetCardContent as WidgetCardContent };
@@ -0,0 +1,72 @@
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { type ReactNode } from 'react';
import { IconTrash, OverflowingTextWithTooltip } from 'twenty-ui/display';
import { IconButton } from 'twenty-ui/input';
import { WidgetGrip } from '@/page-layout/widgets/widget-card/components/WidgetGrip';
import { isDefined } from 'twenty-shared/utils';
export type WidgetCardHeaderProps = {
isInEditMode: boolean;
isEmpty?: boolean;
title: string;
onRemove?: (e?: React.MouseEvent) => void;
forbiddenDisplay?: ReactNode;
className?: string;
};
const StyledWidgetCardHeader = styled.div`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
height: ${({ theme }) => theme.spacing(6)};
flex-shrink: 0;
`;
const StyledTitleContainer = styled.div`
color: ${({ theme }) => theme.font.color.primary};
flex: 1;
font-size: ${({ theme }) => theme.font.size.sm};
padding-inline: ${({ theme }) => theme.spacing(1)};
font-weight: ${({ theme }) => theme.font.weight.medium};
user-select: none;
overflow: hidden;
`;
const StyledIconButton = styled(IconButton)`
display: none;
`;
export const WidgetCardHeader = ({
isEmpty = false,
isInEditMode = false,
title,
onRemove,
forbiddenDisplay,
className,
}: WidgetCardHeaderProps) => {
return (
<StyledWidgetCardHeader className={className}>
{!isEmpty && isInEditMode && (
<WidgetGrip
className="drag-handle"
onClick={(e) => e.stopPropagation()}
/>
)}
<StyledTitleContainer>
<OverflowingTextWithTooltip text={isEmpty ? t`Add Widget` : title} />
</StyledTitleContainer>
{isDefined(forbiddenDisplay) && forbiddenDisplay}
{!isEmpty && isInEditMode && onRemove && (
<StyledIconButton
onClick={onRemove}
Icon={IconTrash}
variant="tertiary"
size="small"
className="widget-card-remove-button"
/>
)}
</StyledWidgetCardHeader>
);
};
@@ -0,0 +1,193 @@
import styled from '@emotion/styled';
import { type Meta, type StoryObj } from '@storybook/react';
import { useEffect, type ReactNode } from 'react';
import { CatalogDecorator, type CatalogStory } from 'twenty-ui/testing';
import { ForbiddenFieldDisplay } from '@/object-record/record-field/ui/meta-types/display/components/ForbiddenFieldDisplay';
import { PageLayoutTestWrapper } from '@/page-layout/hooks/__tests__/PageLayoutTestWrapper';
import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
import { WidgetCard } from '@/page-layout/widgets/widget-card/components/WidgetCard';
import { WidgetCardContent } from '@/page-layout/widgets/widget-card/components/WidgetCardContent';
import { WidgetCardHeader } from '@/page-layout/widgets/widget-card/components/WidgetCardHeader';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
const StyledContainer = styled.div`
height: 200px;
width: 300px;
`;
const StyledMockContent = styled.div`
align-items: center;
color: ${({ theme }) => theme.font.color.tertiary};
display: flex;
font-size: ${({ theme }) => theme.font.size.sm};
justify-content: center;
`;
const EditModeWrapper = ({
children,
isInEditMode,
}: {
children: ReactNode;
isInEditMode: boolean;
}) => {
const setIsPageLayoutInEditMode = useSetRecoilComponentState(
isPageLayoutInEditModeComponentState,
);
useEffect(() => {
setIsPageLayoutInEditMode(isInEditMode);
}, [isInEditMode, setIsPageLayoutInEditMode]);
return <>{children}</>;
};
const meta: Meta<typeof WidgetCard> = {
title: 'Modules/PageLayout/Widgets/WidgetCard',
component: WidgetCard,
};
export default meta;
type Story = StoryObj<typeof WidgetCard>;
export const Default: Story = {
decorators: [
(Story) => {
return (
<PageLayoutTestWrapper>
<EditModeWrapper isInEditMode={true}>
<StyledContainer>
<Story />
</StyledContainer>
</EditModeWrapper>
</PageLayoutTestWrapper>
);
},
I18nFrontDecorator,
],
parameters: {
pseudo: { hover: false },
},
args: {
widgetCardContext: 'dashboard',
isEditing: false,
isDragging: false,
},
render: (args) => (
<WidgetCard
widgetCardContext={args.widgetCardContext}
isEditing={args.isEditing}
isDragging={args.isDragging}
>
<WidgetCardHeader
isInEditMode={true}
onRemove={() => {}}
title="Widget name"
/>
<WidgetCardContent widgetCardContext={args.widgetCardContext}>
<StyledMockContent>Widget</StyledMockContent>
</WidgetCardContent>
</WidgetCard>
),
};
export const Catalog: CatalogStory<Story, typeof WidgetCard> = {
decorators: [
(Story) => {
return (
<StyledContainer>
<Story />
</StyledContainer>
);
},
CatalogDecorator,
I18nFrontDecorator,
],
parameters: {
pseudo: { hover: ['.hover'] },
catalog: {
dimensions: [
{
name: 'contextVariant',
values: [
'Record Page - Default',
'Record Page - Restriction',
'Dashboard - Default',
'Dashboard - Restriction',
],
props: (contextName: string) => {
const widgetCardContext = contextName.includes('Record')
? 'recordPage'
: 'dashboard';
const hasRestriction = contextName.includes('Restriction');
return {
widgetCardContext,
contextVariant: contextName,
hasRestriction,
};
},
},
{
name: 'state',
values: ['Default', 'Hover', 'Selected', 'Dragging', 'Read Mode'],
props: (state: string) => {
const stateProps: {
className?: string;
isDragging?: boolean;
isEditing?: boolean;
} = {};
switch (state) {
case 'Hover':
stateProps.className = 'hover';
break;
case 'Selected':
stateProps.isEditing = true;
break;
case 'Dragging':
stateProps.isDragging = true;
break;
case 'Read Mode':
stateProps.className = 'read-mode';
break;
}
return { ...stateProps, state };
},
},
],
},
},
render: (args: any) => {
const isReadMode = args.state === 'Read Mode';
const widgetCardContext = args.widgetCardContext || 'dashboard';
const hasRestriction = args.hasRestriction || false;
return (
<PageLayoutTestWrapper>
<EditModeWrapper isInEditMode={!isReadMode}>
<WidgetCard
className={args.className}
isDragging={args.isDragging ?? false}
isEditing={args.isEditing ?? false}
widgetCardContext={widgetCardContext}
>
<WidgetCardHeader
forbiddenDisplay={
hasRestriction ? <ForbiddenFieldDisplay /> : undefined
}
isInEditMode={!isReadMode}
onRemove={!isReadMode ? () => {} : undefined}
title="Widget name"
/>
<WidgetCardContent widgetCardContext={widgetCardContext}>
<StyledMockContent>Widget</StyledMockContent>
</WidgetCardContent>
</WidgetCard>
</EditModeWrapper>
</PageLayoutTestWrapper>
);
},
};
@@ -0,0 +1 @@
export type WidgetCardContext = 'dashboard' | 'recordPage';