Improve performance of demo workspace - Rename getImageAbsoluteURIOrBase64 function (#6282)

### Description

1. This PR is a continuation of a previous PR:
https://github.com/twentyhq/twenty/pull/6201#pullrequestreview-2175601222

2. One test case was removed here:
`packages/twenty-front/src/utils/image/__tests__/getImageAbsoluteURI.test.ts`
because since we are not handling base64 images anymore, the result is
the same of the last test case. Would you rather we update the test
instead?


### Refs

- #3514
- https://github.com/twentyhq/twenty/pull/6201

### Demo


https://www.loom.com/share/4f32b535c77a4d418e319b095d09452c?sid=df34adf8-b013-44ef-b794-d54846f52d2d

Fixes #3514

---------

Co-authored-by: gitstart-twenty <gitstart-twenty@users.noreply.github.com>
This commit is contained in:
gitstart-app[bot]
2024-07-29 14:07:21 +02:00
committed by GitHub
parent 00fea17920
commit fed12ddfcd
31 changed files with 124 additions and 97 deletions

View File

@ -9,7 +9,7 @@ import { MainText } from 'src/components/MainText';
import { Title } from 'src/components/Title'; import { Title } from 'src/components/Title';
import { WhatIsTwenty } from 'src/components/WhatIsTwenty'; import { WhatIsTwenty } from 'src/components/WhatIsTwenty';
import { capitalize } from 'src/utils/capitalize'; import { capitalize } from 'src/utils/capitalize';
import { getImageAbsoluteURIOrBase64 } from 'src/utils/getImageAbsoluteURIOrBase64'; import { getImageAbsoluteURI } from 'src/utils/getImageAbsoluteURI';
type SendInviteLinkEmailProps = { type SendInviteLinkEmailProps = {
link: string; link: string;
@ -27,7 +27,7 @@ export const SendInviteLinkEmail = ({
sender, sender,
serverUrl, serverUrl,
}: SendInviteLinkEmailProps) => { }: SendInviteLinkEmailProps) => {
const workspaceLogo = getImageAbsoluteURIOrBase64(workspace.logo, serverUrl); const workspaceLogo = getImageAbsoluteURI(workspace.logo, serverUrl);
return ( return (
<BaseEmail width={333}> <BaseEmail width={333}>
<Title value="Join your team on Twenty" /> <Title value="Join your team on Twenty" />

View File

@ -1,4 +1,4 @@
export const getImageAbsoluteURIOrBase64 = ( export const getImageAbsoluteURI = (
imageUrl?: string | null, imageUrl?: string | null,
serverUrl?: string, serverUrl?: string,
) => { ) => {
@ -6,7 +6,7 @@ export const getImageAbsoluteURIOrBase64 = (
return null; return null;
} }
if (imageUrl?.startsWith('data:') || imageUrl?.startsWith('https:')) { if (imageUrl?.startsWith('https:')) {
return imageUrl; return imageUrl;
} }

View File

@ -16,7 +16,6 @@ import { Card } from '@/ui/layout/card/components/Card';
import { CardContent } from '@/ui/layout/card/components/CardContent'; import { CardContent } from '@/ui/layout/card/components/CardContent';
import { TimelineCalendarEvent } from '~/generated-metadata/graphql'; import { TimelineCalendarEvent } from '~/generated-metadata/graphql';
import { CalendarChannelVisibility } from '~/generated/graphql'; import { CalendarChannelVisibility } from '~/generated/graphql';
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
type CalendarEventRowProps = { type CalendarEventRowProps = {
@ -163,7 +162,7 @@ export const CalendarEventRow = ({
key={[participant.workspaceMemberId, participant.displayName] key={[participant.workspaceMemberId, participant.displayName]
.filter(isDefined) .filter(isDefined)
.join('-')} .join('-')}
avatarUrl={getImageAbsoluteURIOrBase64(participant.avatarUrl)} avatarUrl={participant.avatarUrl}
placeholder={ placeholder={
participant.firstName && participant.lastName participant.firstName && participant.lastName
? `${participant.firstName} ${participant.lastName}` ? `${participant.firstName} ${participant.lastName}`

View File

@ -6,7 +6,6 @@ import {
beautifyExactDateTime, beautifyExactDateTime,
beautifyPastDateRelativeToNow, beautifyPastDateRelativeToNow,
} from '~/utils/date-utils'; } from '~/utils/date-utils';
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
const StyledContainer = styled.div` const StyledContainer = styled.div`
align-items: center; align-items: center;
@ -60,7 +59,7 @@ export const CommentHeader = ({ comment, actionBar }: CommentHeaderProps) => {
<StyledContainer> <StyledContainer>
<StyledLeftContainer> <StyledLeftContainer>
<Avatar <Avatar
avatarUrl={getImageAbsoluteURIOrBase64(avatarUrl)} avatarUrl={avatarUrl}
size="md" size="md"
placeholderColorSeed={author?.id} placeholderColorSeed={author?.id}
placeholder={authorName} placeholder={authorName}

View File

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

View File

@ -10,7 +10,6 @@ import { CardContent } from '@/ui/layout/card/components/CardContent';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer'; import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { MessageChannelVisibility, TimelineThread } from '~/generated/graphql'; import { MessageChannelVisibility, TimelineThread } from '~/generated/graphql';
import { formatToHumanReadableDate } from '~/utils/date-utils'; import { formatToHumanReadableDate } from '~/utils/date-utils';
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
const StyledCardContent = styled(CardContent)<{ const StyledCardContent = styled(CardContent)<{
visibility: MessageChannelVisibility; visibility: MessageChannelVisibility;
@ -154,24 +153,20 @@ export const EmailThreadPreview = ({
<StyledHeading unread={!thread.read}> <StyledHeading unread={!thread.read}>
<StyledParticipantsContainer> <StyledParticipantsContainer>
<Avatar <Avatar
avatarUrl={getImageAbsoluteURIOrBase64( avatarUrl={thread?.firstParticipant?.avatarUrl}
thread?.firstParticipant?.avatarUrl,
)}
placeholder={thread.firstParticipant.displayName} placeholder={thread.firstParticipant.displayName}
type="rounded" type="rounded"
/> />
{thread?.lastTwoParticipants?.[0] && ( {thread?.lastTwoParticipants?.[0] && (
<StyledAvatar <StyledAvatar
avatarUrl={getImageAbsoluteURIOrBase64( avatarUrl={thread.lastTwoParticipants[0].avatarUrl}
thread.lastTwoParticipants[0].avatarUrl,
)}
placeholder={thread.lastTwoParticipants[0].displayName} placeholder={thread.lastTwoParticipants[0].displayName}
type="rounded" type="rounded"
/> />
)} )}
{finalDisplayedName && ( {finalDisplayedName && (
<StyledAvatar <StyledAvatar
avatarUrl={getImageAbsoluteURIOrBase64(finalAvatarUrl)} avatarUrl={finalAvatarUrl}
placeholder={finalDisplayedName} placeholder={finalDisplayedName}
type="rounded" type="rounded"
color={isCountIcon ? GRAY_SCALE.gray50 : undefined} color={isCountIcon ? GRAY_SCALE.gray50 : undefined}

View File

@ -11,7 +11,6 @@ import {
beautifyExactDateTime, beautifyExactDateTime,
beautifyPastDateRelativeToNow, beautifyPastDateRelativeToNow,
} from '~/utils/date-utils'; } from '~/utils/date-utils';
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
const StyledAvatarContainer = styled.div` const StyledAvatarContainer = styled.div`
align-items: center; align-items: center;
@ -154,9 +153,7 @@ export const TimelineActivity = ({
<StyledTimelineItemContainer> <StyledTimelineItemContainer>
<StyledAvatarContainer> <StyledAvatarContainer>
<Avatar <Avatar
avatarUrl={getImageAbsoluteURIOrBase64( avatarUrl={activityForTimeline.author?.avatarUrl}
activityForTimeline.author?.avatarUrl,
)}
placeholder={activityForTimeline.author?.name.firstName ?? ''} placeholder={activityForTimeline.author?.name.firstName ?? ''}
size="sm" size="sm"
type="rounded" type="rounded"

View File

@ -1,6 +1,6 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64'; import { getImageAbsoluteURI } from '~/utils/image/getImageAbsoluteURI';
type LogoProps = { type LogoProps = {
workspaceLogo?: string | null; workspaceLogo?: string | null;
@ -58,7 +58,7 @@ export const Logo = ({ workspaceLogo }: LogoProps) => {
return ( return (
<StyledContainer> <StyledContainer>
<StyledMainLogo logo={getImageAbsoluteURIOrBase64(workspaceLogo)} /> <StyledMainLogo logo={getImageAbsoluteURI(workspaceLogo)} />
<StyledTwentyLogoContainer> <StyledTwentyLogoContainer>
<StyledTwentyLogo src="/icons/android/android-launchericon-192-192.png" /> <StyledTwentyLogo src="/icons/android/android-launchericon-192-192.png" />
</StyledTwentyLogoContainer> </StyledTwentyLogoContainer>

View File

@ -10,7 +10,6 @@ import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/componen
import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection'; import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection';
import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle'; import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle';
import { useNavigationSection } from '@/ui/navigation/navigation-drawer/hooks/useNavigationSection'; import { useNavigationSection } from '@/ui/navigation/navigation-drawer/hooks/useNavigationSection';
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
import { useFavorites } from '../hooks/useFavorites'; import { useFavorites } from '../hooks/useFavorites';
@ -81,7 +80,7 @@ export const Favorites = () => {
Icon={() => ( Icon={() => (
<StyledAvatar <StyledAvatar
placeholderColorSeed={recordId} placeholderColorSeed={recordId}
avatarUrl={getImageAbsoluteURIOrBase64(avatarUrl)} avatarUrl={avatarUrl}
type={avatarType} type={avatarType}
placeholder={labelIdentifier} placeholder={labelIdentifier}
className="fav-avatar" className="fav-avatar"

View File

@ -11,7 +11,7 @@ import {
} from '@/ui/navigation/navigation-drawer/components/NavigationDrawer'; } from '@/ui/navigation/navigation-drawer/components/NavigationDrawer';
import { isNavigationDrawerOpenState } from '@/ui/navigation/states/isNavigationDrawerOpenState'; import { isNavigationDrawerOpenState } from '@/ui/navigation/states/isNavigationDrawerOpenState';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64'; import { getImageAbsoluteURI } from '~/utils/image/getImageAbsoluteURI';
import { useIsSettingsPage } from '../hooks/useIsSettingsPage'; import { useIsSettingsPage } from '../hooks/useIsSettingsPage';
import { currentMobileNavigationDrawerState } from '../states/currentMobileNavigationDrawerState'; import { currentMobileNavigationDrawerState } from '../states/currentMobileNavigationDrawerState';
@ -49,7 +49,7 @@ export const AppNavigationDrawer = ({
: { : {
logo: logo:
(currentWorkspace?.logo && (currentWorkspace?.logo &&
getImageAbsoluteURIOrBase64(currentWorkspace.logo)) ?? getImageAbsoluteURI(currentWorkspace.logo)) ??
undefined, undefined,
title: currentWorkspace?.displayName ?? undefined, title: currentWorkspace?.displayName ?? undefined,
children: <MainNavigationDrawerItems />, children: <MainNavigationDrawerItems />,

View File

@ -2,7 +2,7 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { getLogoUrlFromDomainName } from '~/utils'; import { getLogoUrlFromDomainName } from '~/utils';
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64'; import { getImageAbsoluteURI } from '~/utils/image/getImageAbsoluteURI';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
import { getImageIdentifierFieldValue } from './getImageIdentifierFieldValue'; import { getImageIdentifierFieldValue } from './getImageIdentifierFieldValue';
@ -21,7 +21,7 @@ export const getAvatarUrl = (
} }
if (objectNameSingular === CoreObjectNameSingular.Person) { if (objectNameSingular === CoreObjectNameSingular.Person) {
return getImageAbsoluteURIOrBase64(record.avatarUrl) ?? ''; return getImageAbsoluteURI(record.avatarUrl) ?? '';
} }
const imageIdentifierFieldValue = getImageIdentifierFieldValue( const imageIdentifierFieldValue = getImageIdentifierFieldValue(

View File

@ -1,7 +1,5 @@
import { AvatarChip, IconComponent } from 'twenty-ui'; import { AvatarChip, IconComponent } from 'twenty-ui';
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
import { Filter } from '../types/Filter'; import { Filter } from '../types/Filter';
type GenericEntityFilterChipProps = { type GenericEntityFilterChipProps = {
@ -17,7 +15,7 @@ export const GenericEntityFilterChip = ({
placeholderColorSeed={filter.value} placeholderColorSeed={filter.value}
name={filter.displayValue} name={filter.displayValue}
avatarType="rounded" avatarType="rounded"
avatarUrl={getImageAbsoluteURIOrBase64(filter.displayAvatarUrl) || ''} avatarUrl={filter.displayAvatarUrl}
LeftIcon={Icon} LeftIcon={Icon}
/> />
); );

View File

@ -10,7 +10,6 @@ import { SelectableItem } from '@/ui/layout/selectable-list/components/Selectabl
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { MenuItemMultiSelectAvatar } from '@/ui/navigation/menu-item/components/MenuItemMultiSelectAvatar'; import { MenuItemMultiSelectAvatar } from '@/ui/navigation/menu-item/components/MenuItemMultiSelectAvatar';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId'; import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
export const StyledSelectableItem = styled(SelectableItem)` export const StyledSelectableItem = styled(SelectableItem)`
@ -65,7 +64,7 @@ export const MultipleObjectRecordSelectItem = ({
selected={selected} selected={selected}
avatar={ avatar={
<Avatar <Avatar
avatarUrl={getImageAbsoluteURIOrBase64(recordIdentifier.avatarUrl)} avatarUrl={recordIdentifier.avatarUrl}
placeholderColorSeed={objectRecordId} placeholderColorSeed={objectRecordId}
placeholder={recordIdentifier.name} placeholder={recordIdentifier.name}
size="md" size="md"

View File

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

View File

@ -6,7 +6,6 @@ import { DropdownMenuSkeletonItem } from '@/ui/input/relation-picker/components/
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { MenuItemMultiSelectAvatar } from '@/ui/navigation/menu-item/components/MenuItemMultiSelectAvatar'; import { MenuItemMultiSelectAvatar } from '@/ui/navigation/menu-item/components/MenuItemMultiSelectAvatar';
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
export const MultipleRecordSelectDropdown = ({ export const MultipleRecordSelectDropdown = ({
recordsToSelect, recordsToSelect,
@ -69,7 +68,7 @@ export const MultipleRecordSelectDropdown = ({
} }
avatar={ avatar={
<Avatar <Avatar
avatarUrl={getImageAbsoluteURIOrBase64(record.avatarUrl)} avatarUrl={record.avatarUrl}
placeholderColorSeed={record.id} placeholderColorSeed={record.id}
placeholder={record.name} placeholder={record.name}
size="md" size="md"

View File

@ -6,7 +6,6 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { ImageInput } from '@/ui/input/components/ImageInput'; import { ImageInput } from '@/ui/input/components/ImageInput';
import { useUploadProfilePictureMutation } from '~/generated/graphql'; import { useUploadProfilePictureMutation } from '~/generated/graphql';
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
@ -101,7 +100,7 @@ export const ProfilePictureUploader = () => {
return ( return (
<ImageInput <ImageInput
picture={getImageAbsoluteURIOrBase64(currentWorkspaceMember?.avatarUrl)} picture={currentWorkspaceMember?.avatarUrl}
onUpload={handleUpload} onUpload={handleUpload}
onRemove={handleRemove} onRemove={handleRemove}
onAbort={handleAbort} onAbort={handleAbort}

View File

@ -6,7 +6,6 @@ import {
useUpdateWorkspaceMutation, useUpdateWorkspaceMutation,
useUploadWorkspaceLogoMutation, useUploadWorkspaceLogoMutation,
} from '~/generated/graphql'; } from '~/generated/graphql';
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
export const WorkspaceLogoUploader = () => { export const WorkspaceLogoUploader = () => {
@ -57,7 +56,7 @@ export const WorkspaceLogoUploader = () => {
return ( return (
<ImageInput <ImageInput
picture={getImageAbsoluteURIOrBase64(currentWorkspace?.logo)} picture={currentWorkspace?.logo}
onUpload={onUpload} onUpload={onUpload}
onRemove={onRemove} onRemove={onRemove}
/> />

View File

@ -1,9 +1,10 @@
import React from 'react';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import React, { useMemo } from 'react';
import { IconFileUpload, IconTrash, IconUpload, IconX } from 'twenty-ui'; import { IconFileUpload, IconTrash, IconUpload, IconX } from 'twenty-ui';
import { Button } from '@/ui/input/button/components/Button'; import { Button } from '@/ui/input/button/components/Button';
import { getImageAbsoluteURI } from '~/utils/image/getImageAbsoluteURI';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
const StyledContainer = styled.div` const StyledContainer = styled.div`
@ -105,16 +106,18 @@ export const ImageInput = ({
hiddenFileInput.current?.click(); hiddenFileInput.current?.click();
}; };
const pictureURI = useMemo(() => getImageAbsoluteURI(picture), [picture]);
return ( return (
<StyledContainer className={className}> <StyledContainer className={className}>
<StyledPicture <StyledPicture
withPicture={!!picture} withPicture={!!pictureURI}
disabled={disabled} disabled={disabled}
onClick={onUploadButtonClick} onClick={onUploadButtonClick}
> >
{picture ? ( {pictureURI ? (
<img <img
src={picture || '/images/default-profile-picture.png'} src={pictureURI || '/images/default-profile-picture.png'}
alt="profile" alt="profile"
/> />
) : ( ) : (
@ -139,7 +142,7 @@ export const ImageInput = ({
onClick={onAbort} onClick={onAbort}
variant="secondary" variant="secondary"
title="Abort" title="Abort"
disabled={!picture || disabled} disabled={!pictureURI || disabled}
fullWidth fullWidth
/> />
) : ( ) : (
@ -157,7 +160,7 @@ export const ImageInput = ({
onClick={onRemove} onClick={onRemove}
variant="secondary" variant="secondary"
title="Remove" title="Remove"
disabled={!picture || disabled} disabled={!pictureURI || disabled}
fullWidth fullWidth
/> />
</StyledButtonContainer> </StyledButtonContainer>

View File

@ -14,7 +14,7 @@ import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/consta
import { MULTI_WORKSPACE_DROPDOWN_ID } from '@/ui/navigation/navigation-drawer/constants/MulitWorkspaceDropdownId'; import { MULTI_WORKSPACE_DROPDOWN_ID } from '@/ui/navigation/navigation-drawer/constants/MulitWorkspaceDropdownId';
import { useWorkspaceSwitching } from '@/ui/navigation/navigation-drawer/hooks/useWorkspaceSwitching'; import { useWorkspaceSwitching } from '@/ui/navigation/navigation-drawer/hooks/useWorkspaceSwitching';
import { NavigationDrawerHotKeyScope } from '@/ui/navigation/navigation-drawer/types/NavigationDrawerHotKeyScope'; import { NavigationDrawerHotKeyScope } from '@/ui/navigation/navigation-drawer/types/NavigationDrawerHotKeyScope';
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64'; import { getImageAbsoluteURI } from '~/utils/image/getImageAbsoluteURI';
const StyledLogo = styled.div<{ logo: string }>` const StyledLogo = styled.div<{ logo: string }>`
background: url(${({ logo }) => logo}); background: url(${({ logo }) => logo});
@ -88,7 +88,7 @@ export const MultiWorkspaceDropdownButton = ({
<StyledContainer> <StyledContainer>
<StyledLogo <StyledLogo
logo={ logo={
getImageAbsoluteURIOrBase64( getImageAbsoluteURI(
currentWorkspace?.logo === null currentWorkspace?.logo === null
? DEFAULT_WORKSPACE_LOGO ? DEFAULT_WORKSPACE_LOGO
: currentWorkspace?.logo, : currentWorkspace?.logo,
@ -111,7 +111,7 @@ export const MultiWorkspaceDropdownButton = ({
avatar={ avatar={
<StyledLogo <StyledLogo
logo={ logo={
getImageAbsoluteURIOrBase64( getImageAbsoluteURI(
workspace.logo === null workspace.logo === null
? DEFAULT_WORKSPACE_LOGO ? DEFAULT_WORKSPACE_LOGO
: workspace.logo, : workspace.logo,

View File

@ -1,2 +1,2 @@
export const DEFAULT_WORKSPACE_LOGO = export const DEFAULT_WORKSPACE_LOGO =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAA7EAAAOxAGVKw4bAAACb0lEQVR4nO2VO4taQRTHr3AblbjxEVlwCwVhg7BoqqCIjy/gAyyFWNlYBOxsfH0KuxgQGwXRUkGuL2S7i1barGAgiwbdW93SnGOc4BonPiKahf3DwXFmuP/fPM4ZlvmlTxAhCBdzHnEQWYiv7Mr4C3NeuVYhQYDPzOUUQgDLBQGcLHNhvQK8DACPx8PTxiqVyvISG43GbyaT6Qfpn06n0m63e/tPAPF4vJ1MJu8kEsnWTCkWi1yr1RKGw+GDRqPBOTfr44vFQvD7/Q/lcpmaaVQAr9fLp1IpO22c47hGOBz+MB6PH+Vy+VYDAL8qlUoGtVotzOfzq4MAgsHgE/6KojiQyWR/bKVSqbSszHFM8Pl8z1YK48JsNltCOBwOnrYLO+8AAIjb+nHbycoTiUQfDJ7tFq4YAHiVSmXBxcD41u8flQU8z7fhzO0r83atVns3Go3u9Xr9x0O/RQXo9/tsIBBg6vX606a52Wz+bZ7P5/WwG29gxSJzhKgA6XTaDoFNF+krFAocmC//4yWEcSf2wTm7mCO19xFgSsKOLI16vV7b7XY7mRNoLwA0JymJ5uQIzgIAuX5PzDElT2m+E8BqtQ4ymcx7Yq7T6a6ZE4sKgOadTucaCwkxp1UzlEKh0GDxIXOwDWHAdi6Xe3swQDQa/Q7mywoolUpvsaptymazDWKxmBHTlWXZm405BFZoNpuGgwEmk4mE2SGtVivii4f1AO7J3ZopkQCQj7Ar1FeRChCJRJzVapX6DKNIfSc1Ax+wtQWQ55h6bH8FWDfYV4fO3wlwDr0C/BcADYiTPCxHqIEA2QsCZAkAKnRGkMbKN/sTX5YHPQ1e7SkAAAAASUVORK5CYII='; 'https://twentyhq.github.io/placeholder-images/workspaces/twenty-logo.png';

View File

@ -1,7 +1,5 @@
import { AvatarChip } from 'twenty-ui'; import { AvatarChip } from 'twenty-ui';
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
export type UserChipProps = { export type UserChipProps = {
id: string; id: string;
name: string; name: string;
@ -13,6 +11,6 @@ export const UserChip = ({ id, name, avatarUrl }: UserChipProps) => (
placeholderColorSeed={id} placeholderColorSeed={id}
name={name} name={name}
avatarType="rounded" avatarType="rounded"
avatarUrl={getImageAbsoluteURIOrBase64(avatarUrl) || ''} avatarUrl={avatarUrl}
/> />
); );

View File

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

View File

@ -0,0 +1,21 @@
import { getImageAbsoluteURI } from '../getImageAbsoluteURI';
describe('getImageAbsoluteURI', () => {
it('should return null if imageUrl is null', () => {
const imageUrl = null;
const result = getImageAbsoluteURI(imageUrl);
expect(result).toBeNull();
});
it('should return absolute url if the imageUrl is an absolute url', () => {
const imageUrl = 'https://XXX';
const result = getImageAbsoluteURI(imageUrl);
expect(result).toBe(imageUrl);
});
it('should return fully formed url if imageUrl is a relative url', () => {
const imageUrl = 'XXX';
const result = getImageAbsoluteURI(imageUrl);
expect(result).toBe('http://localhost:3000/files/XXX');
});
});

View File

@ -1,27 +0,0 @@
import { getImageAbsoluteURIOrBase64 } from '../getImageAbsoluteURIOrBase64';
describe('getImageAbsoluteURIOrBase64', () => {
it('should return null if imageUrl is null', () => {
const imageUrl = null;
const result = getImageAbsoluteURIOrBase64(imageUrl);
expect(result).toBeNull();
});
it('should return base64 encoded string if prefixed with data', () => {
const imageUrl = 'data:XXX';
const result = getImageAbsoluteURIOrBase64(imageUrl);
expect(result).toBe(imageUrl);
});
it('should return absolute url if the imageUrl is an absolute url', () => {
const imageUrl = 'https://XXX';
const result = getImageAbsoluteURIOrBase64(imageUrl);
expect(result).toBe(imageUrl);
});
it('should return fully formed url if imageUrl is a relative url', () => {
const imageUrl = 'XXX';
const result = getImageAbsoluteURIOrBase64(imageUrl);
expect(result).toBe('http://localhost:3000/files/XXX');
});
});

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
import { styled } from '@linaria/react'; import { styled } from '@linaria/react';
import { isNonEmptyString, isUndefined } from '@sniptt/guards'; import { isNonEmptyString, isUndefined } from '@sniptt/guards';
import { useContext } from 'react'; import { useContext, useMemo } from 'react';
import { useRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
import { invalidAvatarUrlsState } from '@ui/display/avatar/components/states/isInvalidAvatarUrlState'; import { invalidAvatarUrlsState } from '@ui/display/avatar/components/states/isInvalidAvatarUrlState';
@ -8,7 +8,7 @@ import { AVATAR_PROPERTIES_BY_SIZE } from '@ui/display/avatar/constants/AvatarPr
import { AvatarSize } from '@ui/display/avatar/types/AvatarSize'; import { AvatarSize } from '@ui/display/avatar/types/AvatarSize';
import { AvatarType } from '@ui/display/avatar/types/AvatarType'; import { AvatarType } from '@ui/display/avatar/types/AvatarType';
import { ThemeContext } from '@ui/theme'; import { ThemeContext } from '@ui/theme';
import { Nullable, stringToHslColor } from '@ui/utilities'; import { Nullable, getImageAbsoluteURI, stringToHslColor } from '@ui/utilities';
const StyledAvatar = styled.div<{ const StyledAvatar = styled.div<{
size: AvatarSize; size: AvatarSize;
@ -73,15 +73,21 @@ export const Avatar = ({
invalidAvatarUrlsState, invalidAvatarUrlsState,
); );
const noAvatarUrl = !isNonEmptyString(avatarUrl); const avatarImageURI = useMemo(
() => getImageAbsoluteURI(avatarUrl),
[avatarUrl],
);
const noAvatarUrl = !isNonEmptyString(avatarImageURI);
const placeholderChar = placeholder?.[0]?.toLocaleUpperCase(); const placeholderChar = placeholder?.[0]?.toLocaleUpperCase();
const showPlaceholder = noAvatarUrl || invalidAvatarUrls.includes(avatarUrl); const showPlaceholder =
noAvatarUrl || invalidAvatarUrls.includes(avatarImageURI);
const handleImageError = () => { const handleImageError = () => {
if (isNonEmptyString(avatarUrl)) { if (isNonEmptyString(avatarImageURI)) {
setInvalidAvatarUrls((prev) => [...prev, avatarUrl]); setInvalidAvatarUrls((prev) => [...prev, avatarImageURI]);
} }
}; };
@ -105,7 +111,7 @@ export const Avatar = ({
{showPlaceholder ? ( {showPlaceholder ? (
placeholderChar placeholderChar
) : ( ) : (
<StyledImage src={avatarUrl} onError={handleImageError} alt="" /> <StyledImage src={avatarImageURI} onError={handleImageError} alt="" />
)} )}
</StyledAvatar> </StyledAvatar>
); );

View File

@ -0,0 +1,30 @@
declare global {
interface Window {
_env_?: Record<string, string>;
__APOLLO_CLIENT__?: any;
}
}
const getDefaultUrl = () => {
if (
window.location.hostname === 'localhost' ||
window.location.hostname === '127.0.0.1'
) {
// In development environment front and backend usually run on separate ports
// we set the default value to localhost:3000.
// It dev context, we use env vars to overwrite it
return 'http://localhost:3000';
} else {
// Outside of localhost we assume that they run on the same port
// because the backend will serve the frontend
// It prod context, we use env-config.js + window var to ovewrite it
return `${window.location.protocol}//${window.location.hostname}${
window.location.port ? `:${window.location.port}` : ''
}`;
}
};
export const REACT_APP_SERVER_BASE_URL =
window._env_?.REACT_APP_SERVER_BASE_URL ||
process.env.REACT_APP_SERVER_BASE_URL ||
getDefaultUrl();

View File

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

View File

@ -1,4 +1,5 @@
export * from './color/utils/stringToHslColor'; export * from './color/utils/stringToHslColor';
export * from './image/getImageAbsoluteURI';
export * from './isDefined'; export * from './isDefined';
export * from './state/utils/createState'; export * from './state/utils/createState';
export * from './types/Nullable'; export * from './types/Nullable';