refactor: move Checkmark, Avatar, Chip and Tooltip to twenty-ui (#4946)

Split from https://github.com/twentyhq/twenty/pull/4518

Part of #4766
This commit is contained in:
Thaïs
2024-04-15 12:05:06 +02:00
committed by GitHub
parent acc2092b95
commit b6d0b8a895
93 changed files with 225 additions and 189 deletions

View File

@ -1,7 +1,13 @@
import React from 'react';
import { css, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconCalendarEvent } from 'twenty-ui';
import {
Chip,
ChipAccent,
ChipSize,
ChipVariant,
IconCalendarEvent,
} from 'twenty-ui';
import { CalendarEventParticipantsResponseStatus } from '@/activities/calendar/components/CalendarEventParticipantsResponseStatus';
import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
@ -11,12 +17,6 @@ import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/util
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox';
import {
Chip,
ChipAccent,
ChipSize,
ChipVariant,
} from '@/ui/display/chip/components/Chip';
import { mapArrayToObject } from '~/utils/array/mapArrayToObject';
import { beautifyPastDateRelativeToNow } from '~/utils/date-utils';

View File

@ -3,7 +3,7 @@ import { css, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { format } from 'date-fns';
import { useRecoilValue } from 'recoil';
import { IconArrowRight, IconLock } from 'twenty-ui';
import { Avatar, AvatarGroup, IconArrowRight, IconLock } from 'twenty-ui';
import { CalendarCurrentEventCursor } from '@/activities/calendar/components/CalendarCurrentEventCursor';
import { CalendarContext } from '@/activities/calendar/contexts/CalendarContext';
@ -14,9 +14,8 @@ import { hasCalendarEventEnded } from '@/activities/calendar/utils/hasCalendarEv
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { Card } from '@/ui/layout/card/components/Card';
import { CardContent } from '@/ui/layout/card/components/CardContent';
import { Avatar } from '@/users/components/Avatar';
import { AvatarGroup } from '@/users/components/AvatarGroup';
import { TimelineCalendarEvent } from '~/generated-metadata/graphql';
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
import { isDefined } from '~/utils/isDefined';
type CalendarEventRowProps = {
@ -161,7 +160,7 @@ export const CalendarEventRow = ({
key={[participant.workspaceMemberId, participant.displayName]
.filter(isDefined)
.join('-')}
avatarUrl={participant.avatarUrl}
avatarUrl={getImageAbsoluteURIOrBase64(participant.avatarUrl)}
placeholder={
participant.firstName && participant.lastName
? `${participant.firstName} ${participant.lastName}`

View File

@ -1,12 +1,13 @@
import { Tooltip } from 'react-tooltip';
import styled from '@emotion/styled';
import { Avatar } from 'twenty-ui';
import { Comment } from '@/activities/types/Comment';
import { Avatar } from '@/users/components/Avatar';
import {
beautifyExactDateTime,
beautifyPastDateRelativeToNow,
} from '~/utils/date-utils';
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
const StyledContainer = styled.div`
align-items: center;
@ -75,7 +76,7 @@ export const CommentHeader = ({ comment, actionBar }: CommentHeaderProps) => {
<StyledContainer>
<StyledLeftContainer>
<Avatar
avatarUrl={avatarUrl}
avatarUrl={getImageAbsoluteURIOrBase64(avatarUrl)}
size="md"
entityId={author?.id}
placeholder={authorName}

View File

@ -1,10 +1,10 @@
import { useMemo } from 'react';
import styled from '@emotion/styled';
import { Chip, ChipVariant } from 'twenty-ui';
import { v4 } from 'uuid';
import { ActivityTargetWithTargetRecord } from '@/activities/types/ActivityTargetObject';
import { RecordChip } from '@/object-record/components/RecordChip';
import { Chip, ChipVariant } from '@/ui/display/chip/components/Chip';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { RGBA } from '@/ui/theme/constants/Rgba';

View File

@ -1,14 +1,15 @@
import { useTheme } from '@emotion/react';
import { useRecoilState } from 'recoil';
import { IconCheckbox, IconNotes } from 'twenty-ui';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import {
Chip,
ChipAccent,
ChipSize,
ChipVariant,
} from '@/ui/display/chip/components/Chip';
IconCheckbox,
IconNotes,
} from 'twenty-ui';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
type ActivityTypeDropdownProps = {
activityId: string;

View File

@ -1,9 +1,10 @@
import styled from '@emotion/styled';
import { Avatar } from 'twenty-ui';
import { getDisplayNameFromParticipant } from '@/activities/emails/utils/getDisplayNameFromParticipant';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { RecordChip } from '@/object-record/components/RecordChip';
import { Avatar } from '@/users/components/Avatar';
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
const StyledAvatar = styled(Avatar)`
margin-right: ${({ theme }) => theme.spacing(1)};
@ -67,7 +68,7 @@ export const ParticipantChip = ({
) : (
<StyledChip>
<StyledAvatar
avatarUrl={avatarUrl}
avatarUrl={getImageAbsoluteURIOrBase64(avatarUrl)}
type="rounded"
placeholder={displayName}
size="sm"

View File

@ -1,8 +1,8 @@
import styled from '@emotion/styled';
import { OverflowingTextWithTooltip } from 'twenty-ui';
import { EmailThreadMessageParticipant } from '@/activities/emails/types/EmailThreadMessageParticipant';
import { getDisplayNameFromParticipant } from '@/activities/emails/utils/getDisplayNameFromParticipant';
import { OverflowingTextWithTooltip } from '@/ui/display/tooltip/OverflowingTextWithTooltip';
type EmailThreadMessageReceiversProps = {
receivers: EmailThreadMessageParticipant[];

View File

@ -1,6 +1,7 @@
import { useRef } from 'react';
import styled from '@emotion/styled';
import { useRecoilCallback } from 'recoil';
import { Avatar } from 'twenty-ui';
import { EmailThreadNotShared } from '@/activities/emails/components/EmailThreadNotShared';
import { useEmailThread } from '@/activities/emails/hooks/useEmailThread';
@ -8,9 +9,9 @@ import { emailThreadIdWhenEmailThreadWasClosedState } from '@/activities/emails/
import { CardContent } from '@/ui/layout/card/components/CardContent';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { GRAY_SCALE } from '@/ui/theme/constants/GrayScale';
import { Avatar } from '@/users/components/Avatar';
import { TimelineThread } from '~/generated/graphql';
import { formatToHumanReadableDate } from '~/utils';
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
const StyledCardContent = styled(CardContent)<{ visibility: string }>`
align-items: center;
@ -152,20 +153,24 @@ export const EmailThreadPreview = ({
<StyledHeading unread={!thread.read}>
<StyledParticipantsContainer>
<Avatar
avatarUrl={thread?.firstParticipant?.avatarUrl}
avatarUrl={getImageAbsoluteURIOrBase64(
thread?.firstParticipant?.avatarUrl,
)}
placeholder={thread.firstParticipant.displayName}
type="rounded"
/>
{thread?.lastTwoParticipants?.[0] && (
<StyledAvatar
avatarUrl={thread.lastTwoParticipants[0].avatarUrl}
avatarUrl={getImageAbsoluteURIOrBase64(
thread.lastTwoParticipants[0].avatarUrl,
)}
placeholder={thread.lastTwoParticipants[0].displayName}
type="rounded"
/>
)}
{finalDisplayedName && (
<StyledAvatar
avatarUrl={finalAvatarUrl}
avatarUrl={getImageAbsoluteURIOrBase64(finalAvatarUrl)}
placeholder={finalDisplayedName}
type="rounded"
color={isCountIcon ? GRAY_SCALE.gray50 : undefined}

View File

@ -1,5 +1,6 @@
import { useApolloClient } from '@apollo/client';
import { useRecoilValue } from 'recoil';
import { Nullable } from 'twenty-ui';
import { Activity } from '@/activities/types/Activity';
import { ActivityTarget } from '@/activities/types/ActivityTarget';
@ -7,7 +8,6 @@ import { ActivityTargetWithTargetRecord } from '@/activities/types/ActivityTarge
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
import { Nullable } from '~/types/Nullable';
import { isDefined } from '~/utils/isDefined';
export const useActivityTargetObjectRecords = (activity: Activity) => {

View File

@ -1,13 +1,16 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconCalendar, IconComment } from 'twenty-ui';
import {
IconCalendar,
IconComment,
OverflowingTextWithTooltip,
} from 'twenty-ui';
import { ActivityTargetChips } from '@/activities/components/ActivityTargetChips';
import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTargetObjectRecords';
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
import { Activity } from '@/activities/types/Activity';
import { getActivitySummary } from '@/activities/utils/getActivitySummary';
import { OverflowingTextWithTooltip } from '@/ui/display/tooltip/OverflowingTextWithTooltip';
import { Checkbox, CheckboxShape } from '@/ui/input/components/Checkbox';
import { beautifyExactDate, hasDatePassed } from '~/utils/date-utils';

View File

@ -2,17 +2,17 @@ import { Tooltip } from 'react-tooltip';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { IconCheckbox, IconNotes } from 'twenty-ui';
import { Avatar, IconCheckbox, IconNotes } from 'twenty-ui';
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
import { timelineActivityWithoutTargetsFamilyState } from '@/activities/timeline/states/timelineActivityWithoutTargetsFamilyState';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { Avatar } from '@/users/components/Avatar';
import {
beautifyExactDateTime,
beautifyPastDateRelativeToNow,
} from '~/utils/date-utils';
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
const StyledAvatarContainer = styled.div`
align-items: center;
@ -170,7 +170,9 @@ export const TimelineActivity = ({
<StyledTimelineItemContainer>
<StyledAvatarContainer>
<Avatar
avatarUrl={activityForTimeline.author?.avatarUrl}
avatarUrl={getImageAbsoluteURIOrBase64(
activityForTimeline.author?.avatarUrl,
)}
placeholder={activityForTimeline.author?.name.firstName ?? ''}
size="sm"
type="rounded"

View File

@ -1,7 +1,7 @@
import styled from '@emotion/styled';
import { OverflowingTextWithTooltip } from 'twenty-ui';
import { ActivityType } from '@/activities/types/Activity';
import { OverflowingTextWithTooltip } from '@/ui/display/tooltip/OverflowingTextWithTooltip';
import { Checkbox, CheckboxShape } from '@/ui/input/components/Checkbox';
const StyledTitleContainer = styled.div`

View File

@ -1,6 +1,6 @@
import styled from '@emotion/styled';
import { getImageAbsoluteURIOrBase64 } from '@/users/utils/getProfilePictureAbsoluteURI';
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
type LogoProps = {
workspaceLogo?: string | null;

View File

@ -3,7 +3,7 @@ import styled from '@emotion/styled';
import { isNonEmptyString } from '@sniptt/guards';
import { useRecoilState, useRecoilValue } from 'recoil';
import { Key } from 'ts-key-enum';
import { IconNotes } from 'twenty-ui';
import { Avatar, IconNotes } from 'twenty-ui';
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
import { Activity } from '@/activities/types/Activity';
@ -21,7 +21,6 @@ import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { Avatar } from '@/users/components/Avatar';
import { getLogoUrlFromDomainName } from '~/utils';
import { isDefined } from '~/utils/isDefined';

View File

@ -1,11 +1,12 @@
import styled from '@emotion/styled';
import { Avatar } from 'twenty-ui';
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList';
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection';
import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle';
import { Avatar } from '@/users/components/Avatar';
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
import { useFavorites } from '../hooks/useFavorites';
@ -63,7 +64,7 @@ export const Favorites = () => {
Icon={() => (
<StyledAvatar
entityId={recordId}
avatarUrl={avatarUrl}
avatarUrl={getImageAbsoluteURIOrBase64(avatarUrl)}
type={avatarType}
placeholder={labelIdentifier}
className="fav-avatar"

View File

@ -1,6 +1,6 @@
import { gql } from '@apollo/client';
import { AvatarType } from 'twenty-ui';
import { AvatarType } from '@/users/components/Avatar';
import { ColorScheme } from '@/workspace-member/types/WorkspaceMember';
export const mockId = '8f3b2121-f194-4ba4-9fbf-2d5a37126806';

View File

@ -1,4 +1,4 @@
import { AvatarType } from '@/users/components/Avatar';
import { AvatarType } from 'twenty-ui';
export type Favorite = {
id: string;

View File

@ -11,7 +11,7 @@ import {
} from '@/ui/navigation/navigation-drawer/components/NavigationDrawer';
import { isNavigationDrawerOpenState } from '@/ui/navigation/states/isNavigationDrawerOpenState';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { getImageAbsoluteURIOrBase64 } from '@/users/utils/getProfilePictureAbsoluteURI';
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
import { useIsSettingsPage } from '../hooks/useIsSettingsPage';
import { currentMobileNavigationDrawerState } from '../states/currentMobileNavigationDrawerState';

View File

@ -1,9 +1,9 @@
import { renderHook } from '@testing-library/react';
import { Nullable } from 'twenty-ui';
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
import { Nullable } from '~/types/Nullable';
describe('useColumnDefinitionsFromFieldMetadata', () => {
it('should return empty definitions if no object is passed', () => {

View File

@ -1,10 +1,10 @@
import { useMemo } from 'react';
import { Nullable } from 'twenty-ui';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { filterAvailableTableColumns } from '@/object-record/utils/filterAvailableTableColumns';
import { Nullable } from '~/types/Nullable';
import { formatFieldMetadataItemAsColumnDefinition } from '../utils/formatFieldMetadataItemAsColumnDefinition';
import { formatFieldMetadataItemsAsFilterDefinitions } from '../utils/formatFieldMetadataItemsAsFilterDefinitions';

View File

@ -1,8 +1,8 @@
import * as React from 'react';
import { EntityChip } from 'twenty-ui';
import { useMapToObjectRecordIdentifier } from '@/object-metadata/hooks/useMapToObjectRecordIdentifier';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { EntityChip } from '@/ui/display/chip/components/EntityChip';
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
export type RecordChipProps = {
objectNameSingular: string;
@ -28,7 +28,9 @@ export const RecordChip = ({
entityId={record.id}
name={objectRecordIdentifier.name}
avatarType={objectRecordIdentifier.avatarType}
avatarUrl={objectRecordIdentifier.avatarUrl}
avatarUrl={
getImageAbsoluteURIOrBase64(objectRecordIdentifier.avatarUrl) || ''
}
linkToEntity={objectRecordIdentifier.linkToShowPage}
maxWidth={maxWidth}
className={className}

View File

@ -1,6 +1,6 @@
import { IconComponent } from 'twenty-ui';
import { EntityChip, IconComponent } from 'twenty-ui';
import { EntityChip } from '@/ui/display/chip/components/EntityChip';
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
import { Filter } from '../types/Filter';
@ -17,7 +17,7 @@ export const GenericEntityFilterChip = ({
entityId={filter.value}
name={filter.displayValue}
avatarType="rounded"
avatarUrl={filter.displayAvatarUrl}
avatarUrl={getImageAbsoluteURIOrBase64(filter.displayAvatarUrl) || ''}
LeftIcon={Icon}
/>
);

View File

@ -1,6 +1,7 @@
import { Nullable } from 'twenty-ui';
import { useDateField } from '@/object-record/record-field/meta-types/hooks/useDateField';
import { DateInput } from '@/ui/field/input/components/DateInput';
import { Nullable } from '~/types/Nullable';
import { usePersistField } from '../../../hooks/usePersistField';

View File

@ -1,5 +1,6 @@
import { Nullable } from 'twenty-ui';
import { DateInput } from '@/ui/field/input/components/DateInput';
import { Nullable } from '~/types/Nullable';
import { usePersistField } from '../../../hooks/usePersistField';
import { useDateTimeField } from '../../hooks/useDateTimeField';

View File

@ -1,5 +1,6 @@
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { Avatar } from 'twenty-ui';
import { v4 } from 'uuid';
import { MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID } from '@/object-record/relation-picker/constants/MultiObjectRecordSelectSelectableListId';
@ -7,7 +8,7 @@ import { ObjectRecordForSelect } from '@/object-record/relation-picker/hooks/use
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { MenuItemMultiSelectAvatar } from '@/ui/navigation/menu-item/components/MenuItemMultiSelectAvatar';
import { Avatar } from '@/users/components/Avatar';
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
export const StyledSelectableItem = styled(SelectableItem)`
height: 100%;
@ -42,7 +43,9 @@ export const MultipleObjectRecordSelectItem = ({
isKeySelected={isSelectedByKeyboard}
avatar={
<Avatar
avatarUrl={objectRecordForSelect.recordIdentifier.avatarUrl}
avatarUrl={getImageAbsoluteURIOrBase64(
objectRecordForSelect.recordIdentifier.avatarUrl,
)}
entityId={objectRecordForSelect.record.id}
placeholder={objectRecordForSelect.recordIdentifier.name}
size="md"

View File

@ -1,11 +1,12 @@
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { Avatar } from 'twenty-ui';
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { MenuItemSelectAvatar } from '@/ui/navigation/menu-item/components/MenuItemSelectAvatar';
import { Avatar } from '@/users/components/Avatar';
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
type SelectableMenuItemSelectProps = {
entity: EntityForSelect;
@ -39,7 +40,7 @@ export const SelectableMenuItemSelect = ({
hovered={isSelectedItemId}
avatar={
<Avatar
avatarUrl={entity.avatarUrl}
avatarUrl={getImageAbsoluteURIOrBase64(entity.avatarUrl)}
entityId={entity.id}
placeholder={entity.name}
size="md"

View File

@ -1,11 +1,12 @@
import { useEffect, useState } from 'react';
import { Avatar } from 'twenty-ui';
import { SelectableRecord } from '@/object-record/select/types/SelectableRecord';
import { DropdownMenuSkeletonItem } from '@/ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { MenuItemMultiSelectAvatar } from '@/ui/navigation/menu-item/components/MenuItemMultiSelectAvatar';
import { Avatar } from '@/users/components/Avatar';
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
export const MultipleRecordSelectDropdown = ({
recordsToSelect,
@ -68,7 +69,7 @@ export const MultipleRecordSelectDropdown = ({
}
avatar={
<Avatar
avatarUrl={record.avatarUrl}
avatarUrl={getImageAbsoluteURIOrBase64(record.avatarUrl)}
entityId={record.id}
placeholder={record.name}
size="md"

View File

@ -1,4 +1,4 @@
import { AvatarType } from '@/users/components/Avatar';
import { AvatarType } from 'twenty-ui';
export type SelectableRecord = {
id: string;

View File

@ -1,6 +1,7 @@
import { Nullable } from 'twenty-ui';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { ObjectRecordEdge } from '@/object-record/types/ObjectRecordEdge';
import { Nullable } from '~/types/Nullable';
export type ObjectRecordConnection<T extends ObjectRecord = ObjectRecord> = {
__typename?: string;

View File

@ -1,4 +1,4 @@
import { AvatarType } from '@/users/components/Avatar';
import { AvatarType } from 'twenty-ui';
export type ObjectRecordIdentifier = {
id: string;

View File

@ -1,7 +1,6 @@
import {
EntityChip,
EntityChipVariant,
} from '@/ui/display/chip/components/EntityChip';
import { EntityChip, EntityChipVariant } from 'twenty-ui';
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
export type PersonChipProps = {
id: string;
@ -21,7 +20,7 @@ export const PersonChip = ({
linkToEntity={`/person/${id}`}
name={name}
avatarType="rounded"
avatarUrl={avatarUrl}
avatarUrl={getImageAbsoluteURIOrBase64(avatarUrl) || ''}
variant={variant}
/>
);

View File

@ -2,7 +2,7 @@ import { ReactNode, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useIcons } from 'twenty-ui';
import { Nullable, useIcons } from 'twenty-ui';
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
@ -11,7 +11,6 @@ import { FieldIdentifierType } from '@/settings/data-model/types/FieldIdentifier
import { isFieldTypeSupportedInSettings } from '@/settings/data-model/utils/isFieldTypeSupportedInSettings';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { Nullable } from '~/types/Nullable';
import { RELATION_TYPES } from '../../constants/RelationTypes';

View File

@ -5,8 +5,8 @@ import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMembe
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { ImageInput } from '@/ui/input/components/ImageInput';
import { getImageAbsoluteURIOrBase64 } from '@/users/utils/getProfilePictureAbsoluteURI';
import { useUploadProfilePictureMutation } from '~/generated/graphql';
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
import { isDefined } from '~/utils/isDefined';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';

View File

@ -2,11 +2,11 @@ import { useRecoilState } from 'recoil';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { ImageInput } from '@/ui/input/components/ImageInput';
import { getImageAbsoluteURIOrBase64 } from '@/users/utils/getProfilePictureAbsoluteURI';
import {
useUpdateWorkspaceMutation,
useUploadWorkspaceLogoMutation,
} from '~/generated/graphql';
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
export const WorkspaceLogoUploader = () => {

View File

@ -9,11 +9,11 @@ import {
size,
useFloating,
} from '@floating-ui/react';
import { AppTooltip } from 'twenty-ui';
import { ReadonlyDeep } from 'type-fest';
import { useDebouncedCallback } from 'use-debounce';
import { SelectOption } from '@/spreadsheet-import/types';
import { AppTooltip } from '@/ui/display/tooltip/AppTooltip';
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';

View File

@ -2,9 +2,9 @@
import { Column } from 'react-data-grid';
import { createPortal } from 'react-dom';
import styled from '@emotion/styled';
import { AppTooltip } from 'twenty-ui';
import { Fields } from '@/spreadsheet-import/types';
import { AppTooltip } from '@/ui/display/tooltip/AppTooltip';
const StyledHeaderContainer = styled.div`
align-items: center;

View File

@ -2,10 +2,10 @@
import { Column, useRowSelection } from 'react-data-grid';
import { createPortal } from 'react-dom';
import styled from '@emotion/styled';
import { AppTooltip } from 'twenty-ui';
import { MatchColumnSelect } from '@/spreadsheet-import/components/MatchColumnSelect';
import { Data, Fields } from '@/spreadsheet-import/types';
import { AppTooltip } from '@/ui/display/tooltip/AppTooltip';
import { Checkbox, CheckboxVariant } from '@/ui/input/components/Checkbox';
import { TextInput } from '@/ui/input/components/TextInput';
import { Toggle } from '@/ui/input/components/Toggle';

View File

@ -1,40 +0,0 @@
import { useTheme } from '@emotion/react';
import { motion } from 'framer-motion';
export type AnimatedCheckmarkProps = React.ComponentProps<
typeof motion.path
> & {
isAnimating?: boolean;
color?: string;
duration?: number;
size?: number;
};
export const AnimatedCheckmark = ({
isAnimating = false,
color,
duration = 0.5,
size = 28,
}: AnimatedCheckmarkProps) => {
const theme = useTheme();
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 52 52"
width={size}
height={size}
>
<motion.path
fill="none"
stroke={color ?? theme.grayScale.gray0}
strokeWidth={4}
d="M14 27l7.8 7.8L38 14"
pathLength="1"
strokeDasharray="1"
strokeDashoffset={isAnimating ? '1' : '0'}
animate={{ strokeDashoffset: isAnimating ? '0' : '1' }}
transition={{ duration }}
/>
</svg>
);
};

View File

@ -1,28 +0,0 @@
import React from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconCheck } from 'twenty-ui';
const StyledContainer = styled.div`
align-items: center;
background-color: ${({ theme }) => theme.color.blue};
border-radius: 50%;
display: flex;
height: 20px;
justify-content: center;
width: 20px;
`;
export type CheckmarkProps = React.ComponentPropsWithoutRef<'div'> & {
className?: string;
};
export const Checkmark = ({ className }: CheckmarkProps) => {
const theme = useTheme();
return (
<StyledContainer className={className}>
<IconCheck color={theme.grayScale.gray0} size={14} />
</StyledContainer>
);
};

View File

@ -1,20 +0,0 @@
import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { Checkmark } from '../Checkmark';
const meta: Meta<typeof Checkmark> = {
title: 'UI/Display/Checkmark/Checkmark',
component: Checkmark,
decorators: [ComponentDecorator],
};
export default meta;
type Story = StoryObj<typeof Checkmark>;
export const Default: Story = { args: {} };
export const WithCustomStyles: Story = {
args: { style: { backgroundColor: 'red', height: 40, width: 40 } },
};

View File

@ -1,178 +0,0 @@
import { MouseEvent, ReactNode } from 'react';
import { css } from '@emotion/react';
import styled from '@emotion/styled';
import { OverflowingTextWithTooltip } from '../../tooltip/OverflowingTextWithTooltip';
export enum ChipSize {
Large = 'large',
Small = 'small',
}
export enum ChipAccent {
TextPrimary = 'text-primary',
TextSecondary = 'text-secondary',
}
export enum ChipVariant {
Highlighted = 'highlighted',
Regular = 'regular',
Transparent = 'transparent',
Rounded = 'rounded',
}
type ChipProps = {
size?: ChipSize;
disabled?: boolean;
clickable?: boolean;
label: string;
maxWidth?: number;
variant?: ChipVariant;
accent?: ChipAccent;
leftComponent?: ReactNode;
rightComponent?: ReactNode;
className?: string;
onClick?: (event: MouseEvent<HTMLDivElement>) => void;
};
const StyledContainer = styled.div<
Pick<
ChipProps,
'accent' | 'clickable' | 'disabled' | 'maxWidth' | 'size' | 'variant'
>
>`
--chip-horizontal-padding: ${({ theme }) => theme.spacing(1)};
--chip-vertical-padding: ${({ theme }) => theme.spacing(1)};
align-items: center;
border-radius: ${({ theme }) => theme.border.radius.sm};
color: ${({ theme, disabled }) =>
disabled ? theme.font.color.light : theme.font.color.secondary};
cursor: ${({ clickable, disabled }) =>
clickable ? 'pointer' : disabled ? 'not-allowed' : 'inherit'};
display: inline-flex;
gap: ${({ theme }) => theme.spacing(1)};
height: ${({ theme }) => theme.spacing(3)};
max-width: ${({ maxWidth }) =>
maxWidth
? `calc(${maxWidth}px - 2 * var(--chip-horizontal-padding))`
: '200px'};
overflow: hidden;
padding: var(--chip-vertical-padding) var(--chip-horizontal-padding);
user-select: none;
// Accent style overrides
${({ accent, disabled, theme }) => {
if (accent === ChipAccent.TextPrimary) {
return (
!disabled &&
css`
color: ${theme.font.color.primary};
`
);
}
if (accent === ChipAccent.TextSecondary) {
return css`
font-weight: ${theme.font.weight.medium};
`;
}
}}
// Size style overrides
${({ theme, size }) =>
size === ChipSize.Large &&
css`
height: ${theme.spacing(4)};
`}
// Variant style overrides
${({ disabled, theme, variant }) => {
if (variant === ChipVariant.Regular) {
return (
!disabled &&
css`
:hover {
background-color: ${theme.background.transparent.light};
}
:active {
background-color: ${theme.background.transparent.medium};
}
`
);
}
if (variant === ChipVariant.Highlighted) {
return css`
background-color: ${theme.background.transparent.light};
${!disabled &&
css`
:hover {
background-color: ${theme.background.transparent.medium};
}
:active {
background-color: ${theme.background.transparent.strong};
}
`}
`;
}
if (variant === ChipVariant.Rounded) {
return css`
--chip-horizontal-padding: ${theme.spacing(2)};
--chip-vertical-padding: 3px;
background-color: ${theme.background.transparent.lighter};
border: 1px solid ${theme.border.color.medium};
border-radius: 50px;
`;
}
if (variant === ChipVariant.Transparent) {
return css`
cursor: inherit;
`;
}
}}
`;
const StyledLabel = styled.span`
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`;
export const Chip = ({
size = ChipSize.Small,
label,
disabled = false,
clickable = true,
variant = ChipVariant.Regular,
leftComponent,
rightComponent,
accent = ChipAccent.TextPrimary,
maxWidth,
className,
onClick,
}: ChipProps) => (
<StyledContainer
data-testid="chip"
clickable={clickable}
variant={variant}
accent={accent}
size={size}
disabled={disabled}
className={className}
maxWidth={maxWidth}
onClick={onClick}
>
{leftComponent}
<StyledLabel>
<OverflowingTextWithTooltip text={label} />
</StyledLabel>
{rightComponent}
</StyledContainer>
);

View File

@ -1,81 +0,0 @@
import * as React from 'react';
import { useNavigate } from 'react-router-dom';
import { useTheme } from '@emotion/react';
import { isNonEmptyString } from '@sniptt/guards';
import { IconComponent } from 'twenty-ui';
import { Avatar, AvatarType } from '@/users/components/Avatar';
import { Nullable } from '~/types/Nullable';
import { Chip, ChipVariant } from './Chip';
export type EntityChipProps = {
linkToEntity?: string;
entityId: string;
name: string;
avatarUrl?: string;
avatarType?: Nullable<AvatarType>;
variant?: EntityChipVariant;
LeftIcon?: IconComponent;
className?: string;
maxWidth?: number;
};
export enum EntityChipVariant {
Regular = 'regular',
Transparent = 'transparent',
}
export const EntityChip = ({
linkToEntity,
entityId,
name,
avatarUrl,
avatarType = 'rounded',
variant = EntityChipVariant.Regular,
LeftIcon,
className,
maxWidth,
}: EntityChipProps) => {
const navigate = useNavigate();
const theme = useTheme();
const handleLinkClick = (event: React.MouseEvent<HTMLDivElement>) => {
if (isNonEmptyString(linkToEntity)) {
event.preventDefault();
event.stopPropagation();
navigate(linkToEntity);
}
};
return (
<Chip
label={name}
variant={
linkToEntity
? variant === EntityChipVariant.Regular
? ChipVariant.Highlighted
: ChipVariant.Regular
: ChipVariant.Transparent
}
leftComponent={
LeftIcon ? (
<LeftIcon size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />
) : (
<Avatar
avatarUrl={avatarUrl}
entityId={entityId}
placeholder={name}
size="sm"
type={avatarType}
/>
)
}
clickable={!!linkToEntity}
onClick={handleLinkClick}
className={className}
maxWidth={maxWidth}
/>
);
};

View File

@ -1,69 +0,0 @@
import { Meta, StoryObj } from '@storybook/react';
import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { CatalogStory } from '~/testing/types';
import { Chip, ChipAccent, ChipSize, ChipVariant } from '../Chip';
const meta: Meta<typeof Chip> = {
title: 'UI/Display/Chip/Chip',
component: Chip,
};
export default meta;
type Story = StoryObj<typeof Chip>;
export const Default: Story = {
args: {
label: 'Chip test',
size: ChipSize.Small,
variant: ChipVariant.Highlighted,
accent: ChipAccent.TextPrimary,
disabled: false,
clickable: true,
maxWidth: 200,
},
decorators: [ComponentDecorator],
};
export const Catalog: CatalogStory<Story, typeof Chip> = {
args: { clickable: true, label: 'Hello' },
argTypes: {
size: { control: false },
variant: { control: false },
disabled: { control: false },
className: { control: false },
rightComponent: { control: false },
leftComponent: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'], active: ['.active'] },
catalog: {
dimensions: [
{
name: 'states',
values: ['default', 'hover', 'active', 'disabled'],
props: (state: string) =>
state === 'default' ? {} : { className: state },
},
{
name: 'variants',
values: Object.values(ChipVariant),
props: (variant: ChipVariant) => ({ variant }),
},
{
name: 'sizes',
values: Object.values(ChipSize),
props: (size: ChipSize) => ({ size }),
},
{
name: 'accents',
values: Object.values(ChipAccent),
props: (accent: ChipAccent) => ({ accent }),
},
],
},
},
decorators: [CatalogDecorator],
};

View File

@ -1,21 +0,0 @@
import { Meta, StoryObj } from '@storybook/react';
import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator';
import { EntityChip } from '../EntityChip';
const meta: Meta<typeof EntityChip> = {
title: 'UI/Display/Chip/EntityChip',
component: EntityChip,
decorators: [ComponentWithRouterDecorator],
args: {
name: 'Entity name',
linkToEntity: '/entity-link',
avatarType: 'squared',
},
};
export default meta;
type Story = StoryObj<typeof EntityChip>;
export const Default: Story = {};

View File

@ -1,8 +1,8 @@
import React, { ReactElement, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import styled from '@emotion/styled';
import { Chip, ChipVariant } from 'twenty-ui';
import { Chip, ChipVariant } from '@/ui/display/chip/components/Chip';
import { IntersectionObserverWrapper } from '@/ui/display/expandable-list/IntersectionObserverWrapper';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';

View File

@ -1,12 +1,10 @@
import { Meta, StoryObj } from '@storybook/react';
import { CatalogDecorator, CatalogStory, ComponentDecorator } from 'twenty-ui';
import { Info, InfoAccent } from '@/ui/display/info/components/Info.tsx';
import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator.tsx';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator.tsx';
import { CatalogStory } from '~/testing/types.ts';
import { Info, InfoAccent } from '@/ui/display/info/components/Info';
const meta: Meta<typeof Info> = {
title: 'UI/Display/Info/Info',
title: 'UI/Display/Info',
component: Info,
};

View File

@ -1,73 +0,0 @@
import { PlacesType, PositionStrategy, Tooltip } from 'react-tooltip';
import styled from '@emotion/styled';
import { RGBA } from '@/ui/theme/constants/Rgba';
export enum TooltipPosition {
Top = 'top',
Left = 'left',
Right = 'right',
Bottom = 'bottom',
}
const StyledAppTooltip = styled(Tooltip)`
backdrop-filter: ${({ theme }) => theme.blur.strong};
background-color: ${({ theme }) => RGBA(theme.color.gray80, 0.8)};
border-radius: ${({ theme }) => theme.border.radius.sm};
box-shadow: ${({ theme }) => theme.boxShadow.light};
color: ${({ theme }) => theme.grayScale.gray0};
font-size: ${({ theme }) => theme.font.size.sm};
font-weight: ${({ theme }) => theme.font.weight.regular};
max-width: 40%;
overflow: visible;
padding: ${({ theme }) => theme.spacing(2)};
word-break: break-word;
z-index: ${({ theme }) => theme.lastLayerZIndex};
`;
export type AppTooltipProps = {
className?: string;
anchorSelect?: string;
content?: string;
children?: React.ReactNode;
delayHide?: number;
offset?: number;
noArrow?: boolean;
isOpen?: boolean;
place?: PlacesType;
positionStrategy?: PositionStrategy;
};
export const AppTooltip = ({
anchorSelect,
className,
content,
delayHide,
isOpen,
noArrow,
offset,
place,
positionStrategy,
children,
}: AppTooltipProps) => (
<StyledAppTooltip
{...{
anchorSelect,
className,
content,
delayHide,
isOpen,
noArrow,
offset,
place,
positionStrategy,
children,
}}
/>
);

View File

@ -1,84 +0,0 @@
import { useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import styled from '@emotion/styled';
import { v4 as uuidV4 } from 'uuid';
import { AppTooltip } from './AppTooltip';
const StyledOverflowingText = styled.div<{ cursorPointer: boolean }>`
cursor: ${({ cursorPointer }) => (cursorPointer ? 'pointer' : 'inherit')};
font-family: inherit;
font-size: inherit;
font-weight: inherit;
max-width: 100%;
overflow: hidden;
text-decoration: inherit;
text-overflow: ellipsis;
white-space: nowrap;
`;
export const OverflowingTextWithTooltip = ({
text,
className,
mutliline,
}: {
text: string | null | undefined;
className?: string;
mutliline?: boolean;
}) => {
const textElementId = `title-id-${uuidV4()}`;
const textRef = useRef<HTMLDivElement>(null);
const [isTitleOverflowing, setIsTitleOverflowing] = useState(false);
useEffect(() => {
const isOverflowing =
(text?.length ?? 0) > 0 && textRef.current
? textRef.current?.scrollHeight > textRef.current?.clientHeight ||
textRef.current.scrollWidth > textRef.current.clientWidth
: false;
if (isTitleOverflowing !== isOverflowing) {
setIsTitleOverflowing(isOverflowing);
}
}, [isTitleOverflowing, text]);
const handleTooltipClick = (event: React.MouseEvent<HTMLDivElement>) => {
event.stopPropagation();
event.preventDefault();
};
return (
<>
<StyledOverflowingText
data-testid="tooltip"
className={className}
ref={textRef}
id={textElementId}
cursorPointer={isTitleOverflowing}
>
{text}
</StyledOverflowingText>
{isTitleOverflowing &&
createPortal(
<div onClick={handleTooltipClick}>
<AppTooltip
anchorSelect={`#${textElementId}`}
content={mutliline ? undefined : text ?? ''}
delayHide={0}
offset={5}
noArrow
place="bottom"
positionStrategy="absolute"
>
{mutliline ? <pre>{text}</pre> : ''}
</AppTooltip>
</div>,
document.body,
)}
</>
);
};

View File

@ -1,29 +0,0 @@
import { Meta, StoryObj } from '@storybook/react';
import { userEvent, within } from '@storybook/test';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { OverflowingTextWithTooltip } from '../OverflowingTextWithTooltip';
const placeholderText =
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi tellus diam, rhoncus nec consequat quis, dapibus quis massa. Praesent tincidunt augue at ex bibendum, non finibus augue faucibus. In at gravida orci. Nulla facilisi. Proin ut augue ut nisi pellentesque tristique. Proin sodales libero id turpis tincidunt posuere.';
const meta: Meta<typeof OverflowingTextWithTooltip> = {
title: 'UI/Display/Tooltip/OverflowingTextWithTooltip',
component: OverflowingTextWithTooltip,
};
export default meta;
type Story = StoryObj<typeof OverflowingTextWithTooltip>;
export const Default: Story = {
args: {
text: placeholderText,
},
decorators: [ComponentDecorator],
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const tooltip = await canvas.findByTestId('tooltip');
userEvent.hover(tooltip);
},
};

View File

@ -1,82 +0,0 @@
import { Meta, StoryObj } from '@storybook/react';
import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { CatalogStory } from '~/testing/types';
import { AppTooltip as Tooltip, TooltipPosition } from '../AppTooltip';
const meta: Meta<typeof Tooltip> = {
title: 'UI/Display/Tooltip/Tooltip',
component: Tooltip,
};
export default meta;
type Story = StoryObj<typeof Tooltip>;
export const Default: Story = {
args: {
place: TooltipPosition.Bottom,
content: 'Tooltip Test',
isOpen: true,
anchorSelect: '#hover-text',
},
decorators: [ComponentDecorator],
render: ({
anchorSelect,
className,
content,
delayHide,
isOpen,
noArrow,
offset,
place,
positionStrategy,
}) => (
<>
<p id="hover-text" data-testid="tooltip">
Hover me!
</p>
<Tooltip
{...{
anchorSelect,
className,
content,
delayHide,
isOpen,
noArrow,
offset,
place,
positionStrategy,
}}
/>
</>
),
};
export const Catalog: CatalogStory<Story, typeof Tooltip> = {
args: { isOpen: true, content: 'Tooltip Test' },
play: async ({ canvasElement }) => {
Object.values(TooltipPosition).forEach((position) => {
const element = canvasElement.querySelector(
`#${position}`,
) as HTMLElement;
element.style.margin = '75px';
});
},
parameters: {
catalog: {
dimensions: [
{
name: 'anchorSelect',
values: Object.values(TooltipPosition),
props: (anchorSelect: TooltipPosition) => ({
anchorSelect: `#${anchorSelect}`,
place: anchorSelect,
}),
},
],
},
},
decorators: [CatalogDecorator],
};

View File

@ -1,4 +1,4 @@
import { OverflowingTextWithTooltip } from '../../../display/tooltip/OverflowingTextWithTooltip';
import { OverflowingTextWithTooltip } from 'twenty-ui';
import { EllipsisDisplay } from './EllipsisDisplay';

View File

@ -2,11 +2,11 @@ import { useRef, useState } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { flip, offset, useFloating } from '@floating-ui/react';
import { Nullable } from 'twenty-ui';
import { useRegisterInputEvents } from '@/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents';
import { DateDisplay } from '@/ui/field/display/components/DateDisplay';
import { InternalDatePicker } from '@/ui/input/components/internal/date/components/InternalDatePicker';
import { Nullable } from '~/types/Nullable';
const StyledCalendarContainer = styled.div`
background: ${({ theme }) => theme.background.secondary};

View File

@ -6,8 +6,8 @@ import {
motion,
useAnimation,
} from 'framer-motion';
import { Checkmark } from 'twenty-ui';
import { Checkmark } from '@/ui/display/checkmark/components/Checkmark';
import { ColorScheme } from '@/workspace-member/types/WorkspaceMember';
const StyledColorSchemeBackground = styled.div<

View File

@ -3,14 +3,13 @@ import styled from '@emotion/styled';
import { Decorator, Meta, StoryObj } from '@storybook/react';
import { expect, userEvent, waitFor, within } from '@storybook/test';
import { PlayFunction } from '@storybook/types';
import { Avatar, ComponentDecorator } from 'twenty-ui';
import { Button } from '@/ui/input/button/components/Button';
import { DropdownMenuSkeletonItem } from '@/ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { MenuItemMultiSelectAvatar } from '@/ui/navigation/menu-item/components/MenuItemMultiSelectAvatar';
import { MenuItemSelectAvatar } from '@/ui/navigation/menu-item/components/MenuItemSelectAvatar';
import { Avatar } from '@/users/components/Avatar';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { Dropdown } from '../Dropdown';
import { DropdownMenuHeader } from '../DropdownMenuHeader';

View File

@ -4,9 +4,12 @@ import { useNavigate } from 'react-router-dom';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { IconChevronLeft, IconComponent } from 'twenty-ui';
import {
IconChevronLeft,
IconComponent,
OverflowingTextWithTooltip,
} from 'twenty-ui';
import { OverflowingTextWithTooltip } from '@/ui/display/tooltip/OverflowingTextWithTooltip';
import { IconButton } from '@/ui/input/button/components/IconButton';
import { NavigationDrawerCollapseButton } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerCollapseButton';
import { isNavigationDrawerOpenState } from '@/ui/navigation/states/isNavigationDrawerOpenState';

View File

@ -1,9 +1,9 @@
import { ChangeEvent, ReactNode, useRef } from 'react';
import { Tooltip } from 'react-tooltip';
import styled from '@emotion/styled';
import { Avatar, AvatarType } from 'twenty-ui';
import { v4 as uuidV4 } from 'uuid';
import { Avatar, AvatarType } from '@/users/components/Avatar';
import {
beautifyExactDateTime,
beautifyPastDateRelativeToNow,

View File

@ -1,8 +1,7 @@
import * as React from 'react';
import { Link as ReactLink } from 'react-router-dom';
import styled from '@emotion/styled';
import { Chip, ChipSize, ChipVariant } from '@/ui/display/chip/components/Chip';
import { Chip, ChipSize, ChipVariant } from 'twenty-ui';
type RoundedLinkProps = {
href: string;

View File

@ -1,7 +1,7 @@
import { ReactNode } from 'react';
import styled from '@emotion/styled';
import { OverflowingTextWithTooltip } from 'twenty-ui';
import { OverflowingTextWithTooltip } from '@/ui/display/tooltip/OverflowingTextWithTooltip';
import { Checkbox } from '@/ui/input/components/Checkbox';
import {

View File

@ -1,8 +1,6 @@
import { ReactNode } from 'react';
import { useTheme } from '@emotion/react';
import { IconCheck } from 'twenty-ui';
import { OverflowingTextWithTooltip } from '@/ui/display/tooltip/OverflowingTextWithTooltip';
import { IconCheck, OverflowingTextWithTooltip } from 'twenty-ui';
import {
StyledMenuItemLabel,

View File

@ -1,14 +1,14 @@
import { Meta, StoryObj } from '@storybook/react';
import { Avatar } from '@/users/components/Avatar';
import {
Avatar,
CatalogDecorator,
CatalogDimension,
CatalogOptions,
} from '~/testing/decorators/CatalogDecorator';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
CatalogStory,
ComponentDecorator,
} from 'twenty-ui';
import { avatarUrl } from '~/testing/mock-data/users';
import { CatalogStory } from '~/testing/types';
import { MenuItemMultiSelectAvatar } from '../MenuItemMultiSelectAvatar';

View File

@ -1,14 +1,14 @@
import { Meta, StoryObj } from '@storybook/react';
import { Avatar } from '@/users/components/Avatar';
import {
Avatar,
CatalogDecorator,
CatalogDimension,
CatalogOptions,
} from '~/testing/decorators/CatalogDecorator';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
CatalogStory,
ComponentDecorator,
} from 'twenty-ui';
import { avatarUrl } from '~/testing/mock-data/users';
import { CatalogStory } from '~/testing/types';
import { MenuItemSelectAvatar } from '../MenuItemSelectAvatar';

View File

@ -1,7 +1,9 @@
import { useTheme } from '@emotion/react';
import { IconComponent, IconGripVertical } from 'twenty-ui';
import { OverflowingTextWithTooltip } from '@/ui/display/tooltip/OverflowingTextWithTooltip';
import {
IconComponent,
IconGripVertical,
OverflowingTextWithTooltip,
} from 'twenty-ui';
import {
StyledMenuItemLabel,

View File

@ -1,8 +1,8 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { motion } from 'framer-motion';
import { AnimatedCheckmark } from 'twenty-ui';
import { AnimatedCheckmark } from '@/ui/display/checkmark/components/AnimatedCheckmark';
import { MOBILE_VIEWPORT } from '@/ui/theme/constants/MobileViewport';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';

View File

@ -1,126 +0,0 @@
import { useEffect, useState } from 'react';
import styled from '@emotion/styled';
import { isNonEmptyString } from '@sniptt/guards';
import { Nullable } from '~/types/Nullable';
import { stringToHslColor } from '~/utils/string-to-hsl';
import { getImageAbsoluteURIOrBase64 } from '../utils/getProfilePictureAbsoluteURI';
export type AvatarType = 'squared' | 'rounded';
export type AvatarSize = 'xl' | 'lg' | 'md' | 'sm' | 'xs';
export type AvatarProps = {
avatarUrl?: string | null;
className?: string;
size?: AvatarSize;
placeholder: string | undefined;
entityId?: string;
type?: Nullable<AvatarType>;
color?: string;
backgroundColor?: string;
onClick?: () => void;
};
const propertiesBySize = {
xl: {
fontSize: '16px',
width: '40px',
},
lg: {
fontSize: '13px',
width: '24px',
},
md: {
fontSize: '12px',
width: '16px',
},
sm: {
fontSize: '10px',
width: '14px',
},
xs: {
fontSize: '8px',
width: '12px',
},
};
export const StyledAvatar = styled.div<
AvatarProps & { color: string; backgroundColor: string }
>`
align-items: center;
background-color: ${({ backgroundColor }) => backgroundColor};
${({ avatarUrl }) =>
isNonEmptyString(avatarUrl) ? `background-image: url(${avatarUrl});` : ''}
background-position: center;
background-size: cover;
border-radius: ${(props) => (props.type === 'rounded' ? '50%' : '2px')};
color: ${({ color }) => color};
cursor: ${({ onClick }) => (onClick ? 'pointer' : 'default')};
display: flex;
flex-shrink: 0;
font-size: ${({ size = 'md' }) => propertiesBySize[size].fontSize};
font-weight: ${({ theme }) => theme.font.weight.medium};
height: ${({ size = 'md' }) => propertiesBySize[size].width};
justify-content: center;
width: ${({ size = 'md' }) => propertiesBySize[size].width};
&:hover {
box-shadow: ${({ theme, onClick }) =>
onClick ? '0 0 0 4px ' + theme.background.transparent.light : 'unset'};
}
`;
export const Avatar = ({
avatarUrl,
className,
size = 'md',
placeholder,
entityId = placeholder,
onClick,
type = 'squared',
color,
backgroundColor,
}: AvatarProps) => {
const noAvatarUrl = !isNonEmptyString(avatarUrl);
const [isInvalidAvatarUrl, setIsInvalidAvatarUrl] = useState(false);
useEffect(() => {
if (isNonEmptyString(avatarUrl)) {
new Promise((resolve) => {
const img = new Image();
img.onload = () => resolve(false);
img.onerror = () => resolve(true);
img.src = getImageAbsoluteURIOrBase64(avatarUrl) as string;
}).then((res) => {
setIsInvalidAvatarUrl(res as boolean);
});
}
}, [avatarUrl]);
const fixedColor = color ?? stringToHslColor(entityId ?? '', 75, 25);
const fixedBackgroundColor =
backgroundColor ??
(!isNonEmptyString(avatarUrl)
? stringToHslColor(entityId ?? '', 75, 85)
: 'none');
return (
<StyledAvatar
className={className}
avatarUrl={getImageAbsoluteURIOrBase64(avatarUrl)}
placeholder={placeholder}
size={size}
type={type}
entityId={entityId}
onClick={onClick}
color={fixedColor}
backgroundColor={fixedBackgroundColor}
>
{(noAvatarUrl || isInvalidAvatarUrl) &&
placeholder?.[0]?.toLocaleUpperCase()}
</StyledAvatar>
);
};

View File

@ -1,29 +0,0 @@
import { ReactNode } from 'react';
import styled from '@emotion/styled';
export type AvatarGroupProps = {
avatars: ReactNode[];
};
const StyledContainer = styled.div`
align-items: center;
display: flex;
`;
const StyledItemContainer = styled.div`
margin-right: -3px;
`;
const MAX_AVATARS_NB = 4;
export const AvatarGroup = ({ avatars }: AvatarGroupProps) => {
if (!avatars.length) return null;
return (
<StyledContainer>
{avatars.slice(0, MAX_AVATARS_NB).map((avatar, index) => (
<StyledItemContainer key={index}>{avatar}</StyledItemContainer>
))}
</StyledContainer>
);
};

View File

@ -1,4 +1,6 @@
import { EntityChip } from '@/ui/display/chip/components/EntityChip';
import { EntityChip } from 'twenty-ui';
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
export type UserChipProps = {
id: string;
@ -11,6 +13,6 @@ export const UserChip = ({ id, name, avatarUrl }: UserChipProps) => (
entityId={id}
name={name}
avatarType="rounded"
avatarUrl={avatarUrl}
avatarUrl={getImageAbsoluteURIOrBase64(avatarUrl) || ''}
/>
);

View File

@ -1,33 +0,0 @@
import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { avatarUrl } from '~/testing/mock-data/users';
import { Avatar } from '../Avatar';
const meta: Meta<typeof Avatar> = {
title: 'Modules/Users/Avatar',
component: Avatar,
decorators: [ComponentDecorator],
args: { avatarUrl, size: 'md', placeholder: 'L', type: 'rounded' },
};
export default meta;
type Story = StoryObj<typeof Avatar>;
export const Rounded: Story = {};
export const Squared: Story = {
args: { type: 'squared' },
};
export const NoAvatarPictureRounded: Story = {
args: { avatarUrl: '' },
};
export const NoAvatarPictureSquared: Story = {
args: {
...NoAvatarPictureRounded.args,
...Squared.args,
},
};

View File

@ -1,68 +0,0 @@
import { Meta, StoryObj } from '@storybook/react';
import {
Avatar,
AvatarProps,
AvatarSize,
AvatarType,
} from '@/users/components/Avatar';
import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { avatarUrl } from '~/testing/mock-data/users';
import { AvatarGroup, AvatarGroupProps } from '../AvatarGroup';
const makeAvatar = (userName: string, props: Partial<AvatarProps> = {}) => (
// eslint-disable-next-line react/jsx-props-no-spreading
<Avatar placeholder={userName} entityId={userName} {...props} />
);
const getAvatars = (commonProps: Partial<AvatarProps> = {}) => [
makeAvatar('Matthew', { avatarUrl, ...commonProps }),
makeAvatar('Sophie', commonProps),
makeAvatar('Jane', commonProps),
makeAvatar('Lily', commonProps),
makeAvatar('John', commonProps),
];
const meta: Meta<
AvatarGroupProps & AvatarProps & { numberOfAvatars?: number }
> = {
title: 'Modules/Users/AvatarGroup',
component: AvatarGroup,
render: ({ numberOfAvatars = 5, ...args }) => (
<AvatarGroup avatars={getAvatars(args).slice(0, numberOfAvatars)} />
),
};
export default meta;
type Story = StoryObj<typeof AvatarGroup>;
export const Default: Story = {
decorators: [ComponentDecorator],
};
export const Catalog: Story = {
parameters: {
catalog: {
dimensions: [
{
name: 'number of avatars',
values: [1, 2, 3, 4, 5],
props: (numberOfAvatars: number) => ({ numberOfAvatars }),
},
{
name: 'types',
values: ['rounded', 'squared'] as AvatarType[],
props: (type: AvatarType) => ({ type }),
},
{
name: 'sizes',
values: ['xs', 'sm', 'md', 'lg', 'xl'] as AvatarSize[],
props: (size: AvatarSize) => ({ size }),
},
],
},
},
decorators: [CatalogDecorator],
};

View File

@ -1,19 +0,0 @@
import { REACT_APP_SERVER_BASE_URL } from '~/config';
export const getImageAbsoluteURIOrBase64 = (imageUrl?: string | null) => {
if (!imageUrl) {
return null;
}
if (imageUrl?.startsWith('data:')) {
return imageUrl;
}
if (imageUrl?.startsWith('https:')) {
return imageUrl;
}
const serverFilesUrl = REACT_APP_SERVER_BASE_URL;
return `${serverFilesUrl}/files/${imageUrl}`;
};

View File

@ -5,11 +5,16 @@ import {
OnDragEndResponder,
ResponderProvided,
} from '@hello-pangea/dnd';
import { IconInfoCircle, IconMinus, IconPlus, useIcons } from 'twenty-ui';
import {
AppTooltip,
IconInfoCircle,
IconMinus,
IconPlus,
useIcons,
} from 'twenty-ui';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { AppTooltip } from '@/ui/display/tooltip/AppTooltip';
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';

View File

@ -1,8 +1,8 @@
import styled from '@emotion/styled';
import { Avatar, OverflowingTextWithTooltip } from 'twenty-ui';
import { OverflowingTextWithTooltip } from '@/ui/display/tooltip/OverflowingTextWithTooltip';
import { Avatar } from '@/users/components/Avatar';
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
const StyledContainer = styled.div`
background: ${({ theme }) => theme.background.secondary};
@ -39,7 +39,7 @@ export const WorkspaceMemberCard = ({
}: WorkspaceMemberCardProps) => (
<StyledContainer>
<Avatar
avatarUrl={workspaceMember.avatarUrl}
avatarUrl={getImageAbsoluteURIOrBase64(workspaceMember.avatarUrl)}
entityId={workspaceMember.id}
placeholder={workspaceMember.name.firstName || ''}
type="squared"