Page Layout Initialization on Settings (#14156)

This commit is contained in:
nitin
2025-08-29 20:50:23 +02:00
committed by GitHub
parent a132a0e88b
commit 74eeb98293
15 changed files with 768 additions and 9 deletions
+2
View File
@@ -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
@@ -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 && (
@@ -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%;
`;
@@ -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>
);
};
@@ -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>
);
};
@@ -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: [],
},
});
+68 -1
View File
@@ -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"