Decouple twenty-ui Avatar from app server-URL config (#21968)
Makes `twenty-ui`'s `Avatar` render the `avatarUrl` it receives instead of building it from `window._env_`/`window.location` at module load, so the library no longer depends on the app environment. URL resolution moves to `twenty-front` via a `getAbsoluteImageUrl` helper applied at the call sites. Part of making twenty-ui a standalone library.
This commit is contained in:
+2
-1
@@ -3,6 +3,7 @@ import { isTimelineCalendarEventParticipant } from '@/activities/calendar/types/
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Avatar, AvatarGroup } from 'twenty-ui/data-display';
|
||||
import { type TimelineCalendarEventParticipant } from '~/generated/graphql';
|
||||
import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
|
||||
|
||||
type CalendarEventParticipantsAvatarGroupProps = {
|
||||
participants: CalendarEventParticipant[] | TimelineCalendarEventParticipant[];
|
||||
@@ -51,7 +52,7 @@ export const CalendarEventParticipantsAvatarGroup = ({
|
||||
key={[participant.workspaceMemberId, participant.displayName]
|
||||
.filter(isDefined)
|
||||
.join('-')}
|
||||
avatarUrl={participant.avatarUrl}
|
||||
avatarUrl={getAbsoluteImageUrl(participant.avatarUrl)}
|
||||
placeholder={
|
||||
participant.firstName && participant.lastName
|
||||
? `${participant.firstName} ${participant.lastName}`
|
||||
|
||||
@@ -4,6 +4,7 @@ import React from 'react';
|
||||
import { getDisplayNameFromParticipant } from '@/activities/emails/utils/getDisplayNameFromParticipant';
|
||||
import { CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { RecordChip } from '@/object-record/components/RecordChip';
|
||||
import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
|
||||
import { Avatar } from 'twenty-ui/data-display';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
@@ -69,7 +70,7 @@ export const ParticipantChip = ({
|
||||
<StyledChip>
|
||||
<StyledAvatarContainer>
|
||||
<Avatar
|
||||
avatarUrl={avatarUrl}
|
||||
avatarUrl={getAbsoluteImageUrl(avatarUrl)}
|
||||
type="rounded"
|
||||
placeholder={displayName}
|
||||
size="sm"
|
||||
|
||||
+6
-3
@@ -14,6 +14,7 @@ import {
|
||||
type TimelineThread,
|
||||
} from '~/generated/graphql';
|
||||
import { formatToHumanReadableDate } from '~/utils/date-utils';
|
||||
import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
|
||||
|
||||
const StyledHeading = styled.div<{ unread: boolean }>`
|
||||
display: flex;
|
||||
@@ -118,7 +119,7 @@ export const EmailThreadPreview = ({ thread }: EmailThreadPreviewProps) => {
|
||||
<StyledHeading unread={!thread.read}>
|
||||
<StyledParticipantsContainer>
|
||||
<Avatar
|
||||
avatarUrl={thread?.firstParticipant?.avatarUrl}
|
||||
avatarUrl={getAbsoluteImageUrl(thread?.firstParticipant?.avatarUrl)}
|
||||
placeholder={thread.firstParticipant.displayName}
|
||||
placeholderColorSeed={
|
||||
thread.firstParticipant.workspaceMemberId ||
|
||||
@@ -129,7 +130,9 @@ export const EmailThreadPreview = ({ thread }: EmailThreadPreviewProps) => {
|
||||
{isDefined(thread?.lastTwoParticipants?.[0]) && (
|
||||
<StyledAvatarWrapper>
|
||||
<Avatar
|
||||
avatarUrl={thread.lastTwoParticipants[0].avatarUrl}
|
||||
avatarUrl={getAbsoluteImageUrl(
|
||||
thread.lastTwoParticipants[0].avatarUrl,
|
||||
)}
|
||||
placeholder={thread.lastTwoParticipants[0].displayName}
|
||||
placeholderColorSeed={
|
||||
thread.lastTwoParticipants[0].workspaceMemberId ||
|
||||
@@ -142,7 +145,7 @@ export const EmailThreadPreview = ({ thread }: EmailThreadPreviewProps) => {
|
||||
{finalDisplayedName && (
|
||||
<StyledAvatarWrapper>
|
||||
<Avatar
|
||||
avatarUrl={finalAvatarUrl}
|
||||
avatarUrl={getAbsoluteImageUrl(finalAvatarUrl)}
|
||||
placeholder={finalDisplayedName}
|
||||
type="rounded"
|
||||
color={isCountIcon ? theme.grayScale.gray11 : undefined}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useApplicationChipData } from '@/applications/hooks/useApplicationChipData';
|
||||
import { styled } from '@linaria/react';
|
||||
import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
|
||||
import { Avatar, type AvatarSize } from 'twenty-ui/data-display';
|
||||
import { OverflowingTextWithTooltip } from 'twenty-ui/surfaces';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
@@ -44,7 +45,7 @@ export const AppChip = ({
|
||||
<Avatar
|
||||
type="app"
|
||||
size={size}
|
||||
avatarUrl={applicationChipData.logo}
|
||||
avatarUrl={getAbsoluteImageUrl(applicationChipData.logo)}
|
||||
placeholder={applicationChipData.name}
|
||||
placeholderColorSeed={applicationChipData.seed}
|
||||
color={applicationChipData.colors?.color}
|
||||
|
||||
+4
-3
@@ -34,6 +34,7 @@ import {
|
||||
GetWorkspaceCreationDefaultsDocument,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
|
||||
import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
|
||||
|
||||
const StyledWorkspaceContainer = styled.div`
|
||||
background-color: ${themeCssVariables.background.secondary};
|
||||
@@ -172,9 +173,9 @@ export const SignInUpGlobalScopeForm = () => {
|
||||
<StyledWorkspaceContent>
|
||||
<Avatar
|
||||
placeholder={availableWorkspace.displayName || ''}
|
||||
avatarUrl={
|
||||
availableWorkspace.logo ?? DEFAULT_WORKSPACE_LOGO
|
||||
}
|
||||
avatarUrl={getAbsoluteImageUrl(
|
||||
availableWorkspace.logo ?? DEFAULT_WORKSPACE_LOGO,
|
||||
)}
|
||||
size="lg"
|
||||
/>
|
||||
<StyledWorkspaceTextContainer>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { type MouseEvent } from 'react';
|
||||
|
||||
import { getAvatarType } from '@/object-metadata/utils/getAvatarType';
|
||||
import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
|
||||
import { Avatar } from 'twenty-ui/data-display';
|
||||
import { MenuItemSuggestion } from 'twenty-ui/navigation';
|
||||
|
||||
@@ -40,7 +41,7 @@ export const MentionMenuListItem = ({
|
||||
<Avatar
|
||||
placeholder={label}
|
||||
placeholderColorSeed={recordId}
|
||||
avatarUrl={imageUrl}
|
||||
avatarUrl={getAbsoluteImageUrl(imageUrl)}
|
||||
type={getAvatarType(objectNameSingular) ?? 'rounded'}
|
||||
size="sm"
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getLinkToShowPage } from '@/object-metadata/utils/getLinkToShowPage';
|
||||
import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import {
|
||||
@@ -59,7 +60,7 @@ export const MentionRecordChip = ({
|
||||
placeholder={label}
|
||||
placeholderColorSeed={recordId}
|
||||
avatarType="rounded"
|
||||
avatarUrl={imageUrl}
|
||||
avatarUrl={getAbsoluteImageUrl(imageUrl)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { MenuItemSuggestion } from 'twenty-ui/navigation';
|
||||
import type { MentionSearchResult } from '@/mention/types/MentionSearchResult';
|
||||
import type { MentionSuggestionMenuProps } from '@/mention/types/MentionSuggestionMenuProps';
|
||||
import { SuggestionMenu } from '@/ui/suggestion/components/SuggestionMenu';
|
||||
import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
|
||||
|
||||
const getItemKey = (item: MentionSearchResult) =>
|
||||
`${item.objectNameSingular}-${item.recordId}`;
|
||||
@@ -20,7 +21,7 @@ const renderItem = (
|
||||
placeholder={item.label}
|
||||
placeholderColorSeed={item.recordId}
|
||||
avatarType="rounded"
|
||||
avatarUrl={item.imageUrl}
|
||||
avatarUrl={getAbsoluteImageUrl(item.imageUrl)}
|
||||
/>
|
||||
)}
|
||||
text={item.label}
|
||||
|
||||
+2
-1
@@ -20,6 +20,7 @@ import { useGetStandardObjectIcon } from '@/object-metadata/hooks/useGetStandard
|
||||
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
|
||||
import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
|
||||
|
||||
export const NavigationMenuItemIcon = ({
|
||||
navigationMenuItem,
|
||||
@@ -155,7 +156,7 @@ export const NavigationMenuItemIcon = ({
|
||||
type={recordIdentifier?.avatarType ?? 'icon'}
|
||||
Icon={iconToUse}
|
||||
iconColor={iconColorToUse}
|
||||
avatarUrl={recordIdentifier?.avatarUrl ?? ''}
|
||||
avatarUrl={getAbsoluteImageUrl(recordIdentifier?.avatarUrl ?? '')}
|
||||
placeholder={labelIdentifier}
|
||||
placeholderColorSeed={navigationMenuItem.targetRecordId ?? undefined}
|
||||
/>
|
||||
|
||||
+2
-1
@@ -30,6 +30,7 @@ import { Avatar } from 'twenty-ui/data-display';
|
||||
import { IconLock, useIcons } from 'twenty-ui/icon';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
|
||||
|
||||
export type NavigationDrawerItemForObjectMetadataItemProps = {
|
||||
objectMetadataItem: EnrichedObjectMetadataItem;
|
||||
@@ -150,7 +151,7 @@ export const NavigationDrawerItemForObjectMetadataItem = ({
|
||||
? 'squared'
|
||||
: 'rounded'
|
||||
}
|
||||
avatarUrl={recordIdentifier?.avatarUrl}
|
||||
avatarUrl={getAbsoluteImageUrl(recordIdentifier?.avatarUrl)}
|
||||
placeholderColorSeed={navigationMenuItem!.targetRecordId ?? undefined}
|
||||
placeholder={itemLabel}
|
||||
/>
|
||||
|
||||
+2
-1
@@ -14,6 +14,7 @@ import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadat
|
||||
import { SidePanelItemWithAddToNavigationDrag } from '@/side-panel/components/SidePanelItemWithAddToNavigationDrag';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
|
||||
|
||||
type SearchRecord = {
|
||||
recordId: string;
|
||||
@@ -90,7 +91,7 @@ export const SidePanelNewSidebarItemRecordItem = ({
|
||||
? 'squared'
|
||||
: 'rounded'
|
||||
}
|
||||
avatarUrl={record.imageUrl}
|
||||
avatarUrl={getAbsoluteImageUrl(record.imageUrl)}
|
||||
placeholderColorSeed={record.recordId}
|
||||
placeholder={record.label}
|
||||
/>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { canOpenObjectInSidePanel } from '@/object-record/utils/canOpenObjectInSidePanel';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { ViewOpenRecordIn } from '~/generated-metadata/graphql';
|
||||
import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { type MouseEvent } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@@ -98,7 +99,7 @@ export const RecordChip = ({
|
||||
placeholder={recordChipData.name}
|
||||
placeholderColorSeed={record.id}
|
||||
avatarType={recordChipData.avatarType}
|
||||
avatarUrl={recordChipData.avatarUrl ?? ''}
|
||||
avatarUrl={getAbsoluteImageUrl(recordChipData.avatarUrl ?? '')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -120,7 +121,7 @@ export const RecordChip = ({
|
||||
placeholder={recordChipData.name}
|
||||
placeholderColorSeed={record.id}
|
||||
avatarType={recordChipData.avatarType}
|
||||
avatarUrl={recordChipData.avatarUrl ?? ''}
|
||||
avatarUrl={getAbsoluteImageUrl(recordChipData.avatarUrl ?? '')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
+2
-1
@@ -3,6 +3,7 @@ import { styled } from '@linaria/react';
|
||||
import { Avatar } from 'twenty-ui/data-display';
|
||||
import { MenuItemMultiSelectAvatar } from 'twenty-ui/navigation';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
|
||||
|
||||
const StyledPinnedItemsContainer = styled.div`
|
||||
display: flex;
|
||||
@@ -31,7 +32,7 @@ export const ObjectFilterDropdownRecordPinnedItems = (props: {
|
||||
avatar={
|
||||
selectableItem.avatarUrl ? (
|
||||
<Avatar
|
||||
avatarUrl={selectableItem.avatarUrl}
|
||||
avatarUrl={getAbsoluteImageUrl(selectableItem.avatarUrl)}
|
||||
placeholderColorSeed={selectableItem.id}
|
||||
placeholder={selectableItem.name}
|
||||
type={selectableItem.avatarType}
|
||||
|
||||
+2
-1
@@ -18,6 +18,7 @@ import { MenuItemMultiSelectAvatar } from 'twenty-ui/navigation';
|
||||
|
||||
import { multipleRecordPickerSearchableObjectMetadataItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchableObjectMetadataItemsComponentState';
|
||||
import { type SearchRecord } from '~/generated/graphql';
|
||||
import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
|
||||
|
||||
type MultipleRecordPickerMenuItemContentProps = {
|
||||
searchRecord: SearchRecord;
|
||||
@@ -83,7 +84,7 @@ export const MultipleRecordPickerMenuItemContent = ({
|
||||
selected={isRecordSelectedWithObjectItem}
|
||||
avatar={
|
||||
<Avatar
|
||||
avatarUrl={searchRecord.imageUrl}
|
||||
avatarUrl={getAbsoluteImageUrl(searchRecord.imageUrl)}
|
||||
placeholderColorSeed={searchRecord.recordId}
|
||||
placeholder={displayText}
|
||||
size="md"
|
||||
|
||||
+2
-1
@@ -10,6 +10,7 @@ import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/com
|
||||
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useAtomComponentFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilyStateValue';
|
||||
import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
import { Avatar } from 'twenty-ui/data-display';
|
||||
import { MenuItemSelectAvatar } from 'twenty-ui/navigation';
|
||||
@@ -73,7 +74,7 @@ export const SingleRecordPickerMenuItem = ({
|
||||
focused={isSelectedItemId}
|
||||
avatar={
|
||||
<Avatar
|
||||
avatarUrl={searchRecordStore.imageUrl}
|
||||
avatarUrl={getAbsoluteImageUrl(searchRecordStore.imageUrl)}
|
||||
placeholderColorSeed={morphItem.recordId}
|
||||
placeholder={searchRecordStore.label}
|
||||
size="md"
|
||||
|
||||
+2
-1
@@ -10,6 +10,7 @@ import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectab
|
||||
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
|
||||
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Avatar } from 'twenty-ui/data-display';
|
||||
import { MenuItem, MenuItemMultiSelectAvatar } from 'twenty-ui/navigation';
|
||||
@@ -107,7 +108,7 @@ export const MultipleSelectDropdown = ({
|
||||
text={item.name}
|
||||
avatar={
|
||||
<Avatar
|
||||
avatarUrl={item.avatarUrl}
|
||||
avatarUrl={getAbsoluteImageUrl(item.avatarUrl)}
|
||||
placeholderColorSeed={item.id}
|
||||
placeholder={item.name}
|
||||
size="md"
|
||||
|
||||
+4
-3
@@ -29,6 +29,7 @@ import {
|
||||
AdminPanelRecentUsersDocument,
|
||||
AdminPanelTopWorkspacesDocument,
|
||||
} from '~/generated-admin/graphql';
|
||||
import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
|
||||
|
||||
const StyledEmptyState = styled.div`
|
||||
color: ${themeCssVariables.font.color.tertiary};
|
||||
@@ -136,7 +137,7 @@ export const SettingsAdminGeneral = () => {
|
||||
overflow="hidden"
|
||||
>
|
||||
<Avatar
|
||||
avatarUrl={user.avatarUrl}
|
||||
avatarUrl={getAbsoluteImageUrl(user.avatarUrl)}
|
||||
placeholder={
|
||||
`${user.firstName || ''} ${user.lastName || ''}`.trim() ||
|
||||
user.email
|
||||
@@ -160,7 +161,7 @@ export const SettingsAdminGeneral = () => {
|
||||
{user.workspaceId ? (
|
||||
<>
|
||||
<Avatar
|
||||
avatarUrl={user.workspaceLogo}
|
||||
avatarUrl={getAbsoluteImageUrl(user.workspaceLogo)}
|
||||
placeholder={user.workspaceName || ''}
|
||||
placeholderColorSeed={user.workspaceId}
|
||||
size="sm"
|
||||
@@ -232,7 +233,7 @@ export const SettingsAdminGeneral = () => {
|
||||
overflow="hidden"
|
||||
>
|
||||
<Avatar
|
||||
avatarUrl={workspace.logoUrl}
|
||||
avatarUrl={getAbsoluteImageUrl(workspace.logoUrl)}
|
||||
placeholder={workspace.name || ''}
|
||||
placeholderColorSeed={workspace.id}
|
||||
size="md"
|
||||
|
||||
+6
-10
@@ -12,7 +12,6 @@ import { useContext } from 'react';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import {
|
||||
formatUpgradeCommandName,
|
||||
getImageAbsoluteURI,
|
||||
getSettingsPath,
|
||||
isDefined,
|
||||
} from 'twenty-shared/utils';
|
||||
@@ -30,8 +29,8 @@ import { OverflowingTextWithTooltip } from 'twenty-ui/surfaces';
|
||||
import { H2Title } from 'twenty-ui/typography';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
||||
import { dateLocaleState } from '~/localization/states/dateLocaleState';
|
||||
import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
|
||||
import { formatDateTimeString } from '~/utils/string/formatDateTimeString';
|
||||
|
||||
type SettingsAdminWorkspaceContentProps = {
|
||||
@@ -83,14 +82,11 @@ export const SettingsAdminWorkspaceContent = ({
|
||||
})}
|
||||
leftComponent={
|
||||
<AvatarOrIcon
|
||||
avatarUrl={
|
||||
getImageAbsoluteURI({
|
||||
imageUrl: isNonEmptyString(activeWorkspace?.logo)
|
||||
? activeWorkspace?.logo
|
||||
: DEFAULT_WORKSPACE_LOGO,
|
||||
baseUrl: REACT_APP_SERVER_BASE_URL,
|
||||
}) ?? ''
|
||||
}
|
||||
avatarUrl={getAbsoluteImageUrl(
|
||||
isNonEmptyString(activeWorkspace?.logo)
|
||||
? activeWorkspace?.logo
|
||||
: DEFAULT_WORKSPACE_LOGO,
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
+2
-1
@@ -12,6 +12,7 @@ import { AppTooltip, TooltipDelay } from 'twenty-ui/surfaces';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
import { type RoleWithPartialMembers } from '@/settings/roles/types/RoleWithPartialMembers';
|
||||
import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
|
||||
|
||||
const StyledAssignedText = styled.div`
|
||||
color: ${themeCssVariables.font.color.secondary};
|
||||
@@ -102,7 +103,7 @@ export const SettingsRolesTableRow = ({ role }: SettingsRolesTableRowProps) => {
|
||||
<React.Fragment key={workspaceMember.id}>
|
||||
<div id={`avatar-${workspaceMember.id}`}>
|
||||
<Avatar
|
||||
avatarUrl={workspaceMember.avatarUrl}
|
||||
avatarUrl={getAbsoluteImageUrl(workspaceMember.avatarUrl)}
|
||||
placeholderColorSeed={workspaceMember.id}
|
||||
placeholder={workspaceMember.name.firstName ?? ''}
|
||||
type="rounded"
|
||||
|
||||
+4
-1
@@ -7,6 +7,7 @@ import { t } from '@lingui/core/macro';
|
||||
import { Avatar } from 'twenty-ui/data-display';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
|
||||
|
||||
const StyledSettingsCardContainer = styled.div`
|
||||
margin-top: ${themeCssVariables.spacing[6]};
|
||||
@@ -41,7 +42,9 @@ export const SettingsRoleAssignmentConfirmationModalSubtitle = ({
|
||||
title={selectedRoleTarget.role?.label || ''}
|
||||
Icon={
|
||||
<Avatar
|
||||
avatarUrl={enrichedSelectedWorkspaceMember?.avatarUrl}
|
||||
avatarUrl={getAbsoluteImageUrl(
|
||||
enrichedSelectedWorkspaceMember?.avatarUrl,
|
||||
)}
|
||||
placeholderColorSeed={enrichedSelectedWorkspaceMember?.id}
|
||||
placeholder={workspaceMemberName}
|
||||
size="md"
|
||||
|
||||
+2
-1
@@ -12,6 +12,7 @@ import { OverflowingTextWithTooltip } from 'twenty-ui/surfaces';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { type Agent, type ApiKeyForRole } from '~/generated-metadata/graphql';
|
||||
import { dateLocaleState } from '~/localization/states/dateLocaleState';
|
||||
import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
|
||||
import { formatDateString } from '~/utils/string/formatDateString';
|
||||
import { type PartialWorkspaceMember } from '@/settings/roles/types/RoleWithPartialMembers';
|
||||
|
||||
@@ -63,7 +64,7 @@ export const SettingsRoleAssignmentTableRow = ({
|
||||
);
|
||||
return (
|
||||
<Avatar
|
||||
avatarUrl={enrichedWorkspaceMember?.avatarUrl}
|
||||
avatarUrl={getAbsoluteImageUrl(enrichedWorkspaceMember?.avatarUrl)}
|
||||
placeholderColorSeed={enrichedWorkspaceMember?.id}
|
||||
placeholder={enrichedWorkspaceMember?.name.firstName ?? ''}
|
||||
type="rounded"
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@ import { useGetStandardObjectIcon } from '@/object-metadata/hooks/useGetStandard
|
||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||
import { useRecordChipData } from '@/object-record/hooks/useRecordChipData';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useContext } from 'react';
|
||||
import { Avatar } from 'twenty-ui/data-display';
|
||||
@@ -46,7 +47,7 @@ export const SidePanelContextRecordChipAvatars = ({
|
||||
<Icon color={IconColor} size={theme.icon.size.sm} />
|
||||
) : (
|
||||
<Avatar
|
||||
avatarUrl={recordChipData.avatarUrl}
|
||||
avatarUrl={getAbsoluteImageUrl(recordChipData.avatarUrl)}
|
||||
placeholderColorSeed={recordChipData.recordId}
|
||||
placeholder={recordChipData.name}
|
||||
type={recordChipData.avatarType}
|
||||
|
||||
@@ -25,6 +25,7 @@ import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { dateLocaleState } from '~/localization/states/dateLocaleState';
|
||||
|
||||
import { beautifyPastDateRelativeToNow } from '~/utils/date-utils';
|
||||
import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
|
||||
import { SidePanelPageInfoLayout } from './SidePanelPageInfoLayout';
|
||||
|
||||
const StyledClickableTitle = styled.div`
|
||||
@@ -140,7 +141,7 @@ export const SidePanelRecordInfo = ({
|
||||
icon={
|
||||
recordIdentifier ? (
|
||||
<Avatar
|
||||
avatarUrl={recordIdentifier.avatarUrl}
|
||||
avatarUrl={getAbsoluteImageUrl(recordIdentifier.avatarUrl)}
|
||||
placeholder={recordIdentifier.name}
|
||||
placeholderColorSeed={objectRecordId}
|
||||
size="md"
|
||||
|
||||
+2
-1
@@ -5,6 +5,7 @@ import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
|
||||
import { SidePanelList } from '@/side-panel/components/SidePanelList';
|
||||
import { useSidePanelSearchRecords } from '@/side-panel/pages/search/hooks/useSidePanelSearchRecords';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
|
||||
import { useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { CoreObjectNameSingular, AppPath } from 'twenty-shared/types';
|
||||
@@ -70,7 +71,7 @@ export const SidePanelSearchRecordsPage = () => {
|
||||
LeftComponent={
|
||||
<Avatar
|
||||
type={item.avatarType}
|
||||
avatarUrl={item.imageUrl}
|
||||
avatarUrl={getAbsoluteImageUrl(item.imageUrl)}
|
||||
placeholderColorSeed={item.recordId}
|
||||
placeholder={item.label}
|
||||
/>
|
||||
|
||||
+2
-1
@@ -4,6 +4,7 @@ import { useContext } from 'react';
|
||||
import { type BackgroundMockCompany } from '@/sign-in-background-mock/constants/BackgroundMockCompanies';
|
||||
import { BACKGROUND_MOCK_COLUMN_WIDTHS } from '@/sign-in-background-mock/constants/BackgroundMockColumnWidths';
|
||||
import { BACKGROUND_MOCK_TABLE_DIMENSIONS } from '@/sign-in-background-mock/constants/BackgroundMockTableDimensions';
|
||||
import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
|
||||
import {
|
||||
Avatar,
|
||||
Chip,
|
||||
@@ -125,7 +126,7 @@ export const BackgroundMockTableRow = ({
|
||||
leftComponent={
|
||||
<Avatar
|
||||
type="squared"
|
||||
avatarUrl={logoUrl}
|
||||
avatarUrl={getAbsoluteImageUrl(logoUrl)}
|
||||
placeholder={company.name}
|
||||
placeholderColorSeed={company.id}
|
||||
size="md"
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
IconWebhook,
|
||||
type IconComponent,
|
||||
} from 'twenty-ui/icon';
|
||||
import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
|
||||
|
||||
type ActorDisplayProps = Partial<FieldActorValue> & {
|
||||
avatarUrl?: string | null;
|
||||
@@ -96,7 +97,7 @@ export const ActorDisplay = ({
|
||||
avatarType={workspaceMemberId ? 'rounded' : 'squared'}
|
||||
placeholder={name}
|
||||
Icon={LeftIcon}
|
||||
avatarUrl={avatarUrl ?? undefined}
|
||||
avatarUrl={getAbsoluteImageUrl(avatarUrl ?? undefined)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
+2
-1
@@ -15,6 +15,7 @@ import {
|
||||
beautifyExactDateTime,
|
||||
beautifyPastDateRelativeToNow,
|
||||
} from '~/utils/date-utils';
|
||||
import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
|
||||
|
||||
type ShowPageSummaryCardProps = {
|
||||
avatarPlaceholder: string;
|
||||
@@ -148,7 +149,7 @@ export const ShowPageSummaryCard = ({
|
||||
hasIcon={isDefined(icon)}
|
||||
>
|
||||
<Avatar
|
||||
avatarUrl={logoOrAvatar}
|
||||
avatarUrl={getAbsoluteImageUrl(logoOrAvatar)}
|
||||
onClick={onUploadPicture ? handleAvatarClick : undefined}
|
||||
size="xl"
|
||||
placeholderColorSeed={id}
|
||||
|
||||
@@ -3,12 +3,19 @@ import { useContext } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Avatar } from 'twenty-ui/data-display';
|
||||
import { ThemeContext } from 'twenty-ui/theme-constants';
|
||||
import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
|
||||
|
||||
export const TabAvatar = ({ tab }: { tab: SingleTabProps }) => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
|
||||
if (isDefined(tab.logo)) {
|
||||
return <Avatar avatarUrl={tab.logo} size="md" placeholder={tab.title} />;
|
||||
return (
|
||||
<Avatar
|
||||
avatarUrl={getAbsoluteImageUrl(tab.logo)}
|
||||
size="md"
|
||||
placeholder={tab.title}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
tab.Icon && (
|
||||
|
||||
+4
-1
@@ -9,6 +9,7 @@ import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigat
|
||||
import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo';
|
||||
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
|
||||
import { useContext } from 'react';
|
||||
import { Avatar } from 'twenty-ui/data-display';
|
||||
import { ThemeContext } from 'twenty-ui/theme-constants';
|
||||
@@ -34,7 +35,9 @@ export const MultiWorkspaceDropdownClickableComponent = ({
|
||||
>
|
||||
<Avatar
|
||||
placeholder={currentWorkspace?.displayName || ''}
|
||||
avatarUrl={currentWorkspace?.logo ?? DEFAULT_WORKSPACE_LOGO}
|
||||
avatarUrl={getAbsoluteImageUrl(
|
||||
currentWorkspace?.logo ?? DEFAULT_WORKSPACE_LOGO,
|
||||
)}
|
||||
/>
|
||||
<StyledLabelWrapper>
|
||||
<NavigationDrawerAnimatedCollapseWrapper>
|
||||
|
||||
+7
-4
@@ -45,6 +45,7 @@ import {
|
||||
} from 'twenty-ui/navigation';
|
||||
import { type AvailableWorkspace } from '~/generated-metadata/graphql';
|
||||
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
|
||||
import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
|
||||
|
||||
const StyledDescription = styled.div`
|
||||
color: ${themeCssVariables.font.color.light};
|
||||
@@ -100,7 +101,9 @@ export const MultiWorkspaceDropdownDefaultComponents = () => {
|
||||
Avatar={
|
||||
<Avatar
|
||||
placeholder={currentWorkspace?.displayName || ''}
|
||||
avatarUrl={currentWorkspace?.logo ?? DEFAULT_WORKSPACE_LOGO}
|
||||
avatarUrl={getAbsoluteImageUrl(
|
||||
currentWorkspace?.logo ?? DEFAULT_WORKSPACE_LOGO,
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
@@ -161,9 +164,9 @@ export const MultiWorkspaceDropdownDefaultComponents = () => {
|
||||
avatar={
|
||||
<Avatar
|
||||
placeholder={availableWorkspace.displayName || ''}
|
||||
avatarUrl={
|
||||
availableWorkspace.logo ?? DEFAULT_WORKSPACE_LOGO
|
||||
}
|
||||
avatarUrl={getAbsoluteImageUrl(
|
||||
availableWorkspace.logo ?? DEFAULT_WORKSPACE_LOGO,
|
||||
)}
|
||||
/>
|
||||
}
|
||||
selected={false}
|
||||
|
||||
+4
-1
@@ -4,6 +4,7 @@ import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/consta
|
||||
import { type AvailableWorkspace } from '~/generated-metadata/graphql';
|
||||
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
|
||||
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
|
||||
import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
|
||||
import { getAvailableWorkspacePathAndSearchParams } from '@/auth/utils/availableWorkspacesUtils';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import React from 'react';
|
||||
@@ -49,7 +50,9 @@ export const AvailableWorkspaceItem = ({
|
||||
avatar={
|
||||
<Avatar
|
||||
placeholder={availableWorkspace.displayName || ''}
|
||||
avatarUrl={availableWorkspace.logo ?? DEFAULT_WORKSPACE_LOGO}
|
||||
avatarUrl={getAbsoluteImageUrl(
|
||||
availableWorkspace.logo ?? DEFAULT_WORKSPACE_LOGO,
|
||||
)}
|
||||
/>
|
||||
}
|
||||
selected={isSelected}
|
||||
|
||||
+2
-1
@@ -15,6 +15,7 @@ import {
|
||||
IconSettings,
|
||||
IconWorld,
|
||||
} from 'twenty-ui/icon';
|
||||
import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
|
||||
import { SettingsApplicationRegistrationConfigTab } from '~/pages/settings/applications/tabs/SettingsApplicationRegistrationConfigTab';
|
||||
import { SettingsApplicationRegistrationOAuthTab } from '~/pages/settings/applications/tabs/SettingsApplicationRegistrationOAuthTab';
|
||||
import { SettingsApplicationRegistrationDistributionTab } from '~/pages/settings/applications/tabs/SettingsApplicationRegistrationDistributionTab';
|
||||
@@ -108,7 +109,7 @@ export const SettingsAdminApplicationRegistrationDetail = () => {
|
||||
<Avatar
|
||||
type="app"
|
||||
size="md"
|
||||
avatarUrl={registration.logoUrl ?? undefined}
|
||||
avatarUrl={getAbsoluteImageUrl(registration.logoUrl ?? undefined)}
|
||||
placeholder={registration.name}
|
||||
placeholderColorSeed={registration.name}
|
||||
/>
|
||||
|
||||
+2
-1
@@ -45,6 +45,7 @@ import { H2Title } from 'twenty-ui/typography';
|
||||
import { Button, Toggle } from 'twenty-ui/input';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
|
||||
import {
|
||||
type FeatureFlagKey,
|
||||
type GetAdminWorkspaceChatThreadsQuery,
|
||||
@@ -267,7 +268,7 @@ export const SettingsAdminWorkspaceDetail = () => {
|
||||
overflow="hidden"
|
||||
>
|
||||
<Avatar
|
||||
avatarUrl={user.avatarUrl}
|
||||
avatarUrl={getAbsoluteImageUrl(user.avatarUrl)}
|
||||
placeholder={
|
||||
`${user.firstName || ''} ${user.lastName || ''}`.trim() ||
|
||||
user.email
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useContext } from 'react';
|
||||
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { getObjectColorWithFallback } from '@/object-metadata/utils/getObjectColorWithFallback';
|
||||
import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Avatar, getIconTileColorShades } from 'twenty-ui/data-display';
|
||||
import {
|
||||
@@ -98,7 +99,7 @@ export const SettingsToolIcon = ({
|
||||
if (isDefined(application) && isDefined(marketplaceApp?.logo)) {
|
||||
return (
|
||||
<Avatar
|
||||
avatarUrl={marketplaceApp?.logo ?? null}
|
||||
avatarUrl={getAbsoluteImageUrl(marketplaceApp?.logo ?? null)}
|
||||
placeholder={application.name}
|
||||
placeholderColorSeed={application.name}
|
||||
type="squared"
|
||||
|
||||
+2
-1
@@ -20,6 +20,7 @@ import { SettingsApplicationRegistrationDistributionTab } from '~/pages/settings
|
||||
import { SettingsApplicationRegistrationGeneralTab } from '~/pages/settings/applications/tabs/SettingsApplicationRegistrationGeneralTab';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||
import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
|
||||
import { styled } from '@linaria/react';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
@@ -99,7 +100,7 @@ export const SettingsApplicationRegistrationDetails = () => {
|
||||
<Avatar
|
||||
type="app"
|
||||
size="md"
|
||||
avatarUrl={registration.logoUrl ?? undefined}
|
||||
avatarUrl={getAbsoluteImageUrl(registration.logoUrl ?? undefined)}
|
||||
placeholder={registration.name}
|
||||
placeholderColorSeed={registration.name}
|
||||
/>
|
||||
|
||||
+4
-1
@@ -33,6 +33,7 @@ import { getSettingsPath, isDefined } from 'twenty-shared/utils';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { SettingsApplicationRegistrationShareLinkButtons } from '~/pages/settings/applications/components/SettingsApplicationRegistrationShareLinkButtons';
|
||||
import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
|
||||
|
||||
const StyledSourceRow = styled.div`
|
||||
align-items: center;
|
||||
@@ -119,7 +120,9 @@ export const SettingsApplicationRegistrationGeneralInfo = ({
|
||||
leftComponent={
|
||||
<AvatarOrIcon
|
||||
avatarType="rounded"
|
||||
avatarUrl={ownerWorkspace?.logo ?? undefined}
|
||||
avatarUrl={getAbsoluteImageUrl(
|
||||
ownerWorkspace?.logo ?? undefined,
|
||||
)}
|
||||
/>
|
||||
}
|
||||
label={ownerWorkspace.displayName}
|
||||
|
||||
+2
-1
@@ -13,6 +13,7 @@ import { Card } from 'twenty-ui/surfaces';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { type MarketplaceApp } from '~/generated-metadata/graphql';
|
||||
import { getApplicationDescriptionSummary } from '~/pages/settings/applications/utils/getApplicationDescriptionSummary';
|
||||
import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
|
||||
|
||||
type SettingsAvailableApplicationCardProps = {
|
||||
application: MarketplaceApp;
|
||||
@@ -60,7 +61,7 @@ export const SettingsAvailableApplicationCard = ({
|
||||
<Card rounded fullWidth>
|
||||
<StyledSettingsCardContent alignItems="flex-start" fullHeight>
|
||||
<Avatar
|
||||
avatarUrl={application.logo || null}
|
||||
avatarUrl={getAbsoluteImageUrl(application.logo || null)}
|
||||
placeholder={application.name}
|
||||
placeholderColorSeed={application.name}
|
||||
size="lg"
|
||||
|
||||
+4
-1
@@ -38,6 +38,7 @@ import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { useNavigateApp } from '~/hooks/useNavigateApp';
|
||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||
import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
|
||||
import { normalizeSearchText } from '~/utils/normalizeSearchText';
|
||||
|
||||
const StyledTableContainer = styled.div<{ hasMoreRows?: boolean }>`
|
||||
@@ -248,7 +249,9 @@ export const SettingsWorkspaceMembersTeamTab = () => {
|
||||
<TableCell>
|
||||
<StyledIconWrapper>
|
||||
<Avatar
|
||||
avatarUrl={workspaceMember.avatarUrl}
|
||||
avatarUrl={getAbsoluteImageUrl(
|
||||
workspaceMember.avatarUrl,
|
||||
)}
|
||||
placeholderColorSeed={workspaceMember.id}
|
||||
placeholder={workspaceMember.name.firstName ?? ''}
|
||||
type="rounded"
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
|
||||
|
||||
jest.mock('~/config', () => ({
|
||||
REACT_APP_SERVER_BASE_URL: 'https://example.com',
|
||||
}));
|
||||
|
||||
describe('getAbsoluteImageUrl', () => {
|
||||
it('returns undefined for nullish or empty input', () => {
|
||||
expect(getAbsoluteImageUrl(undefined)).toBeUndefined();
|
||||
expect(getAbsoluteImageUrl(null)).toBeUndefined();
|
||||
expect(getAbsoluteImageUrl('')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns absolute http(s) URLs unchanged', () => {
|
||||
expect(getAbsoluteImageUrl('https://cdn.example.com/avatar.png')).toBe(
|
||||
'https://cdn.example.com/avatar.png',
|
||||
);
|
||||
expect(getAbsoluteImageUrl('http://cdn.example.com/avatar.png')).toBe(
|
||||
'http://cdn.example.com/avatar.png',
|
||||
);
|
||||
});
|
||||
|
||||
it('resolves relative paths against the server base URL under /files', () => {
|
||||
expect(getAbsoluteImageUrl('avatars/avatar.png')).toBe(
|
||||
'https://example.com/files/avatars/avatar.png',
|
||||
);
|
||||
expect(getAbsoluteImageUrl('/avatars/avatar.png')).toBe(
|
||||
'https://example.com/files/avatars/avatar.png',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { getImageAbsoluteURI } from 'twenty-shared/utils';
|
||||
|
||||
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
||||
|
||||
export const getAbsoluteImageUrl = (
|
||||
imageUrl?: string | null,
|
||||
): string | undefined =>
|
||||
isNonEmptyString(imageUrl)
|
||||
? getImageAbsoluteURI({ imageUrl, baseUrl: REACT_APP_SERVER_BASE_URL })
|
||||
: undefined;
|
||||
@@ -14,6 +14,44 @@ describe('getImageAbsoluteURI', () => {
|
||||
expect(result).toBe(imageUrl);
|
||||
});
|
||||
|
||||
it('should return http absolute url unchanged', () => {
|
||||
const imageUrl = 'http://XXX/pic.png';
|
||||
const baseUrl = 'http://localhost:3000';
|
||||
const result = getImageAbsoluteURI({ imageUrl, baseUrl });
|
||||
expect(result).toBe(imageUrl);
|
||||
});
|
||||
|
||||
it('should treat schemes case-insensitively and return them unchanged', () => {
|
||||
const baseUrl = 'http://localhost:3000';
|
||||
expect(getImageAbsoluteURI({ imageUrl: 'HTTPS://XXX', baseUrl })).toBe(
|
||||
'HTTPS://XXX',
|
||||
);
|
||||
expect(
|
||||
getImageAbsoluteURI({ imageUrl: 'Data:image/png;base64,AAAA', baseUrl }),
|
||||
).toBe('Data:image/png;base64,AAAA');
|
||||
});
|
||||
|
||||
it('should return data URIs unchanged', () => {
|
||||
const imageUrl = 'data:image/png;base64,iVBORw0KGgo=';
|
||||
const baseUrl = 'http://localhost:3000';
|
||||
const result = getImageAbsoluteURI({ imageUrl, baseUrl });
|
||||
expect(result).toBe(imageUrl);
|
||||
});
|
||||
|
||||
it('should return blob URLs unchanged', () => {
|
||||
const imageUrl = 'blob:http://localhost:3000/123e4567-e89b-12d3';
|
||||
const baseUrl = 'http://localhost:3000';
|
||||
const result = getImageAbsoluteURI({ imageUrl, baseUrl });
|
||||
expect(result).toBe(imageUrl);
|
||||
});
|
||||
|
||||
it('should return protocol-relative URLs unchanged', () => {
|
||||
const imageUrl = '//cdn.example.com/pic.png';
|
||||
const baseUrl = 'http://localhost:3000';
|
||||
const result = getImageAbsoluteURI({ imageUrl, baseUrl });
|
||||
expect(result).toBe(imageUrl);
|
||||
});
|
||||
|
||||
it('should return fully formed url if imageUrl is a relative url starting with /', () => {
|
||||
const imageUrl = '/path/pic.png';
|
||||
const baseUrl = 'http://localhost:3000';
|
||||
|
||||
@@ -7,7 +7,13 @@ export const getImageAbsoluteURI = ({
|
||||
imageUrl,
|
||||
baseUrl,
|
||||
}: getImageAbsoluteURIProps): string => {
|
||||
if (imageUrl.startsWith('https:') || imageUrl.startsWith('http:')) {
|
||||
const lowerCaseImageUrl = imageUrl.toLowerCase();
|
||||
const isAlreadyAbsoluteUri =
|
||||
['http:', 'https:', 'data:', 'blob:'].some((scheme) =>
|
||||
lowerCaseImageUrl.startsWith(scheme),
|
||||
) || imageUrl.startsWith('//');
|
||||
|
||||
if (isAlreadyAbsoluteUri) {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,9 +8,7 @@ import { type AvatarType } from '@ui/data-display/Avatar/types/AvatarType';
|
||||
import { type IconComponent } from '@ui/icon/types/IconComponent';
|
||||
import { ThemeContext } from '@ui/theme-constants';
|
||||
import { stringToThemeColorP3String } from '@ui/utilities';
|
||||
import { REACT_APP_SERVER_BASE_URL } from '@ui/utilities/config';
|
||||
import { type Nullable } from '@ui/utilities/types/Nullable';
|
||||
import { getImageAbsoluteURI } from '@ui/utilities/utils/getImageAbsoluteURI';
|
||||
|
||||
import styles from './Avatar.module.scss';
|
||||
|
||||
@@ -49,12 +47,7 @@ export const Avatar = ({
|
||||
string | null
|
||||
>(null);
|
||||
|
||||
const avatarImageURI = isNonEmptyString(avatarUrl)
|
||||
? getImageAbsoluteURI({
|
||||
imageUrl: avatarUrl,
|
||||
baseUrl: REACT_APP_SERVER_BASE_URL,
|
||||
})
|
||||
: null;
|
||||
const avatarImageURI = isNonEmptyString(avatarUrl) ? avatarUrl : null;
|
||||
|
||||
const placeholderFirstChar = placeholder?.trim()?.charAt(0);
|
||||
const isPlaceholderFirstCharEmpty =
|
||||
|
||||
Vendored
-7
@@ -1,7 +0,0 @@
|
||||
declare global {
|
||||
interface Window {
|
||||
_env_?: Record<string, string>;
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
@@ -1,21 +0,0 @@
|
||||
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.
|
||||
// In 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
|
||||
// In prod context, we use index.html + 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 || getDefaultUrl();
|
||||
@@ -14,7 +14,6 @@ export {
|
||||
stringToThemeColorP3String,
|
||||
} from './color/utils/stringToThemeColorP3String';
|
||||
export { themeColorSchema } from './color/utils/themeColorSchema';
|
||||
export { REACT_APP_SERVER_BASE_URL } from './config';
|
||||
export { getOsControlSymbol } from './device/getOsControlSymbol';
|
||||
export { getOsShortcutSeparator } from './device/getOsShortcutSeparator';
|
||||
export { getUserDevice } from './device/getUserDevice';
|
||||
@@ -27,7 +26,6 @@ export { useScreenSize } from './screen-size/hooks/useScreenSize';
|
||||
export type { ClickOutsideAttributes } from './types/ClickOutsideAttributes';
|
||||
export type { Nullable } from './types/Nullable';
|
||||
export { getDisplayValueByUrlType } from './utils/getDisplayValueByUrlType';
|
||||
export { getImageAbsoluteURI } from './utils/getImageAbsoluteURI';
|
||||
export { getSafeUrl } from './utils/getSafeUrl';
|
||||
export { isDefined } from './utils/isDefined';
|
||||
export type { LinkifyMatch } from './utils/linkifyText';
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
type getImageAbsoluteURIProps = {
|
||||
imageUrl: string;
|
||||
baseUrl: string;
|
||||
};
|
||||
|
||||
export const getImageAbsoluteURI = ({
|
||||
imageUrl,
|
||||
baseUrl,
|
||||
}: getImageAbsoluteURIProps): string => {
|
||||
if (imageUrl.startsWith('https:') || imageUrl.startsWith('http:')) {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
if (imageUrl.startsWith('/')) {
|
||||
return new URL(`/files${imageUrl}`, baseUrl).toString();
|
||||
}
|
||||
|
||||
return new URL(`/files/${imageUrl}`, baseUrl).toString();
|
||||
};
|
||||
Reference in New Issue
Block a user