From 74eeb982938430f00055ff340477c0d712c153f4 Mon Sep 17 00:00:00 2001
From: nitin <142569587+ehconitin@users.noreply.github.com>
Date: Sat, 30 Aug 2025 00:20:23 +0530
Subject: [PATCH] Page Layout Initialization on Settings (#14156)
---
packages/twenty-front/package.json | 2 +
.../modules/app/components/SettingsRoutes.tsx | 10 +
.../components/GraphWidgetGaugeChart.tsx | 4 -
.../components/GraphWidgetNumberChart.tsx | 3 +-
.../SettingsPageFullWidthContainer.tsx | 25 +++
.../hooks/useSettingsNavigationItems.tsx | 13 ++
.../src/modules/types/SettingsPath.ts | 1 +
.../layout/page/components/DefaultLayout.tsx | 11 +-
.../ui/layout/page/components/PageBody.tsx | 2 +-
.../page-layout/PageLayoutEdition.tsx | 184 ++++++++++++++++++
.../PageLayoutWidgetPlaceholder.tsx | 136 +++++++++++++
.../PageLayoutWidgetPlaceholder.stories.tsx | 156 +++++++++++++++
.../settings/page-layout/mocks/mockWidgets.ts | 144 ++++++++++++++
.../page-layout/states/pageLayoutState.ts | 17 ++
yarn.lock | 69 ++++++-
15 files changed, 768 insertions(+), 9 deletions(-)
create mode 100644 packages/twenty-front/src/modules/settings/components/SettingsPageFullWidthContainer.tsx
create mode 100644 packages/twenty-front/src/pages/settings/page-layout/PageLayoutEdition.tsx
create mode 100644 packages/twenty-front/src/pages/settings/page-layout/components/PageLayoutWidgetPlaceholder.tsx
create mode 100644 packages/twenty-front/src/pages/settings/page-layout/components/__stories__/PageLayoutWidgetPlaceholder.stories.tsx
create mode 100644 packages/twenty-front/src/pages/settings/page-layout/mocks/mockWidgets.ts
create mode 100644 packages/twenty-front/src/pages/settings/page-layout/states/pageLayoutState.ts
diff --git a/packages/twenty-front/package.json b/packages/twenty-front/package.json
index 15f41373a83..9e5520c1c45 100644
--- a/packages/twenty-front/package.json
+++ b/packages/twenty-front/package.json
@@ -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",
diff --git a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx
index 6ca1a4244b7..e7164270a9a 100644
--- a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx
+++ b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx
@@ -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 = ({
/>
>
)}
+ }
+ />
setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
- radialAxisStart={null}
- radialAxisEnd={null}
- circularAxisInner={null}
- circularAxisOuter={null}
layers={['bars', renderValueEndLine]}
/>
{showValue && (
diff --git a/packages/twenty-front/src/modules/dashboards/graphs/components/GraphWidgetNumberChart.tsx b/packages/twenty-front/src/modules/dashboards/graphs/components/GraphWidgetNumberChart.tsx
index 26a747390fd..d977b5cd5b0 100644
--- a/packages/twenty-front/src/modules/dashboards/graphs/components/GraphWidgetNumberChart.tsx
+++ b/packages/twenty-front/src/modules/dashboards/graphs/components/GraphWidgetNumberChart.tsx
@@ -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%;
`;
diff --git a/packages/twenty-front/src/modules/settings/components/SettingsPageFullWidthContainer.tsx b/packages/twenty-front/src/modules/settings/components/SettingsPageFullWidthContainer.tsx
new file mode 100644
index 00000000000..b1e2a77f2e4
--- /dev/null
+++ b/packages/twenty-front/src/modules/settings/components/SettingsPageFullWidthContainer.tsx
@@ -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 (
+
+ } />
+ {children}
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/settings/hooks/useSettingsNavigationItems.tsx b/packages/twenty-front/src/modules/settings/hooks/useSettingsNavigationItems.tsx
index fc972ee1219..eef4c749699 100644
--- a/packages/twenty-front/src/modules/settings/hooks/useSettingsNavigationItems.tsx
+++ b/packages/twenty-front/src/modules/settings/hooks/useSettingsNavigationItems.tsx
@@ -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,
diff --git a/packages/twenty-front/src/modules/types/SettingsPath.ts b/packages/twenty-front/src/modules/types/SettingsPath.ts
index 13dff0e3de9..07d348716da 100644
--- a/packages/twenty-front/src/modules/types/SettingsPath.ts
+++ b/packages/twenty-front/src/modules/types/SettingsPath.ts
@@ -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',
}
diff --git a/packages/twenty-front/src/modules/ui/layout/page/components/DefaultLayout.tsx b/packages/twenty-front/src/modules/ui/layout/page/components/DefaultLayout.tsx
index f8a436a53dd..5b925805196 100644
--- a/packages/twenty-front/src/modules/ui/layout/page/components/DefaultLayout.tsx
+++ b/packages/twenty-front/src/modules/ui/layout/page/components/DefaultLayout.tsx
@@ -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 = () => {
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;
+
+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 (
+
+
+
+ {Array.from({ length: 12 * 20 }).map((_, i) => (
+
+ ))}
+
+
+ {isEmptyState ? (
+
+ ) : (
+ pageLayoutWidgets.map((widget) => (
+
+
+ {widget.type === 'number' && (
+
+ )}
+ {widget.type === 'gauge' && (
+
+ )}
+ {widget.type === 'pie' && (
+
+ )}
+
+
+ ))
+ )}
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/pages/settings/page-layout/components/PageLayoutWidgetPlaceholder.tsx b/packages/twenty-front/src/pages/settings/page-layout/components/PageLayoutWidgetPlaceholder.tsx
new file mode 100644
index 00000000000..ee2583e6de7
--- /dev/null
+++ b/packages/twenty-front/src/pages/settings/page-layout/components/PageLayoutWidgetPlaceholder.tsx
@@ -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 (
+
+
+
+
+
+ Add Widget
+
+
+
+ Click to add a widget
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ {title}
+ {onRemove && (
+
+
+
+ )}
+
+ {children}
+
+ );
+};
diff --git a/packages/twenty-front/src/pages/settings/page-layout/components/__stories__/PageLayoutWidgetPlaceholder.stories.tsx b/packages/twenty-front/src/pages/settings/page-layout/components/__stories__/PageLayoutWidgetPlaceholder.stories.tsx
new file mode 100644
index 00000000000..027a4c96103
--- /dev/null
+++ b/packages/twenty-front/src/pages/settings/page-layout/components/__stories__/PageLayoutWidgetPlaceholder.stories.tsx
@@ -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 = {
+ 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;
+
+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: ,
+ },
+};
+
+export const WithGaugeChart: Story = {
+ args: {
+ title: 'Conversion Rate',
+ children: (
+
+ ),
+ },
+};
+
+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: (
+
+ ),
+ },
+};
+
+export const WithCloseButton: Story = {
+ args: {
+ title: 'Removable Widget',
+ onRemove: () => {},
+ children: ,
+ },
+};
+
+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: ,
+ },
+};
diff --git a/packages/twenty-front/src/pages/settings/page-layout/mocks/mockWidgets.ts b/packages/twenty-front/src/pages/settings/page-layout/mocks/mockWidgets.ts
new file mode 100644
index 00000000000..17059ebbb7e
--- /dev/null
+++ b/packages/twenty-front/src/pages/settings/page-layout/mocks/mockWidgets.ts
@@ -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,
+ },
+ ],
+};
diff --git a/packages/twenty-front/src/pages/settings/page-layout/states/pageLayoutState.ts b/packages/twenty-front/src/pages/settings/page-layout/states/pageLayoutState.ts
new file mode 100644
index 00000000000..f1ca2290966
--- /dev/null
+++ b/packages/twenty-front/src/pages/settings/page-layout/states/pageLayoutState.ts
@@ -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({
+ key: 'pageLayoutWidgetsState',
+ default: [],
+});
+
+export const pageLayoutLayoutsState = atom({
+ key: 'pageLayoutLayoutsState',
+ default: {
+ lg: [],
+ md: [],
+ sm: [],
+ },
+});
diff --git a/yarn.lock b/yarn.lock
index ccb82e6d22f..8ae6807baec 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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"