feat(ai): add current context to ai chat (#13315)
## TODO - [ ] add dropdown to use records from outside the context - [x] add loader for files chip - [x] add roleId where it's necessary - [x] Split AvatarChip in two components. One with the icon that will call the second with leftComponent. - [ ] Fix tests - [x] Fix UI regression on Search
This commit is contained in:
@ -1,38 +1,114 @@
|
||||
import { AvatarChipsLeftComponent } from '@ui/components/avatar-chip/AvatarChipLeftComponent';
|
||||
import { AvatarChipsCommonProps } from '@ui/components/avatar-chip/types/AvatarChipsCommonProps.type';
|
||||
import { Chip, ChipVariant } from '@ui/components/chip/Chip';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { Avatar } from '@ui/display/avatar/components/Avatar';
|
||||
import { AvatarType } from '@ui/display/avatar/types/AvatarType';
|
||||
import { IconComponent } from '@ui/display/icon/types/IconComponent';
|
||||
import { Nullable } from '@ui/utilities';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const StyledIconWithBackgroundContainer = styled.div<{
|
||||
backgroundColor: string;
|
||||
}>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 4px;
|
||||
background-color: ${({ backgroundColor }) => backgroundColor};
|
||||
`;
|
||||
|
||||
const StyledAvatarChipWrapper = styled.div<{
|
||||
isClickable: boolean;
|
||||
divider: AvatarChipProps['divider'];
|
||||
theme: any;
|
||||
}>`
|
||||
${({ divider, theme }) => {
|
||||
const borderStyle = (side: 'left' | 'right') =>
|
||||
`border-${side}: 1px solid ${theme.border.color.light};`;
|
||||
return divider ? borderStyle(divider) : '';
|
||||
}}
|
||||
|
||||
cursor: ${({ isClickable }) => (isClickable ? 'pointer' : 'inherit')};
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
export type AvatarChipProps = {
|
||||
placeholder?: string;
|
||||
avatarUrl?: string;
|
||||
avatarType?: Nullable<AvatarType>;
|
||||
Icon?: IconComponent;
|
||||
IconColor?: string;
|
||||
IconBackgroundColor?: string;
|
||||
isIconInverted?: boolean;
|
||||
placeholderColorSeed?: string;
|
||||
divider?: 'right' | 'left';
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
export type AvatarChipProps = AvatarChipsCommonProps;
|
||||
export const AvatarChip = ({
|
||||
name,
|
||||
LeftIcon,
|
||||
LeftIconColor,
|
||||
Icon,
|
||||
placeholderColorSeed,
|
||||
avatarType,
|
||||
avatarUrl,
|
||||
className,
|
||||
isIconInverted,
|
||||
maxWidth,
|
||||
placeholderColorSeed,
|
||||
size,
|
||||
variant = ChipVariant.Transparent,
|
||||
}: AvatarChipProps) => (
|
||||
<Chip
|
||||
label={name}
|
||||
variant={variant}
|
||||
size={size}
|
||||
leftComponent={
|
||||
<AvatarChipsLeftComponent
|
||||
name={name}
|
||||
LeftIcon={LeftIcon}
|
||||
LeftIconColor={LeftIconColor}
|
||||
avatarType={avatarType}
|
||||
placeholder,
|
||||
isIconInverted = false,
|
||||
IconColor,
|
||||
IconBackgroundColor,
|
||||
onClick,
|
||||
divider,
|
||||
}: AvatarChipProps) => {
|
||||
const theme = useTheme();
|
||||
if (!isDefined(Icon)) {
|
||||
return (
|
||||
<Avatar
|
||||
avatarUrl={avatarUrl}
|
||||
isIconInverted={isIconInverted}
|
||||
placeholderColorSeed={placeholderColorSeed}
|
||||
placeholder={placeholder}
|
||||
size="sm"
|
||||
type={avatarType}
|
||||
onClick={onClick}
|
||||
/>
|
||||
}
|
||||
clickable={false}
|
||||
className={className}
|
||||
maxWidth={maxWidth}
|
||||
/>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
const isClickable = isDefined(onClick);
|
||||
|
||||
if (isIconInverted || isDefined(IconBackgroundColor)) {
|
||||
return (
|
||||
<StyledAvatarChipWrapper
|
||||
isClickable={isClickable}
|
||||
divider={divider}
|
||||
theme={theme}
|
||||
onClick={onClick}
|
||||
>
|
||||
<StyledIconWithBackgroundContainer
|
||||
backgroundColor={
|
||||
IconBackgroundColor ?? theme.background.invertedSecondary
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
color={theme.font.color.inverted}
|
||||
size={theme.icon.size.sm}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
</StyledIconWithBackgroundContainer>
|
||||
</StyledAvatarChipWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledAvatarChipWrapper
|
||||
isClickable={isClickable}
|
||||
divider={divider}
|
||||
theme={theme}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Icon
|
||||
size={theme.icon.size.sm}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
color={IconColor || 'currentColor'}
|
||||
/>
|
||||
</StyledAvatarChipWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,74 +0,0 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { styled } from '@linaria/react';
|
||||
import { Avatar } from '@ui/display/avatar/components/Avatar';
|
||||
import { AvatarType } from '@ui/display/avatar/types/AvatarType';
|
||||
import { IconComponent } from '@ui/display/icon/types/IconComponent';
|
||||
import { Nullable } from '@ui/utilities';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const StyledInvertedIconContainer = styled.div<{ backgroundColor: string }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 4px;
|
||||
background-color: ${({ backgroundColor }) => backgroundColor};
|
||||
`;
|
||||
|
||||
export type AvatarChipsLeftComponentProps = {
|
||||
name: string;
|
||||
avatarUrl?: string;
|
||||
avatarType?: Nullable<AvatarType>;
|
||||
LeftIcon?: IconComponent;
|
||||
LeftIconColor?: string;
|
||||
isIconInverted?: boolean;
|
||||
placeholderColorSeed?: string;
|
||||
};
|
||||
|
||||
export const AvatarChipsLeftComponent: React.FC<
|
||||
AvatarChipsLeftComponentProps
|
||||
> = ({
|
||||
LeftIcon,
|
||||
placeholderColorSeed,
|
||||
avatarType,
|
||||
avatarUrl,
|
||||
name,
|
||||
isIconInverted = false,
|
||||
LeftIconColor,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
if (!isDefined(LeftIcon)) {
|
||||
return (
|
||||
<Avatar
|
||||
avatarUrl={avatarUrl}
|
||||
placeholderColorSeed={placeholderColorSeed}
|
||||
placeholder={name}
|
||||
size="sm"
|
||||
type={avatarType}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isIconInverted) {
|
||||
return (
|
||||
<StyledInvertedIconContainer
|
||||
backgroundColor={theme.background.invertedSecondary}
|
||||
>
|
||||
<LeftIcon
|
||||
color={theme.font.color.inverted}
|
||||
size={theme.icon.size.sm}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
</StyledInvertedIconContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<LeftIcon
|
||||
size={theme.icon.size.sm}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
color={LeftIconColor || 'currentColor'}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -1,64 +0,0 @@
|
||||
import { AvatarChipsLeftComponent } from '@ui/components/avatar-chip/AvatarChipLeftComponent';
|
||||
import { AvatarChipsCommonProps } from '@ui/components/avatar-chip/types/AvatarChipsCommonProps.type';
|
||||
import { AvatarChipVariant } from '@ui/components/avatar-chip/types/AvatarChipsVariant.type';
|
||||
import { ChipVariant } from '@ui/components/chip/Chip';
|
||||
import { LinkChip, LinkChipProps } from '@ui/components/chip/LinkChip';
|
||||
import { TriggerEventType } from '@ui/utilities';
|
||||
|
||||
export type LinkAvatarChipProps = Omit<
|
||||
AvatarChipsCommonProps,
|
||||
'clickable' | 'variant'
|
||||
> & {
|
||||
to: string;
|
||||
onClick?: LinkChipProps['onClick'];
|
||||
onMouseDown?: LinkChipProps['onMouseDown'];
|
||||
variant?: AvatarChipVariant;
|
||||
isLabelHidden?: boolean;
|
||||
triggerEvent?: TriggerEventType;
|
||||
};
|
||||
|
||||
export const LinkAvatarChip = ({
|
||||
to,
|
||||
onClick,
|
||||
name,
|
||||
LeftIcon,
|
||||
LeftIconColor,
|
||||
avatarType,
|
||||
avatarUrl,
|
||||
className,
|
||||
isIconInverted,
|
||||
maxWidth,
|
||||
placeholderColorSeed,
|
||||
size,
|
||||
variant,
|
||||
isLabelHidden,
|
||||
triggerEvent,
|
||||
}: LinkAvatarChipProps) => (
|
||||
<LinkChip
|
||||
to={to}
|
||||
onClick={onClick}
|
||||
label={name}
|
||||
isLabelHidden={isLabelHidden}
|
||||
variant={
|
||||
//Regular but Highlighted -> missleading
|
||||
variant === AvatarChipVariant.Regular
|
||||
? ChipVariant.Highlighted
|
||||
: ChipVariant.Regular
|
||||
}
|
||||
size={size}
|
||||
leftComponent={
|
||||
<AvatarChipsLeftComponent
|
||||
name={name}
|
||||
LeftIcon={LeftIcon}
|
||||
LeftIconColor={LeftIconColor}
|
||||
avatarType={avatarType}
|
||||
avatarUrl={avatarUrl}
|
||||
isIconInverted={isIconInverted}
|
||||
placeholderColorSeed={placeholderColorSeed}
|
||||
/>
|
||||
}
|
||||
className={className}
|
||||
maxWidth={maxWidth}
|
||||
triggerEvent={triggerEvent}
|
||||
/>
|
||||
);
|
||||
@ -0,0 +1,61 @@
|
||||
import { Fragment } from 'react/jsx-runtime';
|
||||
import styled from '@emotion/styled';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { Chip, ChipVariant } from '@ui/components/chip/Chip';
|
||||
|
||||
const StyledIconsContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const StyledChipContainer = styled.div`
|
||||
display: inline-flex;
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
`;
|
||||
|
||||
export type MultipleAvatarChipProps = {
|
||||
Icons: React.ReactNode[];
|
||||
text?: string;
|
||||
onClick?: () => void;
|
||||
testId?: string;
|
||||
maxWidth?: number;
|
||||
forceEmptyText?: boolean;
|
||||
variant?: ChipVariant;
|
||||
rightComponent?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const MultipleAvatarChip = ({
|
||||
Icons,
|
||||
text,
|
||||
onClick,
|
||||
testId,
|
||||
maxWidth,
|
||||
rightComponent,
|
||||
variant = ChipVariant.Static,
|
||||
forceEmptyText = false,
|
||||
}: MultipleAvatarChipProps) => {
|
||||
const leftComponent = (
|
||||
<StyledIconsContainer>
|
||||
{Icons.map((Icon, index) => (
|
||||
<Fragment key={index}>{Icon}</Fragment>
|
||||
))}
|
||||
</StyledIconsContainer>
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledChipContainer onClick={onClick} data-testid={testId}>
|
||||
<Chip
|
||||
label={text || ''}
|
||||
forceEmptyText={forceEmptyText}
|
||||
isLabelHidden={!isNonEmptyString(text) && forceEmptyText}
|
||||
variant={variant}
|
||||
leftComponent={leftComponent}
|
||||
rightComponent={rightComponent}
|
||||
clickable={isDefined(onClick)}
|
||||
maxWidth={maxWidth}
|
||||
/>
|
||||
</StyledChipContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,79 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { ComponentDecorator } from '@ui/testing';
|
||||
import { IconBuildingSkyscraper, IconUser } from '@ui/display';
|
||||
import { AvatarChip } from '../AvatarChip';
|
||||
|
||||
const meta: Meta<typeof AvatarChip> = {
|
||||
title: 'UI/Components/AvatarChip',
|
||||
component: AvatarChip,
|
||||
decorators: [ComponentDecorator],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof AvatarChip>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
placeholder: 'JD',
|
||||
placeholderColorSeed: 'John Doe',
|
||||
},
|
||||
};
|
||||
|
||||
export const WithAvatar: Story = {
|
||||
args: {
|
||||
avatarUrl: 'https://i.pravatar.cc/300',
|
||||
placeholder: 'JD',
|
||||
placeholderColorSeed: 'John Doe',
|
||||
},
|
||||
};
|
||||
|
||||
export const WithIcon: Story = {
|
||||
args: {
|
||||
Icon: IconUser,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithIconBackground: Story = {
|
||||
args: {
|
||||
Icon: IconBuildingSkyscraper,
|
||||
isIconInverted: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithInvertedIcon: Story = {
|
||||
args: {
|
||||
Icon: IconUser,
|
||||
isIconInverted: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithRightDivider: Story = {
|
||||
args: {
|
||||
placeholder: 'JD',
|
||||
placeholderColorSeed: 'John Doe',
|
||||
divider: 'right',
|
||||
},
|
||||
};
|
||||
|
||||
export const WithLeftDivider: Story = {
|
||||
args: {
|
||||
Icon: IconUser,
|
||||
divider: 'left',
|
||||
},
|
||||
};
|
||||
|
||||
export const Clickable: Story = {
|
||||
args: {
|
||||
placeholder: 'JD',
|
||||
placeholderColorSeed: 'John Doe',
|
||||
onClick: () => alert('AvatarChip clicked'),
|
||||
},
|
||||
};
|
||||
|
||||
export const ClickableIcon: Story = {
|
||||
args: {
|
||||
Icon: IconBuildingSkyscraper,
|
||||
isIconInverted: true,
|
||||
onClick: () => alert('Icon AvatarChip clicked'),
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,33 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { ComponentDecorator } from '@ui/testing';
|
||||
import { IconBuildingSkyscraper, IconUser } from '@ui/display';
|
||||
import { MultipleAvatarChip } from '@ui/components';
|
||||
|
||||
const meta: Meta<typeof MultipleAvatarChip> = {
|
||||
title: 'UI/Components/MultipleAvatarChip',
|
||||
component: MultipleAvatarChip,
|
||||
decorators: [ComponentDecorator],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof MultipleAvatarChip>;
|
||||
|
||||
export const SingleIcon: Story = {
|
||||
args: {
|
||||
Icons: [<IconUser size={16} />],
|
||||
text: 'Person',
|
||||
},
|
||||
};
|
||||
|
||||
export const MultipleIcons: Story = {
|
||||
args: {
|
||||
Icons: [<IconUser size={16} />, <IconBuildingSkyscraper size={16} />],
|
||||
text: 'Person & Company',
|
||||
},
|
||||
};
|
||||
|
||||
export const IconsOnly: Story = {
|
||||
args: {
|
||||
Icons: [<IconUser size={16} />, <IconBuildingSkyscraper size={16} />],
|
||||
},
|
||||
};
|
||||
@ -1,9 +0,0 @@
|
||||
import { AvatarChipsLeftComponentProps } from '@ui/components/avatar-chip/AvatarChipLeftComponent';
|
||||
import { ChipSize, ChipVariant } from '@ui/components/chip/Chip';
|
||||
|
||||
export type AvatarChipsCommonProps = {
|
||||
size?: ChipSize;
|
||||
className?: string;
|
||||
maxWidth?: number;
|
||||
variant?: ChipVariant;
|
||||
} & AvatarChipsLeftComponentProps;
|
||||
@ -1,4 +0,0 @@
|
||||
export enum AvatarChipVariant {
|
||||
Regular = 'regular',
|
||||
Transparent = 'transparent',
|
||||
}
|
||||
@ -32,8 +32,9 @@ export type ChipProps = {
|
||||
variant?: ChipVariant;
|
||||
accent?: ChipAccent;
|
||||
leftComponent?: ReactNode | null;
|
||||
rightComponent?: (() => ReactNode) | null;
|
||||
rightComponent?: (() => ReactNode) | ReactNode | null;
|
||||
className?: string;
|
||||
forceEmptyText?: boolean;
|
||||
};
|
||||
|
||||
const StyledDiv = withTheme(styled.div<{ theme: Theme }>`
|
||||
@ -125,6 +126,18 @@ const StyledContainer = withTheme(styled.div<
|
||||
: 'var(--chip-horizontal-padding)'};
|
||||
`);
|
||||
|
||||
const renderRightComponent = (
|
||||
rightComponent: (() => ReactNode) | ReactNode | null,
|
||||
) => {
|
||||
if (!rightComponent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return typeof rightComponent === 'function'
|
||||
? rightComponent()
|
||||
: rightComponent;
|
||||
};
|
||||
|
||||
export const Chip = ({
|
||||
size = ChipSize.Small,
|
||||
label,
|
||||
@ -137,6 +150,7 @@ export const Chip = ({
|
||||
accent = ChipAccent.TextPrimary,
|
||||
className,
|
||||
maxWidth,
|
||||
forceEmptyText = false,
|
||||
}: ChipProps) => {
|
||||
return (
|
||||
<StyledContainer
|
||||
@ -152,10 +166,12 @@ export const Chip = ({
|
||||
{leftComponent}
|
||||
{!isLabelHidden && label && label.trim() ? (
|
||||
<OverflowingTextWithTooltip size={size} text={label} />
|
||||
) : (
|
||||
) : !forceEmptyText ? (
|
||||
<StyledDiv>Untitled</StyledDiv>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
{rightComponent?.()}
|
||||
{renderRightComponent(rightComponent)}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,29 +0,0 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { AvatarChip } from '@ui/components/avatar-chip/AvatarChip';
|
||||
|
||||
import {
|
||||
ComponentDecorator,
|
||||
RecoilRootDecorator,
|
||||
RouterDecorator,
|
||||
} from '@ui/testing';
|
||||
|
||||
const meta: Meta<typeof AvatarChip> = {
|
||||
title: 'UI/Display/Chip/AvatarChip',
|
||||
component: AvatarChip,
|
||||
decorators: [RouterDecorator, ComponentDecorator, RecoilRootDecorator],
|
||||
args: {
|
||||
name: 'Entity name',
|
||||
avatarType: 'squared',
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof AvatarChip>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const Empty: Story = {
|
||||
args: {
|
||||
name: '',
|
||||
},
|
||||
};
|
||||
@ -9,12 +9,8 @@
|
||||
|
||||
export type { AvatarChipProps } from './avatar-chip/AvatarChip';
|
||||
export { AvatarChip } from './avatar-chip/AvatarChip';
|
||||
export type { AvatarChipsLeftComponentProps } from './avatar-chip/AvatarChipLeftComponent';
|
||||
export { AvatarChipsLeftComponent } from './avatar-chip/AvatarChipLeftComponent';
|
||||
export type { LinkAvatarChipProps } from './avatar-chip/LinkAvatarChip';
|
||||
export { LinkAvatarChip } from './avatar-chip/LinkAvatarChip';
|
||||
export type { AvatarChipsCommonProps } from './avatar-chip/types/AvatarChipsCommonProps.type';
|
||||
export { AvatarChipVariant } from './avatar-chip/types/AvatarChipsVariant.type';
|
||||
export type { MultipleAvatarChipProps } from './avatar-chip/MultipleAvatarChip';
|
||||
export { MultipleAvatarChip } from './avatar-chip/MultipleAvatarChip';
|
||||
export type { ChipProps } from './chip/Chip';
|
||||
export { ChipSize, ChipAccent, ChipVariant, Chip } from './chip/Chip';
|
||||
export { LINK_CHIP_CLICK_OUTSIDE_ID } from './chip/constants/LinkChipClickOutsideId';
|
||||
|
||||
Reference in New Issue
Block a user