Page Layout Initialization on Settings (#14156)
This commit is contained in:
@@ -79,6 +79,7 @@
|
||||
"react-datepicker": "^6.7.1",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-error-boundary": "^4.0.11",
|
||||
"react-grid-layout": "^1.5.2",
|
||||
"react-helmet-async": "^1.3.0",
|
||||
"react-hook-form": "^7.45.1",
|
||||
"react-hotkeys-hook": "^4.4.4",
|
||||
@@ -105,6 +106,7 @@
|
||||
"@types/apollo-upload-client": "^17.0.2",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/js-cookie": "^3.0.3",
|
||||
"@types/react-grid-layout": "^1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.39.0",
|
||||
"@typescript-eslint/parser": "^8.39.0",
|
||||
"@typescript-eslint/utils": "^8.39.0",
|
||||
|
||||
@@ -297,6 +297,12 @@ const SettingsObjectFieldEdit = lazy(() =>
|
||||
),
|
||||
);
|
||||
|
||||
const PageLayoutEdition = lazy(() =>
|
||||
import('~/pages/settings/page-layout/PageLayoutEdition').then((module) => ({
|
||||
default: module.PageLayoutEdition,
|
||||
})),
|
||||
);
|
||||
|
||||
const SettingsSecurity = lazy(() =>
|
||||
import('~/pages/settings/security/SettingsSecurity').then((module) => ({
|
||||
default: module.SettingsSecurity,
|
||||
@@ -605,6 +611,10 @@ export const SettingsRoutes = ({
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Route
|
||||
path={SettingsPath.PageLayoutEdition}
|
||||
element={<PageLayoutEdition />}
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<SettingsProtectedRouteWrapper
|
||||
|
||||
-4
@@ -201,10 +201,6 @@ export const GraphWidgetGaugeChart = ({
|
||||
onClick={handleClick}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
radialAxisStart={null}
|
||||
radialAxisEnd={null}
|
||||
circularAxisInner={null}
|
||||
circularAxisOuter={null}
|
||||
layers={['bars', renderValueEndLine]}
|
||||
/>
|
||||
{showValue && (
|
||||
|
||||
+2
-1
@@ -21,8 +21,9 @@ const StyledTrendPercentageValue = styled.span`
|
||||
`;
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { PageBody } from '@/ui/layout/page/components/PageBody';
|
||||
import { PageContainer } from '@/ui/layout/page/components/PageContainer';
|
||||
import { PageHeader } from '@/ui/layout/page/components/PageHeader';
|
||||
import {
|
||||
Breadcrumb,
|
||||
type BreadcrumbProps,
|
||||
} from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
type SettingsPageFullWidthContainerProps = {
|
||||
children: ReactNode;
|
||||
links: BreadcrumbProps['links'];
|
||||
};
|
||||
|
||||
export const SettingsPageFullWidthContainer = ({
|
||||
children,
|
||||
links,
|
||||
}: SettingsPageFullWidthContainerProps) => {
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageHeader title={<Breadcrumb links={links} />} />
|
||||
<PageBody>{children}</PageBody>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
IconFunction,
|
||||
IconHierarchy2,
|
||||
IconKey,
|
||||
IconLayout,
|
||||
IconLock,
|
||||
IconMail,
|
||||
IconRocket,
|
||||
@@ -66,6 +67,9 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => {
|
||||
false;
|
||||
const labPublicFeatureFlags = useRecoilValue(labPublicFeatureFlagsState);
|
||||
const isAIEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
|
||||
const isPageLayoutEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_PAGE_LAYOUT_ENABLED,
|
||||
);
|
||||
|
||||
const permissionMap = usePermissionFlagMap();
|
||||
return [
|
||||
@@ -137,6 +141,15 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => {
|
||||
Icon: IconHierarchy2,
|
||||
isHidden: !permissionMap[PermissionFlagType.DATA_MODEL],
|
||||
},
|
||||
{
|
||||
label: t`Page Layout`,
|
||||
path: SettingsPath.PageLayoutEdition,
|
||||
Icon: IconLayout,
|
||||
isHidden:
|
||||
!isPageLayoutEnabled ||
|
||||
!permissionMap[PermissionFlagType.WORKSPACE],
|
||||
isNew: true,
|
||||
},
|
||||
{
|
||||
label: t`Integrations`,
|
||||
path: SettingsPath.Integrations,
|
||||
|
||||
@@ -53,4 +53,5 @@ export enum SettingsPath {
|
||||
RoleDetail = 'roles/:roleId',
|
||||
RoleObjectLevel = 'roles/:roleId/object/:objectMetadataId',
|
||||
RoleAddObjectLevel = 'roles/:roleId/add-object-permission',
|
||||
PageLayoutEdition = 'page-layout/edition',
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
import { Global, css, useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { AnimatePresence, LayoutGroup, motion } from 'framer-motion';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { Outlet, useLocation } from 'react-router-dom';
|
||||
import { useScreenSize } from 'twenty-ui/utilities';
|
||||
|
||||
const StyledLayout = styled.div`
|
||||
@@ -59,6 +59,10 @@ const StyledMainContainer = styled.div`
|
||||
export const DefaultLayout = () => {
|
||||
const isMobile = useIsMobile();
|
||||
const isSettingsPage = useIsSettingsPage();
|
||||
const location = useLocation();
|
||||
const isPageLayoutEditor = location.pathname.includes(
|
||||
'/settings/page-layout/edition',
|
||||
);
|
||||
const theme = useTheme();
|
||||
const windowsWidth = useScreenSize().width;
|
||||
const showAuthModal = useShowAuthModal();
|
||||
@@ -78,7 +82,10 @@ export const DefaultLayout = () => {
|
||||
<StyledPageContainer
|
||||
animate={{
|
||||
marginLeft:
|
||||
isSettingsPage && !isMobile && !useShowFullScreen
|
||||
isSettingsPage &&
|
||||
!isMobile &&
|
||||
!useShowFullScreen &&
|
||||
!isPageLayoutEditor
|
||||
? (windowsWidth -
|
||||
(OBJECT_SETTINGS_WIDTH +
|
||||
NAV_DRAWER_WIDTHS.menu.desktop.expanded +
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
import { PagePanel } from './PagePanel';
|
||||
import { MOBILE_VIEWPORT } from 'twenty-ui/theme';
|
||||
import { PagePanel } from './PagePanel';
|
||||
|
||||
type PageBodyProps = {
|
||||
children: ReactNode;
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
import { GraphWidgetGaugeChart } from '@/dashboards/graphs/components/GraphWidgetGaugeChart';
|
||||
import { GraphWidgetNumberChart } from '@/dashboards/graphs/components/GraphWidgetNumberChart';
|
||||
import { GraphWidgetPieChart } from '@/dashboards/graphs/components/GraphWidgetPieChart';
|
||||
import { SettingsPageFullWidthContainer } from '@/settings/components/SettingsPageFullWidthContainer';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import styled from '@emotion/styled';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import {
|
||||
Responsive,
|
||||
WidthProvider,
|
||||
type Layout,
|
||||
type Layouts,
|
||||
type ResponsiveProps,
|
||||
} from 'react-grid-layout';
|
||||
import 'react-grid-layout/css/styles.css';
|
||||
import 'react-resizable/css/styles.css';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||
import { PageLayoutWidgetPlaceholder } from './components/PageLayoutWidgetPlaceholder';
|
||||
import { mockLayouts, mockWidgets } from './mocks/mockWidgets';
|
||||
import {
|
||||
pageLayoutLayoutsState,
|
||||
pageLayoutWidgetsState,
|
||||
} from './states/pageLayoutState';
|
||||
|
||||
const StyledGridContainer = styled.div`
|
||||
background: ${({ theme }) => theme.background.secondary};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||
box-sizing: border-box;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
.react-grid-placeholder {
|
||||
background: ${({ theme }) => theme.adaptiveColors.blue3} !important;
|
||||
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledGridOverlay = styled.div`
|
||||
position: absolute;
|
||||
top: ${({ theme }) => theme.spacing(2)};
|
||||
left: ${({ theme }) => theme.spacing(2)};
|
||||
right: ${({ theme }) => theme.spacing(2)};
|
||||
bottom: ${({ theme }) => theme.spacing(2)};
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, 1fr);
|
||||
grid-auto-rows: 50px;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
|
||||
@media (max-width: 480px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) and (min-width: 481px) {
|
||||
grid-template-columns: repeat(12, 1fr);
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledGridCell = styled.div`
|
||||
background: 'transparent';
|
||||
border: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
`;
|
||||
|
||||
type ExtendedResponsiveProps = ResponsiveProps & {
|
||||
maxCols?: number;
|
||||
preventCollision?: boolean;
|
||||
};
|
||||
|
||||
const ResponsiveGridLayout = WidthProvider(
|
||||
Responsive,
|
||||
) as React.ComponentType<ExtendedResponsiveProps>;
|
||||
|
||||
export const PageLayoutEdition = () => {
|
||||
const { t } = useLingui();
|
||||
const [pageLayoutWidgets, setPageLayoutWidgets] = useRecoilState(
|
||||
pageLayoutWidgetsState,
|
||||
);
|
||||
const [pageLayoutLayouts, setPageLayoutLayouts] = useRecoilState(
|
||||
pageLayoutLayoutsState,
|
||||
);
|
||||
|
||||
const handleLayoutChange = (_: Layout[], allLayouts: Layouts) => {
|
||||
setPageLayoutLayouts(allLayouts);
|
||||
};
|
||||
|
||||
const handleAddWidget = () => {
|
||||
// temp: Populate with mock data when empty placeholder is clicked
|
||||
setPageLayoutWidgets(mockWidgets);
|
||||
setPageLayoutLayouts(mockLayouts);
|
||||
};
|
||||
|
||||
const isEmptyState = pageLayoutWidgets.length === 0;
|
||||
|
||||
const emptyLayout: Layouts = {
|
||||
lg: [{ i: 'empty-placeholder', x: 0, y: 0, w: 4, h: 4, static: true }],
|
||||
md: [{ i: 'empty-placeholder', x: 0, y: 0, w: 4, h: 4, static: true }],
|
||||
sm: [{ i: 'empty-placeholder', x: 0, y: 0, w: 1, h: 4, static: true }],
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingsPageFullWidthContainer
|
||||
links={[
|
||||
{
|
||||
children: t`Settings`,
|
||||
href: getSettingsPath(SettingsPath.Workspace),
|
||||
},
|
||||
{
|
||||
children: t`Page Layout`,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<StyledGridContainer>
|
||||
<StyledGridOverlay>
|
||||
{Array.from({ length: 12 * 20 }).map((_, i) => (
|
||||
<StyledGridCell key={i} />
|
||||
))}
|
||||
</StyledGridOverlay>
|
||||
<ResponsiveGridLayout
|
||||
className="layout"
|
||||
layouts={isEmptyState ? emptyLayout : pageLayoutLayouts}
|
||||
breakpoints={{ lg: 1024, md: 768, sm: 480 }}
|
||||
cols={{ lg: 12, md: 12, sm: 1 }}
|
||||
rowHeight={50}
|
||||
containerPadding={[0, 0]}
|
||||
margin={[8, 8]}
|
||||
isDraggable={true}
|
||||
isResizable={true}
|
||||
draggableHandle=".drag-handle"
|
||||
compactType="vertical"
|
||||
preventCollision={false}
|
||||
onLayoutChange={handleLayoutChange}
|
||||
>
|
||||
{isEmptyState ? (
|
||||
<div key="empty-placeholder" onClick={handleAddWidget}>
|
||||
<PageLayoutWidgetPlaceholder title="" isEmpty={true} />
|
||||
</div>
|
||||
) : (
|
||||
pageLayoutWidgets.map((widget) => (
|
||||
<div key={widget.id}>
|
||||
<PageLayoutWidgetPlaceholder title={widget.title}>
|
||||
{widget.type === 'number' && (
|
||||
<GraphWidgetNumberChart
|
||||
value={widget.data.value}
|
||||
trendPercentage={widget.data.trendPercentage}
|
||||
/>
|
||||
)}
|
||||
{widget.type === 'gauge' && (
|
||||
<GraphWidgetGaugeChart
|
||||
data={{
|
||||
value: widget.data.value,
|
||||
min: widget.data.min,
|
||||
max: widget.data.max,
|
||||
label: widget.data.label,
|
||||
}}
|
||||
displayType="percentage"
|
||||
showValue={true}
|
||||
id={`gauge-chart-${widget.id}`}
|
||||
/>
|
||||
)}
|
||||
{widget.type === 'pie' && (
|
||||
<GraphWidgetPieChart
|
||||
data={widget.data.items}
|
||||
showLegend={true}
|
||||
displayType="percentage"
|
||||
id={`pie-chart-${widget.id}`}
|
||||
/>
|
||||
)}
|
||||
</PageLayoutWidgetPlaceholder>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</ResponsiveGridLayout>
|
||||
</StyledGridContainer>
|
||||
</SettingsPageFullWidthContainer>
|
||||
);
|
||||
};
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { type ReactNode } from 'react';
|
||||
import { IconGripVertical, IconX } from 'twenty-ui/display';
|
||||
|
||||
const StyledPlaceholderContainer = styled.div<{ $isEmpty?: boolean }>`
|
||||
background: ${({ theme }) => theme.background.secondary};
|
||||
border: 1px solid
|
||||
${({ theme, $isEmpty }) =>
|
||||
$isEmpty ? theme.border.color.light : theme.border.color.medium};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding: ${({ theme }) => theme.spacing(4)};
|
||||
${({ $isEmpty, theme }) =>
|
||||
$isEmpty &&
|
||||
`
|
||||
border-style: solid;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: ${theme.background.tertiary};
|
||||
border-color: ${theme.border.color.medium};
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const StyledHeader = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledDragHandle = styled.div`
|
||||
cursor: grab;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledTitle = styled.span`
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
flex: 1;
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
`;
|
||||
|
||||
const StyledCloseButton = styled.button`
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledContent = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const StyledEmptyState = styled.div`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledEmptyText = styled.span`
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
`;
|
||||
|
||||
type PageLayoutWidgetPlaceholderProps = {
|
||||
title?: string;
|
||||
onRemove?: () => void;
|
||||
children?: ReactNode;
|
||||
isEmpty?: boolean;
|
||||
};
|
||||
|
||||
export const PageLayoutWidgetPlaceholder = ({
|
||||
title = 'Graph Title',
|
||||
onRemove,
|
||||
children,
|
||||
isEmpty = false,
|
||||
}: PageLayoutWidgetPlaceholderProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
if (isEmpty) {
|
||||
return (
|
||||
<StyledPlaceholderContainer $isEmpty={true}>
|
||||
<StyledHeader>
|
||||
<StyledDragHandle className="drag-handle">
|
||||
<IconGripVertical size={16} color={theme.border.color.light} />
|
||||
</StyledDragHandle>
|
||||
<StyledTitle>Add Widget</StyledTitle>
|
||||
</StyledHeader>
|
||||
<StyledContent>
|
||||
<StyledEmptyState>
|
||||
<StyledEmptyText>Click to add a widget</StyledEmptyText>
|
||||
</StyledEmptyState>
|
||||
</StyledContent>
|
||||
</StyledPlaceholderContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledPlaceholderContainer>
|
||||
<StyledHeader>
|
||||
<StyledDragHandle className="drag-handle">
|
||||
<IconGripVertical size={16} color={theme.border.color.strong} />
|
||||
</StyledDragHandle>
|
||||
<StyledTitle>{title}</StyledTitle>
|
||||
{onRemove && (
|
||||
<StyledCloseButton onClick={onRemove}>
|
||||
<IconX size={16} />
|
||||
</StyledCloseButton>
|
||||
)}
|
||||
</StyledHeader>
|
||||
<StyledContent>{children}</StyledContent>
|
||||
</StyledPlaceholderContainer>
|
||||
);
|
||||
};
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
import { GraphWidgetGaugeChart } from '@/dashboards/graphs/components/GraphWidgetGaugeChart';
|
||||
import { GraphWidgetNumberChart } from '@/dashboards/graphs/components/GraphWidgetNumberChart';
|
||||
import { GraphWidgetPieChart } from '@/dashboards/graphs/components/GraphWidgetPieChart';
|
||||
import { type Meta, type StoryObj } from '@storybook/react';
|
||||
import { ComponentDecorator } from 'twenty-ui/testing';
|
||||
import { PageLayoutWidgetPlaceholder } from '../PageLayoutWidgetPlaceholder';
|
||||
|
||||
const meta: Meta<typeof PageLayoutWidgetPlaceholder> = {
|
||||
title: 'Pages/Settings/PageLayout/PageLayoutWidgetPlaceholder',
|
||||
component: PageLayoutWidgetPlaceholder,
|
||||
decorators: [ComponentDecorator],
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
argTypes: {
|
||||
title: {
|
||||
control: 'text',
|
||||
description: 'Widget title',
|
||||
},
|
||||
isEmpty: {
|
||||
control: 'boolean',
|
||||
description: 'Show empty state',
|
||||
},
|
||||
onRemove: {
|
||||
action: 'onRemove',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof PageLayoutWidgetPlaceholder>;
|
||||
|
||||
export const Empty: Story = {
|
||||
args: {
|
||||
isEmpty: true,
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Empty widget placeholder state with dashed border',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const WithNumberChart: Story = {
|
||||
args: {
|
||||
title: 'Sales Pipeline',
|
||||
children: <GraphWidgetNumberChart value="1,234" trendPercentage={12.5} />,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithGaugeChart: Story = {
|
||||
args: {
|
||||
title: 'Conversion Rate',
|
||||
children: (
|
||||
<GraphWidgetGaugeChart
|
||||
data={{
|
||||
value: 0.5,
|
||||
min: 0,
|
||||
max: 1,
|
||||
label: 'Conversion rate',
|
||||
}}
|
||||
displayType="percentage"
|
||||
showValue={true}
|
||||
id="gauge-chart-story"
|
||||
/>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export const EmptyState: Story = {
|
||||
args: {
|
||||
isEmpty: true,
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Empty widget placeholder state with dashed border',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const WithPieChart: Story = {
|
||||
args: {
|
||||
title: 'Lead Distribution',
|
||||
children: (
|
||||
<GraphWidgetPieChart
|
||||
data={[
|
||||
{
|
||||
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',
|
||||
},
|
||||
]}
|
||||
showLegend={true}
|
||||
displayType="percentage"
|
||||
id="pie-chart-story"
|
||||
/>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export const WithCloseButton: Story = {
|
||||
args: {
|
||||
title: 'Removable Widget',
|
||||
onRemove: () => {},
|
||||
children: <GraphWidgetNumberChart value="42" trendPercentage={-5.2} />,
|
||||
},
|
||||
};
|
||||
|
||||
export const NoContent: Story = {
|
||||
args: {
|
||||
title: 'Empty Content Widget',
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Widget with title but no content',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const LongTitle: Story = {
|
||||
args: {
|
||||
title: 'This is a very long widget title that should be handled gracefully',
|
||||
children: <GraphWidgetNumberChart value="999" trendPercentage={0} />,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,144 @@
|
||||
import { type Layouts } from 'react-grid-layout';
|
||||
|
||||
export type WidgetType = 'number' | 'gauge' | 'pie' | 'view' | 'iframe';
|
||||
|
||||
export type Widget = {
|
||||
id: string;
|
||||
type: WidgetType;
|
||||
title: string;
|
||||
data?: any;
|
||||
};
|
||||
|
||||
export const mockWidgets: Widget[] = [
|
||||
{
|
||||
id: 'widget-1',
|
||||
type: 'number',
|
||||
title: 'Sales Pipeline',
|
||||
data: {
|
||||
value: '1,234',
|
||||
trendPercentage: 12.5,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'widget-2',
|
||||
type: 'gauge',
|
||||
title: 'Conversion Rate',
|
||||
data: {
|
||||
value: 0.5,
|
||||
min: 0,
|
||||
max: 1,
|
||||
label: 'Conversion rate',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'widget-3',
|
||||
type: 'pie',
|
||||
title: 'Lead Distribution',
|
||||
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',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const mockLayouts: Layouts = {
|
||||
lg: [
|
||||
{
|
||||
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,
|
||||
},
|
||||
],
|
||||
md: [
|
||||
{
|
||||
i: 'widget-1',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 3,
|
||||
h: 2,
|
||||
},
|
||||
{
|
||||
i: 'widget-2',
|
||||
x: 3,
|
||||
y: 0,
|
||||
w: 3,
|
||||
h: 5,
|
||||
},
|
||||
{
|
||||
i: 'widget-3',
|
||||
x: 6,
|
||||
y: 0,
|
||||
w: 6,
|
||||
h: 5,
|
||||
},
|
||||
],
|
||||
sm: [
|
||||
{
|
||||
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,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import { type Layouts } from 'react-grid-layout';
|
||||
import { atom } from 'recoil';
|
||||
import { type Widget } from '../mocks/mockWidgets';
|
||||
|
||||
export const pageLayoutWidgetsState = atom<Widget[]>({
|
||||
key: 'pageLayoutWidgetsState',
|
||||
default: [],
|
||||
});
|
||||
|
||||
export const pageLayoutLayoutsState = atom<Layouts>({
|
||||
key: 'pageLayoutLayoutsState',
|
||||
default: {
|
||||
lg: [],
|
||||
md: [],
|
||||
sm: [],
|
||||
},
|
||||
});
|
||||
@@ -20631,6 +20631,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/react-grid-layout@npm:^1":
|
||||
version: 1.3.5
|
||||
resolution: "@types/react-grid-layout@npm:1.3.5"
|
||||
dependencies:
|
||||
"@types/react": "npm:*"
|
||||
checksum: 10c0/abd2a1dda9625c753ff2571a10b69740b2fb9ed1d3141755d54d5814cc12a9701c7c5cd78e8797e945486b441303b82543be71043a32d6a988b57a14237f93c6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/react@npm:*, @types/react@npm:>=16, @types/react@npm:^19, @types/react@npm:^19.0.8":
|
||||
version: 19.1.0
|
||||
resolution: "@types/react@npm:19.1.0"
|
||||
@@ -31538,6 +31547,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fast-equals@npm:^4.0.3":
|
||||
version: 4.0.3
|
||||
resolution: "fast-equals@npm:4.0.3"
|
||||
checksum: 10c0/87fd2609c945ee61e9ed4d041eb2a8f92723fc02884115f67e429dd858d880279e962334894f116b3e9b223f387d246e3db5424ae779287849015ddadbf5ff27
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fast-fifo@npm:^1.2.0, fast-fifo@npm:^1.3.2":
|
||||
version: 1.3.2
|
||||
resolution: "fast-fifo@npm:1.3.2"
|
||||
@@ -44805,7 +44821,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"prop-types@npm:^15.6.1, prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1":
|
||||
"prop-types@npm:15.x, prop-types@npm:^15.6.1, prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1":
|
||||
version: 15.8.1
|
||||
resolution: "prop-types@npm:15.8.1"
|
||||
dependencies:
|
||||
@@ -45561,6 +45577,19 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-draggable@npm:^4.0.3, react-draggable@npm:^4.4.6":
|
||||
version: 4.5.0
|
||||
resolution: "react-draggable@npm:4.5.0"
|
||||
dependencies:
|
||||
clsx: "npm:^2.1.1"
|
||||
prop-types: "npm:^15.8.1"
|
||||
peerDependencies:
|
||||
react: ">= 16.3.0"
|
||||
react-dom: ">= 16.3.0"
|
||||
checksum: 10c0/6f7591fe450555218bf0d9e31984be02451bf3f678fb121f51ac0a0a645d01a1b5ea8248ef9afddcd24239028911fd88032194b9c00b30ad5ece76ea13397fc3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-dropzone@npm:^14.2.3":
|
||||
version: 14.2.3
|
||||
resolution: "react-dropzone@npm:14.2.3"
|
||||
@@ -45616,6 +45645,23 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-grid-layout@npm:^1.5.2":
|
||||
version: 1.5.2
|
||||
resolution: "react-grid-layout@npm:1.5.2"
|
||||
dependencies:
|
||||
clsx: "npm:^2.1.1"
|
||||
fast-equals: "npm:^4.0.3"
|
||||
prop-types: "npm:^15.8.1"
|
||||
react-draggable: "npm:^4.4.6"
|
||||
react-resizable: "npm:^3.0.5"
|
||||
resize-observer-polyfill: "npm:^1.5.1"
|
||||
peerDependencies:
|
||||
react: ">= 16.3.0"
|
||||
react-dom: ">= 16.3.0"
|
||||
checksum: 10c0/b6605d1435fe116c3720d168100a5a08da924c6905686fe8a486c33b82abbde8ccacbb59e5c6243fa52f5e808ad393a7bdf0c09a3446ebf76efe43f29d9f13ee
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-helmet-async@npm:^1.3.0":
|
||||
version: 1.3.0
|
||||
resolution: "react-helmet-async@npm:1.3.0"
|
||||
@@ -45930,6 +45976,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-resizable@npm:^3.0.5":
|
||||
version: 3.0.5
|
||||
resolution: "react-resizable@npm:3.0.5"
|
||||
dependencies:
|
||||
prop-types: "npm:15.x"
|
||||
react-draggable: "npm:^4.0.3"
|
||||
peerDependencies:
|
||||
react: ">= 16.3"
|
||||
checksum: 10c0/cfe50aa6efb79e0aa09bd681a5beab2fcd1186737c4952eb4c3974ed9395d5d263ccd1130961d06b8f5e24c8f544dd2967b5c740ce68719962d1771de7bdb350
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-responsive@npm:^9.0.2":
|
||||
version: 9.0.2
|
||||
resolution: "react-responsive@npm:9.0.2"
|
||||
@@ -46955,6 +47013,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"resize-observer-polyfill@npm:^1.5.1":
|
||||
version: 1.5.1
|
||||
resolution: "resize-observer-polyfill@npm:1.5.1"
|
||||
checksum: 10c0/5e882475067f0b97dc07e0f37c3e335ac5bc3520d463f777cec7e894bb273eddbfecb857ae668e6fb6881fd6f6bb7148246967172139302da50fa12ea3a15d95
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"resolve-alpn@npm:^1.0.0":
|
||||
version: 1.2.1
|
||||
resolution: "resolve-alpn@npm:1.2.1"
|
||||
@@ -50899,6 +50964,7 @@ __metadata:
|
||||
"@types/apollo-upload-client": "npm:^17.0.2"
|
||||
"@types/file-saver": "npm:^2.0.7"
|
||||
"@types/js-cookie": "npm:^3.0.3"
|
||||
"@types/react-grid-layout": "npm:^1"
|
||||
"@typescript-eslint/eslint-plugin": "npm:^8.39.0"
|
||||
"@typescript-eslint/parser": "npm:^8.39.0"
|
||||
"@typescript-eslint/utils": "npm:^8.39.0"
|
||||
@@ -50936,6 +51002,7 @@ __metadata:
|
||||
react-datepicker: "npm:^6.7.1"
|
||||
react-dropzone: "npm:^14.2.3"
|
||||
react-error-boundary: "npm:^4.0.11"
|
||||
react-grid-layout: "npm:^1.5.2"
|
||||
react-helmet-async: "npm:^1.3.0"
|
||||
react-hook-form: "npm:^7.45.1"
|
||||
react-hotkeys-hook: "npm:^4.4.4"
|
||||
|
||||
Reference in New Issue
Block a user