Datamodel overview improvements (#5771)
closes #5586 <img width="707" alt="Bildschirmfoto 2024-06-07 um 14 05 39" src="https://github.com/twentyhq/twenty/assets/48770548/af5fa200-d71b-41ed-9478-35becfc306a3">
This commit is contained in:
@ -1,24 +1,32 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import ReactFlow, {
|
import ReactFlow, {
|
||||||
applyEdgeChanges,
|
applyEdgeChanges,
|
||||||
applyNodeChanges,
|
applyNodeChanges,
|
||||||
Background,
|
Background,
|
||||||
Controls,
|
|
||||||
EdgeChange,
|
EdgeChange,
|
||||||
getIncomers,
|
getIncomers,
|
||||||
getOutgoers,
|
getOutgoers,
|
||||||
NodeChange,
|
NodeChange,
|
||||||
useEdgesState,
|
useEdgesState,
|
||||||
useNodesState,
|
useNodesState,
|
||||||
|
useReactFlow,
|
||||||
} from 'reactflow';
|
} from 'reactflow';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { IconX } from 'twenty-ui';
|
import {
|
||||||
|
IconLock,
|
||||||
|
IconLockOpen,
|
||||||
|
IconMaximize,
|
||||||
|
IconMinus,
|
||||||
|
IconPlus,
|
||||||
|
IconX,
|
||||||
|
} from 'twenty-ui';
|
||||||
|
|
||||||
import { SettingsDataModelOverviewEffect } from '@/settings/data-model/graph-overview/components/SettingsDataModelOverviewEffect';
|
import { SettingsDataModelOverviewEffect } from '@/settings/data-model/graph-overview/components/SettingsDataModelOverviewEffect';
|
||||||
import { SettingsDataModelOverviewObject } from '@/settings/data-model/graph-overview/components/SettingsDataModelOverviewObject';
|
import { SettingsDataModelOverviewObject } from '@/settings/data-model/graph-overview/components/SettingsDataModelOverviewObject';
|
||||||
import { SettingsDataModelOverviewRelationMarkers } from '@/settings/data-model/graph-overview/components/SettingsDataModelOverviewRelationMarkers';
|
import { SettingsDataModelOverviewRelationMarkers } from '@/settings/data-model/graph-overview/components/SettingsDataModelOverviewRelationMarkers';
|
||||||
import { calculateHandlePosition } from '@/settings/data-model/graph-overview/util/calculateHandlePosition';
|
import { calculateHandlePosition } from '@/settings/data-model/graph-overview/util/calculateHandlePosition';
|
||||||
import { Button } from '@/ui/input/button/components/Button';
|
import { Button } from '@/ui/input/button/components/Button';
|
||||||
|
import { IconButtonGroup } from '@/ui/input/button/components/IconButtonGroup';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
import 'reactflow/dist/style.css';
|
import 'reactflow/dist/style.css';
|
||||||
@ -28,34 +36,6 @@ const NodeTypes = {
|
|||||||
};
|
};
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
height: 100%;
|
height: 100%;
|
||||||
.has-many-edge {
|
|
||||||
&.selected path.react-flow__edge-path {
|
|
||||||
marker-end: url(#hasManySelected);
|
|
||||||
stroke-width: 1.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.has-many-edge--highlighted {
|
|
||||||
path.react-flow__edge-path,
|
|
||||||
path.react-flow__edge-interaction,
|
|
||||||
path.react-flow__connection-path {
|
|
||||||
stroke: ${({ theme }) => theme.tag.background.blue} !important;
|
|
||||||
stroke-width: 1.5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.has-many-edge-reversed {
|
|
||||||
&.selected path.react-flow__edge-path {
|
|
||||||
marker-end: url(#hasManyReversedSelected);
|
|
||||||
stroke-width: 1.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.has-many-edge-reversed--highlighted {
|
|
||||||
path.react-flow__edge-path,
|
|
||||||
path.react-flow__edge-interaction,
|
|
||||||
path.react-flow__connection-path {
|
|
||||||
stroke: ${({ theme }) => theme.tag.background.blue} !important;
|
|
||||||
stroke-width: 1.5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.react-flow__handle {
|
.react-flow__handle {
|
||||||
border: 0 !important;
|
border: 0 !important;
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
@ -75,27 +55,6 @@ const StyledContainer = styled.div`
|
|||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateX(50%) translateY(-50%);
|
transform: translateX(50%) translateY(-50%);
|
||||||
}
|
}
|
||||||
.top-handle {
|
|
||||||
top: 0;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%) translateY(-50%);
|
|
||||||
}
|
|
||||||
.bottom-handle {
|
|
||||||
bottom: 0;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%) translateY(50%);
|
|
||||||
}
|
|
||||||
.react-flow__panel {
|
|
||||||
display: flex;
|
|
||||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
|
||||||
box-shadow: unset;
|
|
||||||
|
|
||||||
button {
|
|
||||||
background: ${({ theme }) => theme.background.secondary};
|
|
||||||
border-bottom: none;
|
|
||||||
fill: ${({ theme }) => theme.font.color.secondary};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.react-flow__node {
|
.react-flow__node {
|
||||||
z-index: -1 !important;
|
z-index: -1 !important;
|
||||||
}
|
}
|
||||||
@ -109,8 +68,11 @@ const StyledCloseButton = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const SettingsDataModelOverview = () => {
|
export const SettingsDataModelOverview = () => {
|
||||||
|
const { fitView, zoomIn, zoomOut } = useReactFlow();
|
||||||
|
|
||||||
const [nodes, setNodes] = useNodesState([]);
|
const [nodes, setNodes] = useNodesState([]);
|
||||||
const [edges, setEdges] = useEdgesState([]);
|
const [edges, setEdges] = useEdgesState([]);
|
||||||
|
const [isInteractive, setInteractive] = useState(true);
|
||||||
|
|
||||||
const onNodesChange = useCallback(
|
const onNodesChange = useCallback(
|
||||||
(changes: NodeChange[]) =>
|
(changes: NodeChange[]) =>
|
||||||
@ -236,10 +198,34 @@ export const SettingsDataModelOverview = () => {
|
|||||||
onEdgesChange={onEdgesChange}
|
onEdgesChange={onEdgesChange}
|
||||||
nodeTypes={NodeTypes}
|
nodeTypes={NodeTypes}
|
||||||
onNodesChange={handleNodesChange}
|
onNodesChange={handleNodesChange}
|
||||||
|
nodesDraggable={isInteractive}
|
||||||
|
elementsSelectable={isInteractive}
|
||||||
proOptions={{ hideAttribution: true }}
|
proOptions={{ hideAttribution: true }}
|
||||||
>
|
>
|
||||||
<Background />
|
<Background />
|
||||||
<Controls />
|
|
||||||
|
<IconButtonGroup
|
||||||
|
className="react-flow__panel react-flow__controls bottom left"
|
||||||
|
size="small"
|
||||||
|
iconButtons={[
|
||||||
|
{
|
||||||
|
Icon: IconPlus,
|
||||||
|
onClick: () => zoomIn(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Icon: IconMinus,
|
||||||
|
onClick: () => zoomOut(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Icon: IconMaximize,
|
||||||
|
onClick: () => fitView(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Icon: isInteractive ? IconLockOpen : IconLock,
|
||||||
|
onClick: () => setInteractive(!isInteractive),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
></IconButtonGroup>
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -15,12 +15,16 @@ type ObjectFieldRowProps = {
|
|||||||
const StyledRow = styled.div`
|
const StyledRow = styled.div`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: ${({ theme }) => theme.spacing(1)};
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0 ${({ theme }) => theme.spacing(2)};
|
padding: 0 ${({ theme }) => theme.spacing(2)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const StyledFieldName = styled.div`
|
||||||
|
color: ${({ theme }) => theme.font.color.primary};
|
||||||
|
`;
|
||||||
|
|
||||||
export const ObjectFieldRow = ({ field }: ObjectFieldRowProps) => {
|
export const ObjectFieldRow = ({ field }: ObjectFieldRowProps) => {
|
||||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||||
const { getIcon } = useIcons();
|
const { getIcon } = useIcons();
|
||||||
@ -37,7 +41,9 @@ export const ObjectFieldRow = ({ field }: ObjectFieldRowProps) => {
|
|||||||
return (
|
return (
|
||||||
<StyledRow>
|
<StyledRow>
|
||||||
{Icon && <Icon size={theme.icon.size.md} />}
|
{Icon && <Icon size={theme.icon.size.md} />}
|
||||||
{capitalize(relatedObject?.namePlural ?? '')}
|
<StyledFieldName>
|
||||||
|
{capitalize(relatedObject?.namePlural ?? '')}
|
||||||
|
</StyledFieldName>
|
||||||
<Handle
|
<Handle
|
||||||
type={field.toRelationMetadata ? 'source' : 'target'}
|
type={field.toRelationMetadata ? 'source' : 'target'}
|
||||||
position={Position.Right}
|
position={Position.Right}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { Link } from 'react-router-dom';
|
|||||||
import { NodeProps } from 'reactflow';
|
import { NodeProps } from 'reactflow';
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { IconTag, useIcons } from 'twenty-ui';
|
import { IconChevronDown, useIcons } from 'twenty-ui';
|
||||||
|
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||||
@ -18,13 +18,14 @@ type SettingsDataModelOverviewObjectProps = NodeProps<ObjectMetadataItem>;
|
|||||||
|
|
||||||
const StyledNode = styled.div`
|
const StyledNode = styled.div`
|
||||||
background-color: ${({ theme }) => theme.background.secondary};
|
background-color: ${({ theme }) => theme.background.secondary};
|
||||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 220px;
|
width: 220px;
|
||||||
padding: ${({ theme }) => theme.spacing(2)};
|
padding: ${({ theme }) => theme.spacing(2)};
|
||||||
gap: ${({ theme }) => theme.spacing(2)};
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||||
|
box-shadow: ${({ theme }) => theme.boxShadow.light};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledHeader = styled.div`
|
const StyledHeader = styled.div`
|
||||||
@ -37,7 +38,7 @@ const StyledObjectName = styled.div`
|
|||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 4px 4px 0 0;
|
border-radius: 4px 4px 0 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
font-weight: bold;
|
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||||
gap: ${({ theme }) => theme.spacing(1)};
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
position: relative;
|
position: relative;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@ -67,7 +68,7 @@ const StyledCardRowOther = styled.div`
|
|||||||
display: flex;
|
display: flex;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
padding: 0 ${({ theme }) => theme.spacing(2)};
|
padding: 0 ${({ theme }) => theme.spacing(2)};
|
||||||
gap: ${({ theme }) => theme.spacing(1)};
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledCardRowText = styled.div``;
|
const StyledCardRowText = styled.div``;
|
||||||
@ -79,6 +80,7 @@ const StyledObjectInstanceCount = styled.div`
|
|||||||
const StyledObjectLink = styled(Link)`
|
const StyledObjectLink = styled(Link)`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: ${({ theme }) => theme.font.color.primary};
|
color: ${({ theme }) => theme.font.color.primary};
|
||||||
|
|
||||||
@ -130,10 +132,8 @@ export const SettingsDataModelOverviewObject = ({
|
|||||||
))}
|
))}
|
||||||
{countNonRelation > 0 && (
|
{countNonRelation > 0 && (
|
||||||
<StyledCardRowOther>
|
<StyledCardRowOther>
|
||||||
<IconTag size={theme.icon.size.md} />
|
<IconChevronDown size={theme.icon.size.md} />
|
||||||
<StyledCardRowText>
|
<StyledCardRowText>{countNonRelation} fields</StyledCardRowText>
|
||||||
{countNonRelation} other fields
|
|
||||||
</StyledCardRowText>
|
|
||||||
</StyledCardRowOther>
|
</StyledCardRowOther>
|
||||||
)}
|
)}
|
||||||
</StyledInnerCard>
|
</StyledInnerCard>
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { ReactFlowProvider } from 'reactflow';
|
||||||
import { IconSettings } from 'twenty-ui';
|
import { IconSettings } from 'twenty-ui';
|
||||||
|
|
||||||
import { SettingsDataModelOverview } from '@/settings/data-model/graph-overview/components/SettingsDataModelOverview';
|
import { SettingsDataModelOverview } from '@/settings/data-model/graph-overview/components/SettingsDataModelOverview';
|
||||||
@ -6,7 +7,9 @@ import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'
|
|||||||
export const SettingsObjectOverview = () => {
|
export const SettingsObjectOverview = () => {
|
||||||
return (
|
return (
|
||||||
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
||||||
<SettingsDataModelOverview />
|
<ReactFlowProvider>
|
||||||
|
<SettingsDataModelOverview />
|
||||||
|
</ReactFlowProvider>
|
||||||
</SubMenuTopBarContainer>
|
</SubMenuTopBarContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -106,9 +106,11 @@ export {
|
|||||||
IconList,
|
IconList,
|
||||||
IconListNumbers,
|
IconListNumbers,
|
||||||
IconLock,
|
IconLock,
|
||||||
|
IconLockOpen,
|
||||||
IconMail,
|
IconMail,
|
||||||
IconMailCog,
|
IconMailCog,
|
||||||
IconMap,
|
IconMap,
|
||||||
|
IconMaximize,
|
||||||
IconMinus,
|
IconMinus,
|
||||||
IconMoneybag,
|
IconMoneybag,
|
||||||
IconMouse2,
|
IconMouse2,
|
||||||
|
|||||||
Reference in New Issue
Block a user