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:
@ -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}>
|
||||
|
||||
@ -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}`;
|
||||
};
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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}>
|
||||
|
||||
@ -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});
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
) ?? '',
|
||||
})) ?? [];
|
||||
|
||||
|
||||
@ -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 =
|
||||
|
||||
@ -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>;
|
||||
};
|
||||
@ -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],
|
||||
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@ -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],
|
||||
);
|
||||
|
||||
|
||||
@ -254,7 +254,7 @@ export {
|
||||
IconVideo,
|
||||
IconWand,
|
||||
IconWorld,
|
||||
IconX
|
||||
IconX,
|
||||
} from '@tabler/icons-react';
|
||||
|
||||
export type { TablerIconsProps } from '@tabler/icons-react';
|
||||
|
||||
@ -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);
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user