mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-18 00:52:21 +03:00
On Company Show, in team section, I can detach a person from a company (#1202)
* On Company Show, in team section, I can detach a person from a company Co-authored-by: v1b3m <vibenjamin6@gmail.com> Co-authored-by: RubensRafael <rubensrafael2@live.com> * On Company Show, in team section, I can detach a person from a company Co-authored-by: v1b3m <vibenjamin6@gmail.com> Co-authored-by: RubensRafael <rubensrafael2@live.com> * Temporary fix disconnect optional relations Co-authored-by: v1b3m <vibenjamin6@gmail.com> Co-authored-by: RubensRafael <rubensrafael2@live.com> * Refactor the PR logic Co-authored-by: v1b3m <vibenjamin6@gmail.com> Co-authored-by: RubensRafael <rubensrafael2@live.com> * Add requested changes Co-authored-by: v1b3m <vibenjamin6@gmail.com> Co-authored-by: RubensRafael <rubensrafael2@live.com> * Refactor the dropdown Co-authored-by: v1b3m <vibenjamin6@gmail.com> Co-authored-by: RubensRafael <rubensrafael2@live.com> --------- Co-authored-by: v1b3m <vibenjamin6@gmail.com> Co-authored-by: RubensRafael <rubensrafael2@live.com>
This commit is contained in:
parent
9bbdf933e9
commit
8f7044207d
@ -1,17 +1,34 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { getOperationName } from '@apollo/client/utilities';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { autoUpdate, flip, offset, useFloating } from '@floating-ui/react';
|
||||||
|
import { IconDotsVertical, IconLinkOff } from '@tabler/icons-react';
|
||||||
|
|
||||||
|
import { IconButton } from '@/ui/button/components/IconButton';
|
||||||
|
import { DropdownMenu } from '@/ui/dropdown/components/DropdownMenu';
|
||||||
|
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer';
|
||||||
|
import { DropdownMenuSelectableItem } from '@/ui/dropdown/components/DropdownMenuSelectableItem';
|
||||||
|
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
import { Avatar } from '@/users/components/Avatar';
|
import { Avatar } from '@/users/components/Avatar';
|
||||||
import { Person } from '~/generated/graphql';
|
import { Person, useUpdateOnePersonMutation } from '~/generated/graphql';
|
||||||
|
|
||||||
|
import { GET_PEOPLE } from '../graphql/queries/getPeople';
|
||||||
|
|
||||||
export type PeopleCardProps = {
|
export type PeopleCardProps = {
|
||||||
person: Pick<Person, 'id' | 'avatarUrl' | 'displayName' | 'jobTitle'>;
|
person: Pick<Person, 'id' | 'avatarUrl' | 'displayName' | 'jobTitle'>;
|
||||||
hasBottomBorder?: boolean;
|
hasBottomBorder?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledCard = styled.div<{ hasBottomBorder: boolean }>`
|
const StyledCard = styled.div<{
|
||||||
|
isHovered: boolean;
|
||||||
|
hasBottomBorder?: boolean;
|
||||||
|
}>`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
|
background: ${({ theme, isHovered }) =>
|
||||||
|
isHovered ? theme.background.tertiary : 'auto'};
|
||||||
border-bottom: 1px solid
|
border-bottom: 1px solid
|
||||||
${({ theme, hasBottomBorder }) =>
|
${({ theme, hasBottomBorder }) =>
|
||||||
hasBottomBorder ? theme.border.color.light : 'transparent'};
|
hasBottomBorder ? theme.border.color.light : 'transparent'};
|
||||||
@ -19,7 +36,6 @@ const StyledCard = styled.div<{ hasBottomBorder: boolean }>`
|
|||||||
gap: ${({ theme }) => theme.spacing(2)};
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
height: ${({ theme }) => theme.spacing(8)};
|
height: ${({ theme }) => theme.spacing(8)};
|
||||||
padding: ${({ theme }) => theme.spacing(3)};
|
padding: ${({ theme }) => theme.spacing(3)};
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: ${({ theme }) => theme.background.tertiary};
|
background: ${({ theme }) => theme.background.tertiary};
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -57,8 +73,64 @@ export function PeopleCard({
|
|||||||
hasBottomBorder = true,
|
hasBottomBorder = true,
|
||||||
}: PeopleCardProps) {
|
}: PeopleCardProps) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
const [isOptionsOpen, setIsOptionsOpen] = useState(false);
|
||||||
|
const [updatePerson] = useUpdateOnePersonMutation();
|
||||||
|
const { refs, floatingStyles } = useFloating({
|
||||||
|
strategy: 'absolute',
|
||||||
|
middleware: [offset(10), flip()],
|
||||||
|
whileElementsMounted: autoUpdate,
|
||||||
|
placement: 'right-start',
|
||||||
|
});
|
||||||
|
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
useListenClickOutside({
|
||||||
|
refs: [refs.floating],
|
||||||
|
callback: () => {
|
||||||
|
setIsOptionsOpen(false);
|
||||||
|
if (isOptionsOpen) {
|
||||||
|
setIsHovered(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleMouseEnter() {
|
||||||
|
setIsHovered(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseLeave() {
|
||||||
|
if (!isOptionsOpen) {
|
||||||
|
setIsHovered(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleToggleOptions(e: React.MouseEvent<HTMLButtonElement>) {
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsOptionsOpen(!isOptionsOpen);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDetachPerson() {
|
||||||
|
updatePerson({
|
||||||
|
variables: {
|
||||||
|
where: {
|
||||||
|
id: person.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
company: {
|
||||||
|
disconnect: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
refetchQueries: [getOperationName(GET_PEOPLE) ?? ''],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledCard
|
<StyledCard
|
||||||
|
isHovered={isHovered}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
onClick={() => navigate(`/person/${person.id}`)}
|
onClick={() => navigate(`/person/${person.id}`)}
|
||||||
hasBottomBorder={hasBottomBorder}
|
hasBottomBorder={hasBottomBorder}
|
||||||
>
|
>
|
||||||
@ -72,6 +144,26 @@ export function PeopleCard({
|
|||||||
<StyledTitle>{person.displayName}</StyledTitle>
|
<StyledTitle>{person.displayName}</StyledTitle>
|
||||||
{person.jobTitle && <StyledJobTitle>{person.jobTitle}</StyledJobTitle>}
|
{person.jobTitle && <StyledJobTitle>{person.jobTitle}</StyledJobTitle>}
|
||||||
</StyledCardInfo>
|
</StyledCardInfo>
|
||||||
|
{isHovered && (
|
||||||
|
<div ref={refs.setReference}>
|
||||||
|
<IconButton
|
||||||
|
onClick={handleToggleOptions}
|
||||||
|
variant="shadow"
|
||||||
|
size="small"
|
||||||
|
icon={<IconDotsVertical size={theme.icon.size.md} />}
|
||||||
|
/>
|
||||||
|
{isOptionsOpen && (
|
||||||
|
<DropdownMenu ref={refs.setFloating} style={floatingStyles}>
|
||||||
|
<DropdownMenuItemsContainer onClick={(e) => e.stopPropagation()}>
|
||||||
|
<DropdownMenuSelectableItem onClick={handleDetachPerson}>
|
||||||
|
<IconButton icon={<IconLinkOff size={14} />} size="small" />
|
||||||
|
Detach relation
|
||||||
|
</DropdownMenuSelectableItem>
|
||||||
|
</DropdownMenuItemsContainer>
|
||||||
|
</DropdownMenu>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</StyledCard>
|
</StyledCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -59,6 +59,19 @@ const simpleAbilityCheck: OperationAbilityChecker = async (
|
|||||||
prisma,
|
prisma,
|
||||||
data,
|
data,
|
||||||
) => {
|
) => {
|
||||||
|
// TODO: Replace user by workspaceMember and remove this check
|
||||||
|
if (
|
||||||
|
modelName === 'User' ||
|
||||||
|
modelName === 'UserSettings' ||
|
||||||
|
modelName === 'Workspace'
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof data === 'boolean') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Extract entity name from model name
|
// Extract entity name from model name
|
||||||
const entity = camelCase(modelName);
|
const entity = camelCase(modelName);
|
||||||
// Handle all operations cases
|
// Handle all operations cases
|
||||||
@ -76,15 +89,6 @@ const simpleAbilityCheck: OperationAbilityChecker = async (
|
|||||||
|
|
||||||
// Check if user try to connect an element that is not allowed to read
|
// Check if user try to connect an element that is not allowed to read
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
// TODO: Replace user by workspaceMember and remove this check
|
|
||||||
if (
|
|
||||||
modelName === 'User' ||
|
|
||||||
modelName === 'UserSettings' ||
|
|
||||||
modelName === 'Workspace'
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ability.can(AbilityAction.Read, subject(modelName, item))) {
|
if (!ability.can(AbilityAction.Read, subject(modelName, item))) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user