Fix broken image urls in Settings > Profile and Invite To Workspace Email (#8942)

Fixes #8601

We had 3 implementations of getImageAbsoluteURI: in twenty-front, in
twenty-ui and in twenty-emails. I was able to remove the one in
twenty-front but I could not remove it from twenty-emails as this is a
standalone for now. The vision is to introduce shared utils in a
twenty-shared package
This commit is contained in:
Charles Bochet
2024-12-07 14:57:32 +01:00
committed by GitHub
parent ab8ad46685
commit 99f53a5030
17 changed files with 72 additions and 84 deletions

View File

@ -20,7 +20,7 @@ type SendInviteLinkEmailProps = {
firstName: string;
lastName: string;
};
serverUrl?: string;
serverUrl: string;
};
export const SendInviteLinkEmail = ({
@ -29,7 +29,9 @@ export const SendInviteLinkEmail = ({
sender,
serverUrl,
}: SendInviteLinkEmailProps) => {
const workspaceLogo = getImageAbsoluteURI(workspace.logo, serverUrl);
const workspaceLogo = workspace.logo
? getImageAbsoluteURI(workspace.logo, serverUrl)
: null;
return (
<BaseEmail width={333}>

View File

@ -1,16 +1,9 @@
export const getImageAbsoluteURI = (
imageUrl?: string | null,
serverUrl?: string,
) => {
if (!imageUrl) {
return null;
}
if (imageUrl?.startsWith('https:')) {
export const getImageAbsoluteURI = (imageUrl: string, serverUrl: string) => {
if (imageUrl.startsWith('https:') || imageUrl.startsWith('http:')) {
return imageUrl;
}
return serverUrl?.endsWith('/')
return serverUrl.endsWith('/')
? `${serverUrl.substring(0, serverUrl.length - 1)}/files/${imageUrl}`
: `${serverUrl || ''}/files/${imageUrl}`;
: `${serverUrl}/files/${imageUrl}`;
};

View File

@ -1,6 +1,5 @@
import styled from '@emotion/styled';
import { getImageAbsoluteURI } from '~/utils/image/getImageAbsoluteURI';
import { getImageAbsoluteURI, isDefined } from 'twenty-ui';
type LogoProps = {
primaryLogo?: string | null;
@ -49,7 +48,9 @@ export const Logo = (props: LogoProps) => {
const primaryLogoUrl = getImageAbsoluteURI(
props.primaryLogo ?? defaultPrimaryLogoUrl,
);
const secondaryLogoUrl = getImageAbsoluteURI(props.secondaryLogo);
const secondaryLogoUrl = isDefined(props.secondaryLogo)
? getImageAbsoluteURI(props.secondaryLogo)
: null;
return (
<StyledContainer>

View File

@ -8,11 +8,10 @@ import {
NavigationDrawerProps,
} from '@/ui/navigation/navigation-drawer/components/NavigationDrawer';
import { isAdvancedModeEnabledState } from '@/ui/navigation/navigation-drawer/states/isAdvancedModeEnabledState';
import { getImageAbsoluteURI } from '~/utils/image/getImageAbsoluteURI';
import { useIsSettingsDrawer } from '@/navigation/hooks/useIsSettingsDrawer';
import { AdvancedSettingsToggle } from 'twenty-ui';
import { AdvancedSettingsToggle, getImageAbsoluteURI } from 'twenty-ui';
import { MainNavigationDrawerItems } from './MainNavigationDrawerItems';
export type AppNavigationDrawerProps = {

View File

@ -2,11 +2,11 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { getLogoUrlFromDomainName } from '~/utils';
import { getImageAbsoluteURI } from '~/utils/image/getImageAbsoluteURI';
import { isDefined } from '~/utils/isDefined';
import { Company } from '@/companies/types/Company';
import { getCompanyDomainName } from '@/object-metadata/utils/getCompanyDomainName';
import { getImageAbsoluteURI } from 'twenty-ui';
import { getImageIdentifierFieldValue } from './getImageIdentifierFieldValue';
export const getAvatarUrl = (
@ -25,7 +25,9 @@ export const getAvatarUrl = (
}
if (objectNameSingular === CoreObjectNameSingular.Person) {
return getImageAbsoluteURI(record.avatarUrl) ?? '';
return isDefined(record.avatarUrl)
? getImageAbsoluteURI(record.avatarUrl)
: '';
}
const imageIdentifierFieldValue = getImageIdentifierFieldValue(

View File

@ -1,8 +1,14 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import React, { useMemo } from 'react';
import { Button, IconPhotoUp, IconTrash, IconUpload, IconX } from 'twenty-ui';
import { getImageAbsoluteURI } from '~/utils/image/getImageAbsoluteURI';
import React from 'react';
import {
Button,
IconPhotoUp,
IconTrash,
IconUpload,
IconX,
getImageAbsoluteURI,
} from 'twenty-ui';
import { isDefined } from '~/utils/isDefined';
const StyledContainer = styled.div`
@ -109,7 +115,7 @@ export const ImageInput = ({
hiddenFileInput.current?.click();
};
const pictureURI = useMemo(() => getImageAbsoluteURI(picture), [picture]);
const pictureURI = isDefined(picture) ? getImageAbsoluteURI(picture) : null;
return (
<StyledContainer className={className}>

View File

@ -1,5 +1,6 @@
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { Workspaces } from '@/auth/states/workspaces';
import { useBuildWorkspaceUrl } from '@/domain-manager/hooks/useBuildWorkspaceUrl';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
@ -17,9 +18,8 @@ import {
IconChevronDown,
MenuItemSelectAvatar,
UndecoratedLink,
getImageAbsoluteURI,
} from 'twenty-ui';
import { getImageAbsoluteURI } from '~/utils/image/getImageAbsoluteURI';
import { useBuildWorkspaceUrl } from '@/domain-manager/hooks/useBuildWorkspaceUrl';
const StyledLogo = styled.div<{ logo: string }>`
background: url(${({ logo }) => logo});

View File

@ -1,7 +1,7 @@
import { Helmet } from 'react-helmet-async';
import { workspacePublicDataState } from '@/auth/states/workspacePublicDataState';
import { Helmet } from 'react-helmet-async';
import { useRecoilValue } from 'recoil';
import { getImageAbsoluteURI } from '~/utils/image/getImageAbsoluteURI';
import { getImageAbsoluteURI } from 'twenty-ui';
export const PageFavicon = () => {
const workspacePublicData = useRecoilValue(workspacePublicDataState);

View File

@ -107,7 +107,7 @@ export const SettingsAdminFeatureFlags = () => {
title: workspace.name,
logo:
getImageAbsoluteURI(
workspace.logo === null ? DEFAULT_WORKSPACE_LOGO : workspace.logo,
isDefined(workspace.logo) ? workspace.logo : DEFAULT_WORKSPACE_LOGO,
) ?? '',
})) ?? [];

View File

@ -1,13 +1,13 @@
import { Meta, StoryObj } from '@storybook/react';
import { within } from '@storybook/test';
import { graphql, http, HttpResponse } from 'msw';
import { HttpResponse, graphql, http } from 'msw';
import { getImageAbsoluteURI } from 'twenty-ui';
import { SettingsServerlessFunctionDetail } from '~/pages/settings/serverless-functions/SettingsServerlessFunctionDetail';
import {
PageDecorator,
PageDecoratorArgs,
} from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { getImageAbsoluteURI } from '~/utils/image/getImageAbsoluteURI';
import { sleep } from '~/utils/sleep';
const SOURCE_CODE_FULL_PATH =

View File

@ -1,26 +0,0 @@
import { REACT_APP_SERVER_BASE_URL } from '~/config';
type ImageAbsoluteURI<T extends string | null | undefined> = T extends string
? string
: null;
export const getImageAbsoluteURI = <T extends string | null | undefined>(
imageUrl: T,
): ImageAbsoluteURI<T> => {
if (!imageUrl) {
return null as ImageAbsoluteURI<T>;
}
if (imageUrl.startsWith('https:') || imageUrl.startsWith('http:')) {
return imageUrl as ImageAbsoluteURI<T>;
}
const serverFilesUrl = new URL(REACT_APP_SERVER_BASE_URL);
serverFilesUrl.pathname = `/files/`;
serverFilesUrl.pathname += imageUrl.startsWith('/')
? imageUrl.slice(1)
: imageUrl;
return serverFilesUrl.toString() as ImageAbsoluteURI<T>;
};

View File

@ -3,12 +3,13 @@ import { Module } from '@nestjs/common';
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
import { FileModule } from 'src/engine/core-modules/file/file.module';
import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service';
import { WorkspaceInvitationResolver } from 'src/engine/core-modules/workspace-invitation/workspace-invitation.resolver';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
@Module({
imports: [
@ -17,6 +18,7 @@ import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/doma
[AppToken, UserWorkspace, Workspace],
'core',
),
FileModule,
OnboardingModule,
],
exports: [WorkspaceInvitationService],

View File

@ -1,15 +1,16 @@
import { UseGuards } from '@nestjs/common';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service';
import { WorkspaceInvitation } from 'src/engine/core-modules/workspace-invitation/dtos/workspace-invitation.dto';
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { User } from 'src/engine/core-modules/user/user.entity';
import { SendInvitationsOutput } from 'src/engine/core-modules/workspace-invitation/dtos/send-invitations.output';
import { WorkspaceInvitation } from 'src/engine/core-modules/workspace-invitation/dtos/workspace-invitation.dto';
import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { SendInvitationsInput } from './dtos/send-invitations.input';
@ -18,6 +19,7 @@ import { SendInvitationsInput } from './dtos/send-invitations.input';
export class WorkspaceInvitationResolver {
constructor(
private readonly workspaceInvitationService: WorkspaceInvitationService,
private readonly fileService: FileService,
) {}
@Mutation(() => String)
@ -57,9 +59,19 @@ export class WorkspaceInvitationResolver {
@AuthUser() user: User,
@AuthWorkspace() workspace: Workspace,
): Promise<SendInvitationsOutput> {
let workspaceLogoWithToken = '';
if (workspace.logo) {
const workspaceLogoToken = await this.fileService.encodeFileToken({
workspaceId: workspace.id,
});
workspaceLogoWithToken = `${workspace.logo}?token=${workspaceLogoToken}`;
}
return await this.workspaceInvitationService.sendInvitations(
sendInviteLinkInput.emails,
workspace,
{ ...workspace, logo: workspaceLogoWithToken },
user,
);
}

View File

@ -9,7 +9,12 @@ import { AvatarSize } from '@ui/display/avatar/types/AvatarSize';
import { AvatarType } from '@ui/display/avatar/types/AvatarType';
import { IconComponent } from '@ui/display/icon/types/IconComponent';
import { ThemeContext } from '@ui/theme';
import { Nullable, getImageAbsoluteURI, stringToHslColor } from '@ui/utilities';
import {
Nullable,
getImageAbsoluteURI,
isDefined,
stringToHslColor,
} from '@ui/utilities';
const StyledAvatar = styled.div<{
size: AvatarSize;
@ -82,7 +87,7 @@ export const Avatar = ({
);
const avatarImageURI = useMemo(
() => getImageAbsoluteURI(avatarUrl),
() => (isDefined(avatarUrl) ? getImageAbsoluteURI(avatarUrl) : null),
[avatarUrl],
);

View File

@ -254,7 +254,7 @@ export {
IconVideo,
IconWand,
IconWorld,
IconX
IconX,
} from '@tabler/icons-react';
export type { TablerIconsProps } from '@tabler/icons-react';

View File

@ -1,18 +1,18 @@
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 absolute url if the imageUrl is an absolute unsecure url', () => {
const imageUrl = 'http://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);

View File

@ -1,15 +1,7 @@
import { REACT_APP_SERVER_BASE_URL } from '@ui/utilities/config';
// TODO: this is a code smell trying to guess whether it's a relative path or not
// We should instead put the meaning onto our variables and parameters
// imageUrl should be either imageAbsoluteURL or imageRelativeServerPath
// But we need to refactor the chain of calls to this function
export const getImageAbsoluteURI = (imageUrl?: string | null) => {
if (!imageUrl) {
return null;
}
if (imageUrl?.startsWith('http')) {
export const getImageAbsoluteURI = (imageUrl: string) => {
if (imageUrl.startsWith('https:') || imageUrl.startsWith('http:')) {
return imageUrl;
}