Implemented comment thread target picker with new dropdown components (#295)
* First draft of new relation picker and usage in comments --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -7,6 +7,7 @@
|
|||||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
"@emotion/react": "^11.10.6",
|
"@emotion/react": "^11.10.6",
|
||||||
"@emotion/styled": "^11.10.5",
|
"@emotion/styled": "^11.10.5",
|
||||||
|
"@floating-ui/react": "^0.24.3",
|
||||||
"@hello-pangea/dnd": "^16.2.0",
|
"@hello-pangea/dnd": "^16.2.0",
|
||||||
"@tabler/icons-react": "^2.20.0",
|
"@tabler/icons-react": "^2.20.0",
|
||||||
"@tanstack/react-table": "^8.8.5",
|
"@tanstack/react-table": "^8.8.5",
|
||||||
|
|||||||
@ -1151,14 +1151,14 @@ export type GetCommentThreadsByTargetsQueryVariables = Exact<{
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type GetCommentThreadsByTargetsQuery = { __typename?: 'Query', findManyCommentThreads: Array<{ __typename?: 'CommentThread', id: string, comments?: Array<{ __typename?: 'Comment', id: string, body: string, createdAt: string, updatedAt: string, author: { __typename?: 'User', id: string, displayName: string, avatarUrl?: string | null } }> | null }> };
|
export type GetCommentThreadsByTargetsQuery = { __typename?: 'Query', findManyCommentThreads: Array<{ __typename?: 'CommentThread', id: string, comments?: Array<{ __typename?: 'Comment', id: string, body: string, createdAt: string, updatedAt: string, author: { __typename?: 'User', id: string, displayName: string, avatarUrl?: string | null } }> | null, commentThreadTargets?: Array<{ __typename?: 'CommentThreadTarget', commentableId: string, commentableType: CommentableType }> | null }> };
|
||||||
|
|
||||||
export type GetCommentThreadQueryVariables = Exact<{
|
export type GetCommentThreadQueryVariables = Exact<{
|
||||||
commentThreadId: Scalars['String'];
|
commentThreadId: Scalars['String'];
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type GetCommentThreadQuery = { __typename?: 'Query', findManyCommentThreads: Array<{ __typename?: 'CommentThread', id: string, comments?: Array<{ __typename?: 'Comment', id: string, body: string, createdAt: string, updatedAt: string, author: { __typename?: 'User', id: string, displayName: string, avatarUrl?: string | null } }> | null }> };
|
export type GetCommentThreadQuery = { __typename?: 'Query', findManyCommentThreads: Array<{ __typename?: 'CommentThread', id: string, comments?: Array<{ __typename?: 'Comment', id: string, body: string, createdAt: string, updatedAt: string, author: { __typename?: 'User', id: string, displayName: string, avatarUrl?: string | null } }> | null, commentThreadTargets?: Array<{ __typename?: 'CommentThreadTarget', commentableId: string, commentableType: CommentableType }> | null }> };
|
||||||
|
|
||||||
export type GetCompaniesQueryVariables = Exact<{
|
export type GetCompaniesQueryVariables = Exact<{
|
||||||
orderBy?: InputMaybe<Array<CompanyOrderByWithRelationInput> | CompanyOrderByWithRelationInput>;
|
orderBy?: InputMaybe<Array<CompanyOrderByWithRelationInput> | CompanyOrderByWithRelationInput>;
|
||||||
@ -1414,6 +1414,10 @@ export const GetCommentThreadsByTargetsDocument = gql`
|
|||||||
avatarUrl
|
avatarUrl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
commentThreadTargets {
|
||||||
|
commentableId
|
||||||
|
commentableType
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@ -1461,6 +1465,10 @@ export const GetCommentThreadDocument = gql`
|
|||||||
avatarUrl
|
avatarUrl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
commentThreadTargets {
|
||||||
|
commentableId
|
||||||
|
commentableType
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -80,7 +80,7 @@ export function CommentHeader({ comment }: OwnProps) {
|
|||||||
<Avatar
|
<Avatar
|
||||||
avatarUrl={avatarUrl}
|
avatarUrl={avatarUrl}
|
||||||
size={16}
|
size={16}
|
||||||
placeholderLetter={capitalizedFirstUsernameLetter}
|
placeholder={capitalizedFirstUsernameLetter}
|
||||||
/>
|
/>
|
||||||
<StyledName>{authorName}</StyledName>
|
<StyledName>{authorName}</StyledName>
|
||||||
{showDate && (
|
{showDate && (
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import { isNonEmptyString } from '@/utils/type-guards/isNonEmptyString';
|
|||||||
import { useCreateCommentMutation } from '~/generated/graphql';
|
import { useCreateCommentMutation } from '~/generated/graphql';
|
||||||
|
|
||||||
import { CommentThreadItem } from './CommentThreadItem';
|
import { CommentThreadItem } from './CommentThreadItem';
|
||||||
|
import { CommentThreadRelationPicker } from './CommentThreadRelationPicker';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
commentThread: CommentThreadForDrawer;
|
commentThread: CommentThreadForDrawer;
|
||||||
@ -88,6 +89,7 @@ export function CommentThread({ commentThread }: OwnProps) {
|
|||||||
<CommentThreadItem key={comment.id} comment={comment} />
|
<CommentThreadItem key={comment.id} comment={comment} />
|
||||||
))}
|
))}
|
||||||
</StyledThreadItemListContainer>
|
</StyledThreadItemListContainer>
|
||||||
|
<CommentThreadRelationPicker commentThread={commentThread} />
|
||||||
<AutosizeTextInput onValidate={handleSendComment} />
|
<AutosizeTextInput onValidate={handleSendComment} />
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -0,0 +1,206 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import {
|
||||||
|
autoUpdate,
|
||||||
|
flip,
|
||||||
|
offset,
|
||||||
|
shift,
|
||||||
|
size,
|
||||||
|
useFloating,
|
||||||
|
} from '@floating-ui/react';
|
||||||
|
import { debounce } from 'lodash';
|
||||||
|
|
||||||
|
import { CommentThreadForDrawer } from '@/comments/types/CommentThreadForDrawer';
|
||||||
|
import CompanyChip from '@/companies/components/CompanyChip';
|
||||||
|
import { DropdownMenu } from '@/ui/components/menu/DropdownMenu';
|
||||||
|
import { DropdownMenuCheckableItem } from '@/ui/components/menu/DropdownMenuCheckableItem';
|
||||||
|
import { DropdownMenuItem } from '@/ui/components/menu/DropdownMenuItem';
|
||||||
|
import { DropdownMenuItemContainer } from '@/ui/components/menu/DropdownMenuItemContainer';
|
||||||
|
import { DropdownMenuSearch } from '@/ui/components/menu/DropdownMenuSearch';
|
||||||
|
import { DropdownMenuSeparator } from '@/ui/components/menu/DropdownMenuSeparator';
|
||||||
|
import { IconArrowUpRight } from '@/ui/icons';
|
||||||
|
import { Avatar } from '@/users/components/Avatar';
|
||||||
|
import { getLogoUrlFromDomainName } from '@/utils/utils';
|
||||||
|
import { QueryMode, useSearchCompanyQueryQuery } from '~/generated/graphql';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
commentThread: CommentThreadForDrawer;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledContainer = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: ${(props) => props.theme.spacing(2)};
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledRelationLabel = styled.div`
|
||||||
|
color: ${(props) => props.theme.text60};
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledRelationContainer = styled.div`
|
||||||
|
--horizontal-padding: ${(props) => props.theme.spacing(1)};
|
||||||
|
--vertical-padding: ${(props) => props.theme.spacing(1.5)};
|
||||||
|
|
||||||
|
border: 1px solid transparent;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
gap: ${(props) => props.theme.spacing(2)};
|
||||||
|
|
||||||
|
height: calc(32px - 2 * var(--vertical-padding));
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: ${(props) => props.theme.secondaryBackground};
|
||||||
|
border: 1px solid ${(props) => props.theme.lightBorder};
|
||||||
|
}
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
padding: var(--vertical-padding) var(--horizontal-padding);
|
||||||
|
|
||||||
|
width: calc(100% - 2 * var(--horizontal-padding));
|
||||||
|
`;
|
||||||
|
|
||||||
|
// TODO: refactor icon button with new figma and merge
|
||||||
|
// const StyledAddButton = styled.div`
|
||||||
|
// align-items: center;
|
||||||
|
// background: ${(props) => props.theme.primaryBackgroundTransparent};
|
||||||
|
// border-radius: ${(props) => props.theme.borderRadius};
|
||||||
|
// box-shadow: ${(props) => props.theme.modalBoxShadow};
|
||||||
|
|
||||||
|
// cursor: pointer;
|
||||||
|
// display: flex;
|
||||||
|
// flex-direction: row;
|
||||||
|
|
||||||
|
// &:hover {
|
||||||
|
// background-color: ${(props) => props.theme.tertiaryBackground};
|
||||||
|
// }
|
||||||
|
|
||||||
|
// height: 20px;
|
||||||
|
// justify-content: center;
|
||||||
|
|
||||||
|
// width: 20px;
|
||||||
|
// `;
|
||||||
|
|
||||||
|
export function CommentThreadRelationPicker({ commentThread }: OwnProps) {
|
||||||
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||||
|
const [searchFilter, setSearchFilter] = useState('');
|
||||||
|
|
||||||
|
const debouncedSetSearchFilter = debounce(setSearchFilter, 100, {
|
||||||
|
leading: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { refs, floatingStyles } = useFloating({
|
||||||
|
strategy: 'fixed',
|
||||||
|
middleware: [offset(), flip(), shift(), size()],
|
||||||
|
whileElementsMounted: autoUpdate,
|
||||||
|
open: isMenuOpen,
|
||||||
|
});
|
||||||
|
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const companyIds = commentThread.commentThreadTargets
|
||||||
|
?.filter((relation) => relation.commentableType === 'Company')
|
||||||
|
.map((relation) => relation.commentableId);
|
||||||
|
|
||||||
|
// const personIds = commentThread.commentThreadTargets
|
||||||
|
// ?.filter((relation) => relation.commentableType === 'Person')
|
||||||
|
// .map((relation) => relation.commentableId);
|
||||||
|
|
||||||
|
const { data: dataForChips } = useSearchCompanyQueryQuery({
|
||||||
|
variables: {
|
||||||
|
where: {
|
||||||
|
id: {
|
||||||
|
in: companyIds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: dataForSelect } = useSearchCompanyQueryQuery({
|
||||||
|
variables: {
|
||||||
|
where: {
|
||||||
|
name: {
|
||||||
|
contains: `%${searchFilter}%`,
|
||||||
|
mode: QueryMode.Insensitive,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
limit: 5,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleFilterChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||||
|
debouncedSetSearchFilter(event.currentTarget.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleChangeRelationsClick() {
|
||||||
|
setIsMenuOpen((isOpen) => !isOpen);
|
||||||
|
}
|
||||||
|
|
||||||
|
const companiesForChips = dataForChips?.searchResults ?? [];
|
||||||
|
const companiesForSelect = dataForSelect?.searchResults ?? [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledContainer>
|
||||||
|
<IconArrowUpRight size={20} color={theme.text40} />
|
||||||
|
<StyledRelationLabel>Relations</StyledRelationLabel>
|
||||||
|
<StyledRelationContainer
|
||||||
|
ref={refs.setReference}
|
||||||
|
onClick={handleChangeRelationsClick}
|
||||||
|
>
|
||||||
|
{companiesForChips?.map((company) => (
|
||||||
|
<CompanyChip
|
||||||
|
key={company.id}
|
||||||
|
name={company.name}
|
||||||
|
picture={getLogoUrlFromDomainName(company.domainName)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</StyledRelationContainer>
|
||||||
|
{/* <StyledAddButton id="add-button" onClick={handleAddButtonClick}>
|
||||||
|
<IconPlus size={14} color={theme.text40} strokeWidth={1.5} />
|
||||||
|
</StyledAddButton> */}
|
||||||
|
{isMenuOpen && (
|
||||||
|
<DropdownMenu ref={refs.setFloating} style={floatingStyles}>
|
||||||
|
<DropdownMenuSearch
|
||||||
|
value={searchFilter}
|
||||||
|
onChange={handleFilterChange}
|
||||||
|
/>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItemContainer>
|
||||||
|
{companiesForSelect?.slice(0, 5)?.map((company) => (
|
||||||
|
<DropdownMenuCheckableItem
|
||||||
|
checked={
|
||||||
|
companiesForChips
|
||||||
|
?.map((companyForChip) => companyForChip.id)
|
||||||
|
?.includes(company.id) ?? false
|
||||||
|
}
|
||||||
|
onChange={(newCheckedValue) => {
|
||||||
|
if (newCheckedValue) {
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
avatarUrl={getLogoUrlFromDomainName(company.domainName)}
|
||||||
|
placeholder={company.name}
|
||||||
|
size={16}
|
||||||
|
/>
|
||||||
|
{company.name}
|
||||||
|
</DropdownMenuCheckableItem>
|
||||||
|
))}
|
||||||
|
{companiesForSelect?.length === 0 && (
|
||||||
|
<DropdownMenuItem>No result</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</DropdownMenuItemContainer>
|
||||||
|
</DropdownMenu>
|
||||||
|
)}
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||||
|
import { mockedCommentThreads } from '~/testing/mock-data/comment-threads';
|
||||||
|
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
|
||||||
|
|
||||||
|
import { CommentThreadRelationPicker } from '../CommentThreadRelationPicker';
|
||||||
|
|
||||||
|
const meta: Meta<typeof CommentThreadRelationPicker> = {
|
||||||
|
title: 'Comments/CommentThreadRelationPicker',
|
||||||
|
component: CommentThreadRelationPicker,
|
||||||
|
parameters: {
|
||||||
|
msw: graphqlMocks,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledContainer = styled.div`
|
||||||
|
width: 400px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof CommentThreadRelationPicker>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
render: getRenderWrapperForComponent(
|
||||||
|
<StyledContainer>
|
||||||
|
<CommentThreadRelationPicker commentThread={mockedCommentThreads[0]} />
|
||||||
|
</StyledContainer>,
|
||||||
|
),
|
||||||
|
};
|
||||||
@ -25,6 +25,10 @@ export const GET_COMMENT_THREADS_BY_TARGETS = gql`
|
|||||||
avatarUrl
|
avatarUrl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
commentThreadTargets {
|
||||||
|
commentableId
|
||||||
|
commentableType
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@ -44,6 +48,10 @@ export const GET_COMMENT_THREAD = gql`
|
|||||||
avatarUrl
|
avatarUrl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
commentThreadTargets {
|
||||||
|
commentableId
|
||||||
|
commentableType
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
import { Avatar } from '@/users/components/Avatar';
|
||||||
|
|
||||||
export type CompanyChipPropsType = {
|
export type CompanyChipPropsType = {
|
||||||
name: string;
|
name: string;
|
||||||
picture?: string;
|
picture?: string;
|
||||||
@ -15,6 +16,8 @@ const StyledContainer = styled.span`
|
|||||||
gap: ${(props) => props.theme.spacing(1)};
|
gap: ${(props) => props.theme.spacing(1)};
|
||||||
padding: ${(props) => props.theme.spacing(1)};
|
padding: ${(props) => props.theme.spacing(1)};
|
||||||
|
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
:hover {
|
:hover {
|
||||||
filter: brightness(95%);
|
filter: brightness(95%);
|
||||||
}
|
}
|
||||||
@ -30,10 +33,11 @@ function CompanyChip({ name, picture }: CompanyChipPropsType) {
|
|||||||
return (
|
return (
|
||||||
<StyledContainer data-testid="company-chip">
|
<StyledContainer data-testid="company-chip">
|
||||||
{picture && (
|
{picture && (
|
||||||
<img
|
<Avatar
|
||||||
data-testid="company-chip-image"
|
avatarUrl={picture?.toString()}
|
||||||
src={picture?.toString()}
|
placeholder={name}
|
||||||
alt={`${name}-company-logo`}
|
type="squared"
|
||||||
|
size={14}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{name}
|
{name}
|
||||||
|
|||||||
@ -2,8 +2,8 @@ import * as React from 'react';
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
name: string;
|
name?: string;
|
||||||
id: string;
|
id?: string;
|
||||||
checked?: boolean;
|
checked?: boolean;
|
||||||
indeterminate?: boolean;
|
indeterminate?: boolean;
|
||||||
onChange?: (newCheckedValue: boolean) => void;
|
onChange?: (newCheckedValue: boolean) => void;
|
||||||
|
|||||||
@ -17,4 +17,6 @@ export const DropdownMenu = styled.div`
|
|||||||
height: fit-content;
|
height: fit-content;
|
||||||
|
|
||||||
width: 200px;
|
width: 200px;
|
||||||
|
|
||||||
|
z-index: ${(props) => props.theme.lastLayerZIndex};
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import { DropdownMenuButton } from './DropdownMenuButton';
|
|||||||
type Props = {
|
type Props = {
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
onChange?: (newCheckedValue: boolean) => void;
|
onChange?: (newCheckedValue: boolean) => void;
|
||||||
id: string;
|
id?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DropdownMenuCheckableItemContainer = styled(DropdownMenuButton)`
|
const DropdownMenuCheckableItemContainer = styled(DropdownMenuButton)`
|
||||||
|
|||||||
@ -25,8 +25,10 @@ type Story = StoryObj<typeof DropdownMenu>;
|
|||||||
|
|
||||||
const FakeContentBelow = () => (
|
const FakeContentBelow = () => (
|
||||||
<div style={{ position: 'absolute' }}>
|
<div style={{ position: 'absolute' }}>
|
||||||
askjdlaksjdlaksjdlakjsdlkj lkajsldkjalskd jalksdj alksjd alskjd alksjd
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
|
||||||
alksjd laksjd askjdlaksjdlaksjdlakjsdlkj lkajsldkjalskd jalksdj alksjd
|
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
|
||||||
|
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
|
||||||
|
consequat.
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -217,7 +219,7 @@ const FakeSelectableMenuItemWithAvatarList = () => {
|
|||||||
onClick={() => setSelectedItem(item.id)}
|
onClick={() => setSelectedItem(item.id)}
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
placeholderLetter="A"
|
placeholder="A"
|
||||||
avatarUrl={item.avatarUrl}
|
avatarUrl={item.avatarUrl}
|
||||||
size={16}
|
size={16}
|
||||||
type="squared"
|
type="squared"
|
||||||
@ -303,7 +305,7 @@ const FakeCheckableMenuItemWithAvatarList = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
placeholderLetter="A"
|
placeholder="A"
|
||||||
avatarUrl={item.avatarUrl}
|
avatarUrl={item.avatarUrl}
|
||||||
size={16}
|
size={16}
|
||||||
type="squared"
|
type="squared"
|
||||||
|
|||||||
@ -0,0 +1,3 @@
|
|||||||
|
export function SelectSingleEntity() {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
@ -26,3 +26,4 @@ export { IconChevronDown } from '@tabler/icons-react';
|
|||||||
export { IconArrowNarrowDown } from '@tabler/icons-react';
|
export { IconArrowNarrowDown } from '@tabler/icons-react';
|
||||||
export { IconArrowNarrowUp } from '@tabler/icons-react';
|
export { IconArrowNarrowUp } from '@tabler/icons-react';
|
||||||
export { IconArrowRight } from '@tabler/icons-react';
|
export { IconArrowRight } from '@tabler/icons-react';
|
||||||
|
export { IconArrowUpRight } from '@tabler/icons-react';
|
||||||
|
|||||||
@ -54,7 +54,7 @@ const lightThemeSpecific = {
|
|||||||
lighterBackgroundTransparent: 'rgba(0, 0, 0, 0.02)',
|
lighterBackgroundTransparent: 'rgba(0, 0, 0, 0.02)',
|
||||||
|
|
||||||
primaryBorder: 'rgba(0, 0, 0, 0.08)',
|
primaryBorder: 'rgba(0, 0, 0, 0.08)',
|
||||||
lightBorder: '#f5f5f5',
|
lightBorder: 'rgba(245, 245, 245, 1)',
|
||||||
mediumBorder: '#ebebeb',
|
mediumBorder: '#ebebeb',
|
||||||
|
|
||||||
clickableElementBackgroundTransition: 'background 0.1s ease',
|
clickableElementBackgroundTransition: 'background 0.1s ease',
|
||||||
|
|||||||
@ -5,26 +5,30 @@ import { isNonEmptyString } from '@/utils/type-guards/isNonEmptyString';
|
|||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
avatarUrl: string | null | undefined;
|
avatarUrl: string | null | undefined;
|
||||||
size: number;
|
size: number;
|
||||||
placeholderLetter: string;
|
placeholder: string;
|
||||||
type?: 'squared' | 'rounded';
|
type?: 'squared' | 'rounded';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const StyledAvatar = styled.div<Omit<OwnProps, 'placeholderLetter'>>`
|
export const StyledAvatar = styled.div<Omit<OwnProps, 'placeholder'>>`
|
||||||
align-items: center;
|
|
||||||
background-color: ${(props) =>
|
background-color: ${(props) =>
|
||||||
!isNonEmptyString(props.avatarUrl)
|
!isNonEmptyString(props.avatarUrl)
|
||||||
? props.theme.tertiaryBackground
|
? props.theme.tertiaryBackground
|
||||||
: 'none'};
|
: 'none'};
|
||||||
background-image: url(${(props) =>
|
background-image: url(${(props) =>
|
||||||
isNonEmptyString(props.avatarUrl) ? props.avatarUrl : 'none'});
|
isNonEmptyString(props.avatarUrl) ? props.avatarUrl : 'none'});
|
||||||
|
background-image: url(${(props) =>
|
||||||
|
isNonEmptyString(props.avatarUrl) ? props.avatarUrl : 'none'});
|
||||||
background-position: center center;
|
background-position: center center;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
border-radius: ${(props) => (props.type === 'rounded' ? '50%' : '2px')};
|
border-radius: ${(props) => (props.type === 'rounded' ? '50%' : '2px')};
|
||||||
|
border-radius: ${(props) => (props.type === 'rounded' ? '50%' : '2px')};
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
height: ${(props) => props.size}px;
|
height: ${(props) => props.size}px;
|
||||||
|
height: ${(props) => props.size}px;
|
||||||
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
width: ${(props) => props.size}px;
|
||||||
|
|
||||||
width: ${(props) => props.size}px;
|
width: ${(props) => props.size}px;
|
||||||
`;
|
`;
|
||||||
@ -50,7 +54,7 @@ export const StyledPlaceholderLetter = styled.div<StyledPlaceholderLetterProps>`
|
|||||||
export function Avatar({
|
export function Avatar({
|
||||||
avatarUrl,
|
avatarUrl,
|
||||||
size,
|
size,
|
||||||
placeholderLetter,
|
placeholder,
|
||||||
type = 'squared',
|
type = 'squared',
|
||||||
}: OwnProps) {
|
}: OwnProps) {
|
||||||
const noAvatarUrl = !isNonEmptyString(avatarUrl);
|
const noAvatarUrl = !isNonEmptyString(avatarUrl);
|
||||||
@ -59,7 +63,7 @@ export function Avatar({
|
|||||||
<StyledAvatar avatarUrl={avatarUrl} size={size} type={type}>
|
<StyledAvatar avatarUrl={avatarUrl} size={size} type={type}>
|
||||||
{noAvatarUrl && (
|
{noAvatarUrl && (
|
||||||
<StyledPlaceholderLetter size={size}>
|
<StyledPlaceholderLetter size={size}>
|
||||||
{placeholderLetter}
|
{placeholder[0]?.toLocaleUpperCase()}
|
||||||
</StyledPlaceholderLetter>
|
</StyledPlaceholderLetter>
|
||||||
)}
|
)}
|
||||||
</StyledAvatar>
|
</StyledAvatar>
|
||||||
|
|||||||
@ -17,34 +17,24 @@ const avatarUrl =
|
|||||||
|
|
||||||
export const Rounded: Story = {
|
export const Rounded: Story = {
|
||||||
render: getRenderWrapperForComponent(
|
render: getRenderWrapperForComponent(
|
||||||
<Avatar
|
<Avatar avatarUrl={avatarUrl} size={16} placeholder="L" type="rounded" />,
|
||||||
avatarUrl={avatarUrl}
|
|
||||||
size={16}
|
|
||||||
placeholderLetter="L"
|
|
||||||
type="rounded"
|
|
||||||
/>,
|
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Squared: Story = {
|
export const Squared: Story = {
|
||||||
render: getRenderWrapperForComponent(
|
render: getRenderWrapperForComponent(
|
||||||
<Avatar
|
<Avatar avatarUrl={avatarUrl} size={16} placeholder="L" type="squared" />,
|
||||||
avatarUrl={avatarUrl}
|
|
||||||
size={16}
|
|
||||||
placeholderLetter="L"
|
|
||||||
type="squared"
|
|
||||||
/>,
|
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NoAvatarPictureRounded: Story = {
|
export const NoAvatarPictureRounded: Story = {
|
||||||
render: getRenderWrapperForComponent(
|
render: getRenderWrapperForComponent(
|
||||||
<Avatar avatarUrl={''} size={16} placeholderLetter="L" type="rounded" />,
|
<Avatar avatarUrl={''} size={16} placeholder="L" type="rounded" />,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NoAvatarPictureSquared: Story = {
|
export const NoAvatarPictureSquared: Story = {
|
||||||
render: getRenderWrapperForComponent(
|
render: getRenderWrapperForComponent(
|
||||||
<Avatar avatarUrl={''} size={16} placeholderLetter="L" type="squared" />,
|
<Avatar avatarUrl={''} size={16} placeholder="L" type="squared" />,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|||||||
76
front/src/testing/mock-data/comment-threads.ts
Normal file
76
front/src/testing/mock-data/comment-threads.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { CommentableType, CommentThread } from '~/generated/graphql';
|
||||||
|
|
||||||
|
export const mockedCommentThreads: Array<CommentThread> = [
|
||||||
|
{
|
||||||
|
id: '89bb825c-171e-4bcc-9cf7-43448d6fb230',
|
||||||
|
createdAt: '2023-04-26T10:12:42.33625+00:00',
|
||||||
|
updatedAt: '2023-04-26T10:23:42.33625+00:00',
|
||||||
|
commentThreadTargets: [
|
||||||
|
{
|
||||||
|
id: '89bb825c-171e-4bcc-9cf7-43448d6fb300',
|
||||||
|
createdAt: '2023-04-26T10:12:42.33625+00:00',
|
||||||
|
updatedAt: '2023-04-26T10:23:42.33625+00:00',
|
||||||
|
commentableType: CommentableType.Company,
|
||||||
|
commentableId: '89bb825c-171e-4bcc-9cf7-43448d6fb278', // airbnb
|
||||||
|
commentThreadId: '89bb825c-171e-4bcc-9cf7-43448d6fb230',
|
||||||
|
commentThread: {
|
||||||
|
id: '89bb825c-171e-4bcc-9cf7-43448d6fb230',
|
||||||
|
createdAt: '2023-04-26T10:12:42.33625+00:00',
|
||||||
|
updatedAt: '2023-04-26T10:23:42.33625+00:00',
|
||||||
|
},
|
||||||
|
__typename: 'CommentThreadTarget',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '89bb825c-171e-4bcc-9cf7-43448d6fb301',
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
commentableType: CommentableType.Company,
|
||||||
|
commentableId: 'b396e6b9-dc5c-4643-bcff-61b6cf7523ae', // aircall
|
||||||
|
commentThreadId: '89bb825c-171e-4bcc-9cf7-43448d6fb231',
|
||||||
|
commentThread: {
|
||||||
|
id: '89bb825c-171e-4bcc-9cf7-43448d6fb231',
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
__typename: 'CommentThreadTarget',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
__typename: 'CommentThread',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '89bb825c-171e-4bcc-9cf7-43448d6fb278',
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
commentThreadTargets: [
|
||||||
|
{
|
||||||
|
id: '89bb825c-171e-4bcc-9cf7-43448d6fb278',
|
||||||
|
createdAt: '2023-04-26T10:12:42.33625+00:00',
|
||||||
|
updatedAt: '2023-04-26T10:23:42.33625+00:00',
|
||||||
|
commentableType: CommentableType.Person,
|
||||||
|
commentableId: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b', // Alexandre
|
||||||
|
commentThreadId: '89bb825c-171e-4bcc-9cf7-43448d6fb278',
|
||||||
|
commentThread: {
|
||||||
|
id: '89bb825c-171e-4bcc-9cf7-43448d6fb278',
|
||||||
|
createdAt: '2023-04-26T10:12:42.33625+00:00',
|
||||||
|
updatedAt: '2023-04-26T10:23:42.33625+00:00',
|
||||||
|
},
|
||||||
|
__typename: 'CommentThreadTarget',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '89bb825c-171e-4bcc-9cf7-43448d6fb278',
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
commentableType: CommentableType.Person,
|
||||||
|
commentableId: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6d', // Jean d'Eau
|
||||||
|
commentThreadId: '89bb825c-171e-4bcc-9cf7-43448d6fb278',
|
||||||
|
commentThread: {
|
||||||
|
id: '89bb825c-171e-4bcc-9cf7-43448d6fb278',
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
__typename: 'CommentThreadTarget',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
__typename: 'CommentThread',
|
||||||
|
},
|
||||||
|
];
|
||||||
@ -52,6 +52,11 @@ function filterData<DataT>(
|
|||||||
filterElement.contains.replaceAll('%', '').toLocaleLowerCase(),
|
filterElement.contains.replaceAll('%', '').toLocaleLowerCase(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (filterElement.in) {
|
||||||
|
const itemValue = item[key as keyof typeof item] as string;
|
||||||
|
|
||||||
|
return filterElement.in.includes(itemValue);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1881,13 +1881,29 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.3.0.tgz#113bc85fa102cf890ae801668f43ee265c547a09"
|
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.3.0.tgz#113bc85fa102cf890ae801668f43ee265c547a09"
|
||||||
integrity sha512-vX1WVAdPjZg9DkDkC+zEx/tKtnST6/qcNpwcjeBgco3XRNHz5PUA+ivi/yr6G3o0kMR60uKBJcfOdfzOFI7PMQ==
|
integrity sha512-vX1WVAdPjZg9DkDkC+zEx/tKtnST6/qcNpwcjeBgco3XRNHz5PUA+ivi/yr6G3o0kMR60uKBJcfOdfzOFI7PMQ==
|
||||||
|
|
||||||
"@floating-ui/dom@^1.0.0":
|
"@floating-ui/dom@^1.0.0", "@floating-ui/dom@^1.3.0":
|
||||||
version "1.3.0"
|
version "1.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.3.0.tgz#69456f2164fc3d33eb40837686eaf71537235ac9"
|
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.3.0.tgz#69456f2164fc3d33eb40837686eaf71537235ac9"
|
||||||
integrity sha512-qIAwejE3r6NeA107u4ELDKkH8+VtgRKdXqtSPaKflL2S2V+doyN+Wt9s5oHKXPDo4E8TaVXaHT3+6BbagH31xw==
|
integrity sha512-qIAwejE3r6NeA107u4ELDKkH8+VtgRKdXqtSPaKflL2S2V+doyN+Wt9s5oHKXPDo4E8TaVXaHT3+6BbagH31xw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@floating-ui/core" "^1.3.0"
|
"@floating-ui/core" "^1.3.0"
|
||||||
|
|
||||||
|
"@floating-ui/react-dom@^2.0.1":
|
||||||
|
version "2.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.0.1.tgz#7972a4fc488a8c746cded3cfe603b6057c308a91"
|
||||||
|
integrity sha512-rZtAmSht4Lry6gdhAJDrCp/6rKN7++JnL1/Anbr/DdeyYXQPxvg/ivrbYvJulbRf4vL8b212suwMM2lxbv+RQA==
|
||||||
|
dependencies:
|
||||||
|
"@floating-ui/dom" "^1.3.0"
|
||||||
|
|
||||||
|
"@floating-ui/react@^0.24.3":
|
||||||
|
version "0.24.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.24.3.tgz#4f11f09c7245555724f5167dd6925133457db89c"
|
||||||
|
integrity sha512-wWC9duiog4HmbgKSKObDRuXqMjZR/6m75MIG+slm5CVWbridAjK9STcnCsGYmdpK78H/GmzYj4ADVP8paZVLYQ==
|
||||||
|
dependencies:
|
||||||
|
"@floating-ui/react-dom" "^2.0.1"
|
||||||
|
aria-hidden "^1.1.3"
|
||||||
|
tabbable "^6.0.1"
|
||||||
|
|
||||||
"@graphql-codegen/cli@^3.3.1":
|
"@graphql-codegen/cli@^3.3.1":
|
||||||
version "3.3.1"
|
version "3.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/@graphql-codegen/cli/-/cli-3.3.1.tgz#103e7a2263126fdde168a1ce623fc2bdc05352f0"
|
resolved "https://registry.yarnpkg.com/@graphql-codegen/cli/-/cli-3.3.1.tgz#103e7a2263126fdde168a1ce623fc2bdc05352f0"
|
||||||
@ -5437,7 +5453,7 @@ argparse@^2.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
|
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
|
||||||
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
|
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
|
||||||
|
|
||||||
aria-hidden@^1.1.1:
|
aria-hidden@^1.1.1, aria-hidden@^1.1.3:
|
||||||
version "1.2.3"
|
version "1.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.3.tgz#14aeb7fb692bbb72d69bebfa47279c1fd725e954"
|
resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.3.tgz#14aeb7fb692bbb72d69bebfa47279c1fd725e954"
|
||||||
integrity sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==
|
integrity sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==
|
||||||
@ -15557,6 +15573,11 @@ synchronous-promise@^2.0.15:
|
|||||||
resolved "https://registry.yarnpkg.com/synchronous-promise/-/synchronous-promise-2.0.17.tgz#38901319632f946c982152586f2caf8ddc25c032"
|
resolved "https://registry.yarnpkg.com/synchronous-promise/-/synchronous-promise-2.0.17.tgz#38901319632f946c982152586f2caf8ddc25c032"
|
||||||
integrity sha512-AsS729u2RHUfEra9xJrE39peJcc2stq2+poBXX8bcM08Y6g9j/i/PUzwNQqkaJde7Ntg1TO7bSREbR5sdosQ+g==
|
integrity sha512-AsS729u2RHUfEra9xJrE39peJcc2stq2+poBXX8bcM08Y6g9j/i/PUzwNQqkaJde7Ntg1TO7bSREbR5sdosQ+g==
|
||||||
|
|
||||||
|
tabbable@^6.0.1:
|
||||||
|
version "6.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-6.1.2.tgz#b0d3ca81d582d48a80f71b267d1434b1469a3703"
|
||||||
|
integrity sha512-qCN98uP7i9z0fIS4amQ5zbGBOq+OSigYeGvPy7NDk8Y9yncqDZ9pRPgfsc2PJIVM9RrJj7GIfuRgmjoUU9zTHQ==
|
||||||
|
|
||||||
tailwindcss@^3.0.2:
|
tailwindcss@^3.0.2:
|
||||||
version "3.3.2"
|
version "3.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.3.2.tgz#2f9e35d715fdf0bbf674d90147a0684d7054a2d3"
|
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.3.2.tgz#2f9e35d715fdf0bbf674d90147a0684d7054a2d3"
|
||||||
|
|||||||
@ -65,15 +65,6 @@ export class CommentThreadResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return createdCommentThread;
|
return createdCommentThread;
|
||||||
|
|
||||||
// return this.prismaService.commentThread.create({
|
|
||||||
// data: {
|
|
||||||
// ...args.data,
|
|
||||||
// ...{ commentThreadTargets: undefined },
|
|
||||||
// ...{ comments: { createMany: { data: newCommentData } } },
|
|
||||||
// ...{ workspace: { connect: { id: workspace.id } } },
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Query(() => [CommentThread])
|
@Query(() => [CommentThread])
|
||||||
@ -86,6 +77,7 @@ export class CommentThreadResolver {
|
|||||||
args,
|
args,
|
||||||
workspace,
|
workspace,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await this.prismaService.commentThread.findMany(
|
const result = await this.prismaService.commentThread.findMany(
|
||||||
preparedArgs,
|
preparedArgs,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import * as TypeGraphQL from '@nestjs/graphql';
|
import * as TypeGraphQL from '@nestjs/graphql';
|
||||||
|
import { CommentThreadTarget } from 'src/api/@generated/comment-thread-target/comment-thread-target.model';
|
||||||
import { CommentThread } from 'src/api/@generated/comment-thread/comment-thread.model';
|
import { CommentThread } from 'src/api/@generated/comment-thread/comment-thread.model';
|
||||||
import { Comment } from 'src/api/@generated/comment/comment.model';
|
import { Comment } from 'src/api/@generated/comment/comment.model';
|
||||||
import { PrismaService } from 'src/database/prisma.service';
|
import { PrismaService } from 'src/database/prisma.service';
|
||||||
@ -23,4 +24,21 @@ export class CommentThreadRelationsResolver {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@TypeGraphQL.ResolveField(() => [CommentThreadTarget], {
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
async commentThreadTargets(
|
||||||
|
@TypeGraphQL.Root() commentThread: CommentThread,
|
||||||
|
): Promise<CommentThreadTarget[]> {
|
||||||
|
return this.prismaService.commentThreadTarget.findMany({
|
||||||
|
where: {
|
||||||
|
commentThreadId: commentThread.id,
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
// TODO: find a way to pass it in the query
|
||||||
|
createdAt: 'desc',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user