Add Twenty Shared & Fix profile image rendering (#8841)

PR Summary: 

1. Added `Twenty Shared` Package to centralize utilitiies as mentioned
in #8942
2. Optimization of `getImageAbsoluteURI.ts` to handle edge cases


![image](https://github.com/user-attachments/assets/c72a3061-6eba-46b8-85ac-869f06bf23c0)

---------

Co-authored-by: Antoine Moreaux <moreaux.antoine@gmail.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Mohammed Abdul Razak Wahab
2024-12-17 13:54:21 +05:30
committed by GitHub
parent 4e329d08b0
commit 08a9db2df6
39 changed files with 453 additions and 129 deletions

View File

@ -35,6 +35,7 @@ jobs:
package.json
packages/twenty-front/**
packages/twenty-ui/**
packages/twenty-shared/**
- name: Skip if no relevant changes
if: steps.changed-files.outputs.any_changed == 'false'

View File

@ -49,10 +49,14 @@ jobs:
package.json
packages/twenty-server/**
packages/twenty-emails/**
packages/twenty-shared/**
- name: Install dependencies
if: steps.changed-files.outputs.any_changed == 'true'
uses: ./.github/workflows/actions/yarn-install
- name: Build twenty-shared
if: steps.changed-files.outputs.any_changed == 'true'
run: npx nx build twenty-shared
- name: Server / Restore Task Cache
if: steps.changed-files.outputs.any_changed == 'true'
uses: ./.github/workflows/actions/task-cache
@ -120,6 +124,7 @@ jobs:
package.json
packages/twenty-server/**
packages/twenty-emails/**
packages/twenty-shared/**
- name: Install dependencies
if: steps.changed-files.outputs.any_changed == 'true'

48
.github/workflows/ci-shared.yaml vendored Normal file
View File

@ -0,0 +1,48 @@
name: CI Shared
on:
push:
branches:
- main
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
shared-test:
timeout-minutes: 30
runs-on: ubuntu-latest
env:
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
strategy:
matrix:
task: [lint, typecheck, test]
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
with:
access_token: ${{ github.token }}
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check for changed files
id: changed-files
uses: tj-actions/changed-files@v11
with:
files: |
packages/twenty-shared/**
- name: Skip if no relevant changes
if: steps.changed-files.outputs.any_changed == 'false'
run: echo "No relevant changes. Skipping CI."
- name: Install dependencies
if: steps.changed-files.outputs.any_changed == 'true'
uses: ./.github/workflows/actions/yarn-install
- name: Run ${{ matrix.task }} task
if: steps.changed-files.outputs.any_changed == 'true'
uses: ./.github/workflows/actions/nx-affected
with:
tag: scope:frontend
tasks: ${{ matrix.task }}

12
nx.json
View File

@ -47,7 +47,8 @@
"configurations": {
"ci": { "cacheStrategy": "content" },
"fix": { "fix": true }
}
},
"dependsOn": ["^build"]
},
"fmt": {
"executor": "nx:run-commands",
@ -63,7 +64,8 @@
"configurations": {
"ci": { "cacheStrategy": "content" },
"fix": { "write": true }
}
},
"dependsOn": ["^build"]
},
"typecheck": {
"executor": "nx:run-commands",
@ -74,7 +76,8 @@
},
"configurations": {
"watch": { "watch": true }
}
},
"dependsOn": ["^build"]
},
"test": {
"executor": "@nx/jest:jest",
@ -115,7 +118,8 @@
"command": "VITE_DISABLE_TYPESCRIPT_CHECKER=true VITE_DISABLE_ESLINT_CHECKER=true storybook build",
"output-dir": "storybook-static",
"config-dir": ".storybook"
}
},
"dependsOn": ["^build"]
},
"storybook:serve:dev": {
"executor": "nx:run-commands",

View File

@ -249,7 +249,6 @@
"@types/express": "^4.17.13",
"@types/graphql-fields": "^1.3.6",
"@types/graphql-upload": "^8.0.12",
"@types/jest": "^29.5.11",
"@types/js-cookie": "^3.0.3",
"@types/js-levenshtein": "^1.1.3",
"@types/lodash.camelcase": "^4.3.7",
@ -365,6 +364,7 @@
"packages/twenty-zapier",
"packages/twenty-website",
"packages/twenty-e2e-testing",
"packages/twenty-shared",
"tools/eslint-rules"
]
}

View File

@ -10,7 +10,7 @@ import { MainText } from 'src/components/MainText';
import { Title } from 'src/components/Title';
import { WhatIsTwenty } from 'src/components/WhatIsTwenty';
import { capitalize } from 'src/utils/capitalize';
import { getImageAbsoluteURI } from 'src/utils/getImageAbsoluteURI';
import { getImageAbsoluteURI } from 'twenty-shared';
type SendInviteLinkEmailProps = {
link: string;
@ -30,7 +30,7 @@ export const SendInviteLinkEmail = ({
serverUrl,
}: SendInviteLinkEmailProps) => {
const workspaceLogo = workspace.logo
? getImageAbsoluteURI(workspace.logo, serverUrl)
? getImageAbsoluteURI({ imageUrl: workspace.logo, baseUrl: serverUrl })
: null;
return (

View File

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

View File

@ -1,4 +1,5 @@
{
"type": "module",
"compilerOptions": {
"jsx": "react-jsx",
"allowJs": false,
@ -6,7 +7,10 @@
"allowSyntheticDefaultImports": true,
"strict": true,
"types": ["vite/client"],
"baseUrl": "."
"baseUrl": ".",
"paths": {
"twenty-shared": ["../../packages/twenty-shared/dist"]
}
},
"files": [],
"include": [],

View File

@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/icons/android/android-launchericon-48-48.png" />
<link rel="icon" type="image/x-icon" href="/icons/android/android-launchericon-48-48.png" data-rh="true"/>
<link rel="apple-touch-icon" href="/icons/ios/192.png" />
<meta name="theme-color" content="#000000" />

View File

@ -1,6 +1,8 @@
import styled from '@emotion/styled';
import { isNonEmptyString } from '@sniptt/guards';
import { getImageAbsoluteURI } from 'twenty-ui';
import { getImageAbsoluteURI } from 'twenty-shared';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
type LogoProps = {
primaryLogo?: string | null;
@ -46,16 +48,21 @@ const StyledPrimaryLogo = styled.div<{ src: string }>`
export const Logo = (props: LogoProps) => {
const defaultPrimaryLogoUrl = `${window.location.origin}/icons/android/android-launchericon-192-192.png`;
const primaryLogoUrl = getImageAbsoluteURI(
props.primaryLogo ?? defaultPrimaryLogoUrl,
);
const primaryLogoUrl = getImageAbsoluteURI({
imageUrl: props.primaryLogo ?? defaultPrimaryLogoUrl,
baseUrl: REACT_APP_SERVER_BASE_URL,
});
const secondaryLogoUrl = isNonEmptyString(props.secondaryLogo)
? getImageAbsoluteURI(props.secondaryLogo)
? getImageAbsoluteURI({
imageUrl: props.secondaryLogo,
baseUrl: REACT_APP_SERVER_BASE_URL,
})
: null;
return (
<StyledContainer>
<StyledPrimaryLogo src={primaryLogoUrl} />
<StyledPrimaryLogo src={primaryLogoUrl ?? ''} />
{secondaryLogoUrl && (
<StyledSecondaryLogoContainer>
<StyledSecondaryLogo src={secondaryLogoUrl} />

View File

@ -19,7 +19,7 @@ export const useGetPublicWorkspaceDataBySubdomain = () => {
workspacePublicDataState,
);
const { loading } = useGetPublicWorkspaceDataBySubdomainQuery({
const { loading, data, error } = useGetPublicWorkspaceDataBySubdomainQuery({
skip:
(isMultiWorkspaceEnabled && isDefaultDomain) ||
isDefined(workspacePublicData),
@ -38,5 +38,7 @@ export const useGetPublicWorkspaceDataBySubdomain = () => {
return {
loading,
data: data?.getPublicWorkspaceDataBySubdomain,
error,
};
};

View File

@ -8,11 +8,14 @@ import {
NavigationDrawerProps,
} from '@/ui/navigation/navigation-drawer/components/NavigationDrawer';
import { isAdvancedModeEnabledState } from '@/ui/navigation/navigation-drawer/states/isAdvancedModeEnabledState';
import { getImageAbsoluteURI } from 'twenty-shared';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
import { useIsSettingsDrawer } from '@/navigation/hooks/useIsSettingsDrawer';
import { AdvancedSettingsToggle, getImageAbsoluteURI } from 'twenty-ui';
import { MainNavigationDrawerItems } from './MainNavigationDrawerItems';
import { MainNavigationDrawerItems } from '@/navigation/components/MainNavigationDrawerItems';
import { isNonEmptyString } from '@sniptt/guards';
import { AdvancedSettingsToggle } from 'twenty-ui';
export type AppNavigationDrawerProps = {
className?: string;
@ -40,10 +43,12 @@ export const AppNavigationDrawer = ({
),
}
: {
logo:
(currentWorkspace?.logo &&
getImageAbsoluteURI(currentWorkspace.logo)) ??
undefined,
logo: isNonEmptyString(currentWorkspace?.logo)
? getImageAbsoluteURI({
imageUrl: currentWorkspace.logo,
baseUrl: REACT_APP_SERVER_BASE_URL,
})
: undefined,
title: currentWorkspace?.displayName ?? undefined,
children: <MainNavigationDrawerItems />,
footer: <SupportDropdown />,

View File

@ -1,12 +1,12 @@
import { Company } from '@/companies/types/Company';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { getCompanyDomainName } from '@/object-metadata/utils/getCompanyDomainName';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { getImageAbsoluteURI } from 'twenty-shared';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
import { getLogoUrlFromDomainName } from '~/utils';
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 = (
@ -26,7 +26,10 @@ export const getAvatarUrl = (
if (objectNameSingular === CoreObjectNameSingular.Person) {
return isDefined(record.avatarUrl)
? getImageAbsoluteURI(record.avatarUrl)
? getImageAbsoluteURI({
imageUrl: record.avatarUrl,
baseUrl: REACT_APP_SERVER_BASE_URL,
})
: '';
}

View File

@ -2,14 +2,9 @@ import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { isNonEmptyString } from '@sniptt/guards';
import React from 'react';
import {
Button,
IconPhotoUp,
IconTrash,
IconUpload,
IconX,
getImageAbsoluteURI,
} from 'twenty-ui';
import { getImageAbsoluteURI } from 'twenty-shared';
import { Button, IconPhotoUp, IconTrash, IconUpload, IconX } from 'twenty-ui';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
import { isDefined } from '~/utils/isDefined';
const StyledContainer = styled.div`
@ -117,7 +112,10 @@ export const ImageInput = ({
};
const pictureURI = isNonEmptyString(picture)
? getImageAbsoluteURI(picture)
? getImageAbsoluteURI({
imageUrl: picture,
baseUrl: REACT_APP_SERVER_BASE_URL,
})
: null;
return (

View File

@ -14,12 +14,13 @@ import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useState } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { getImageAbsoluteURI } from 'twenty-shared';
import {
IconChevronDown,
MenuItemSelectAvatar,
UndecoratedLink,
getImageAbsoluteURI,
} from 'twenty-ui';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
const StyledLogo = styled.div<{ logo: string }>`
background: url(${({ logo }) => logo});
@ -102,9 +103,12 @@ export const MultiWorkspaceDropdownButton = ({
isNavigationDrawerExpanded={isNavigationDrawerExpanded}
>
<StyledLogo
logo={getImageAbsoluteURI(
currentWorkspace?.logo ?? DEFAULT_WORKSPACE_LOGO,
)}
logo={
getImageAbsoluteURI({
imageUrl: currentWorkspace?.logo ?? '',
baseUrl: REACT_APP_SERVER_BASE_URL,
}) ?? ''
}
/>
<NavigationDrawerAnimatedCollapseWrapper>
<StyledLabel>{currentWorkspace?.displayName ?? ''}</StyledLabel>
@ -132,9 +136,12 @@ export const MultiWorkspaceDropdownButton = ({
text={workspace.displayName ?? '(No name)'}
avatar={
<StyledLogo
logo={getImageAbsoluteURI(
workspace.logo ?? DEFAULT_WORKSPACE_LOGO,
)}
logo={
getImageAbsoluteURI({
imageUrl: workspace.logo ?? DEFAULT_WORKSPACE_LOGO,
baseUrl: REACT_APP_SERVER_BASE_URL,
}) ?? ''
}
/>
}
selected={currentWorkspace?.id === workspace.id}

View File

@ -1,18 +1,23 @@
import { workspacePublicDataState } from '@/auth/states/workspacePublicDataState';
import { Helmet } from 'react-helmet-async';
import { useRecoilValue } from 'recoil';
import { getImageAbsoluteURI } from 'twenty-ui';
import { getImageAbsoluteURI } from 'twenty-shared';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
export const PageFavicon = () => {
const workspacePublicData = useRecoilValue(workspacePublicDataState);
return (
<Helmet>
{workspacePublicData?.logo && (
<link
rel="icon"
type="image/x-icon"
href={getImageAbsoluteURI(workspacePublicData.logo)}
href={
getImageAbsoluteURI({
imageUrl: workspacePublicData.logo,
baseUrl: REACT_APP_SERVER_BASE_URL,
}) ?? ''
}
/>
)}
</Helmet>

View File

@ -1,6 +1,5 @@
import { useRecoilValue } from 'recoil';
import { workspacePublicDataState } from '@/auth/states/workspacePublicDataState';
import { useEffect } from 'react';
import { isDefined } from '~/utils/isDefined';
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
@ -9,8 +8,10 @@ import { lastAuthenticatedWorkspaceDomainState } from '@/domain-manager/states/l
import { useReadWorkspaceSubdomainFromCurrentLocation } from '@/domain-manager/hooks/useReadWorkspaceSubdomainFromCurrentLocation';
import { useIsCurrentLocationOnDefaultDomain } from '@/domain-manager/hooks/useIsCurrentLocationOnDefaultDomain';
import { useGetPublicWorkspaceDataBySubdomain } from '@/domain-manager/hooks/useGetPublicWorkspaceDataBySubdomain';
export const WorkspaceProviderEffect = () => {
const workspacePublicData = useRecoilValue(workspacePublicDataState);
const { data: getPublicWorkspaceData } =
useGetPublicWorkspaceDataBySubdomain();
const lastAuthenticatedWorkspaceDomain = useRecoilValue(
lastAuthenticatedWorkspaceDomainState,
@ -26,16 +27,16 @@ export const WorkspaceProviderEffect = () => {
useEffect(() => {
if (
isMultiWorkspaceEnabled &&
isDefined(workspacePublicData?.subdomain) &&
workspacePublicData.subdomain !== workspaceSubdomain
isDefined(getPublicWorkspaceData?.subdomain) &&
getPublicWorkspaceData.subdomain !== workspaceSubdomain
) {
redirectToWorkspaceDomain(workspacePublicData.subdomain);
redirectToWorkspaceDomain(getPublicWorkspaceData.subdomain);
}
}, [
workspaceSubdomain,
isMultiWorkspaceEnabled,
redirectToWorkspaceDomain,
workspacePublicData,
getPublicWorkspaceData,
]);
useEffect(() => {

View File

@ -13,10 +13,11 @@ import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo';
import styled from '@emotion/styled';
import { isNonEmptyString } from '@sniptt/guards';
import { useState } from 'react';
import { getImageAbsoluteURI } from 'twenty-shared';
import {
Button,
getImageAbsoluteURI,
H1Title,
H1TitleFontColor,
H2Title,
@ -25,6 +26,7 @@ import {
Section,
Toggle,
} from 'twenty-ui';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
const StyledLinkContainer = styled.div`
margin-right: ${({ theme }) => theme.spacing(2)};
@ -104,9 +106,12 @@ export const SettingsAdminFeatureFlags = () => {
id: workspace.id,
title: workspace.name,
logo:
getImageAbsoluteURI(
isDefined(workspace.logo) ? workspace.logo : DEFAULT_WORKSPACE_LOGO,
) ?? '',
getImageAbsoluteURI({
imageUrl: isNonEmptyString(workspace.logo)
? workspace.logo
: DEFAULT_WORKSPACE_LOGO,
baseUrl: REACT_APP_SERVER_BASE_URL,
}) ?? '',
})) ?? [];
const renderWorkspaceContent = () => {

View File

@ -1,7 +1,8 @@
import { Meta, StoryObj } from '@storybook/react';
import { within } from '@storybook/test';
import { HttpResponse, graphql, http } from 'msw';
import { getImageAbsoluteURI } from 'twenty-ui';
import { getImageAbsoluteURI } from 'twenty-shared';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
import { SettingsServerlessFunctionDetail } from '~/pages/settings/serverless-functions/SettingsServerlessFunctionDetail';
import {
PageDecorator,
@ -43,9 +44,15 @@ const meta: Meta<PageDecoratorArgs> = {
},
});
}),
http.get(getImageAbsoluteURI(SOURCE_CODE_FULL_PATH) || '', () => {
return HttpResponse.text('export const handler = () => {}');
}),
http.get(
getImageAbsoluteURI({
imageUrl: SOURCE_CODE_FULL_PATH,
baseUrl: REACT_APP_SERVER_BASE_URL,
}) || '',
() => {
return HttpResponse.text('export const handler = () => {}');
},
),
],
},
},

View File

@ -24,7 +24,8 @@
"@/*": ["packages/twenty-front/src/modules/*"],
"~/*": ["packages/twenty-front/src/*"],
"twenty-ui": ["packages/twenty-ui/src/index.ts"],
"@ui/*": ["packages/twenty-ui/src/*"]
"@ui/*": ["packages/twenty-ui/src/*"],
"twenty-shared": ["packages/twenty-shared/dist"]
}
},
"files": [],

View File

@ -0,0 +1,15 @@
module.exports = {
extends: ['../../.eslintrc.cjs'],
ignorePatterns: ['!**/*'],
overrides: [
{
files: ['*.ts', '*.tsx'],
parserOptions: {
project: ['packages/twenty-shared/tsconfig.{json,*.json}'],
},
rules: {
'@nx/dependency-checks': 'error',
},
},
],
};

1
packages/twenty-shared/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
dist

View File

@ -0,0 +1,39 @@
import { JestConfigWithTsJest, pathsToModuleNameMapper } from 'ts-jest';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const tsConfig = require('./tsconfig.json');
const jestConfig: JestConfigWithTsJest = {
displayName: 'twenty-ui',
preset: '../../jest.preset.js',
testEnvironment: 'jsdom',
transformIgnorePatterns: ['../../node_modules/'],
transform: {
'^.+\\.[tj]sx?$': [
'@swc/jest',
{
jsc: {
parser: { syntax: 'typescript', tsx: true },
transform: { react: { runtime: 'automatic' } },
},
},
],
},
moduleNameMapper: {
'\\.(jpg|jpeg|png|gif|webp|svg|svg\\?react)$':
'<rootDir>/__mocks__/imageMock.js',
...pathsToModuleNameMapper(tsConfig.compilerOptions.paths),
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
extensionsToTreatAsEsm: ['.ts', '.tsx'],
coverageDirectory: './coverage',
coverageThreshold: {
global: {
statements: 100,
lines: 100,
functions: 100,
},
},
};
export default jestConfig;

View File

@ -0,0 +1,20 @@
{
"name": "twenty-shared",
"version": "0.40.0-canary",
"license": "AGPL-3.0",
"main": "./dist/index.js",
"scripts": {
"build": "npx vite build"
},
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.js"
}
},
"engines": {
"node": "^18.17.1",
"npm": "please-use-yarn",
"yarn": "^4.0.2"
}
}

View File

@ -0,0 +1,38 @@
{
"name": "twenty-shared",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "packages/twenty-shared/src",
"projectType": "library",
"tags": ["scope:shared"],
"targets": {
"build": {
"dependsOn": ["^build"],
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "{projectRoot}/dist"
}
},
"typecheck": {},
"test": {},
"lint": {
"options": {
"lintFilePatterns": [
"{projectRoot}/src/**/*.{ts,tsx,json}",
"{projectRoot}/package.json"
],
"reportUnusedDisableDirectives": "error"
},
"configurations": {
"fix": {}
}
},
"fmt": {
"options": {
"files": "src"
},
"configurations": {
"fix": {}
}
}
}
}

View File

@ -0,0 +1 @@
export * from './utils/image/getImageAbsoluteURI';

View File

@ -0,0 +1,38 @@
import { getImageAbsoluteURI } from '../getImageAbsoluteURI';
describe('getImageAbsoluteURI', () => {
it('should return baseUrl if imageUrl is empty string', () => {
const imageUrl = '';
const baseUrl = 'http://localhost:3000';
const result = getImageAbsoluteURI({ imageUrl, baseUrl });
expect(result).toBe('http://localhost:3000/files/');
});
it('should return absolute url if the imageUrl is an absolute url', () => {
const imageUrl = 'https://XXX';
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';
const result = getImageAbsoluteURI({ imageUrl, baseUrl });
expect(result).toBe('http://localhost:3000/files/path/pic.png');
});
it('should return fully formed url if imageUrl is a relative url nost starting with slash', () => {
const imageUrl = 'pic.png';
const baseUrl = 'http://localhost:3000';
const result = getImageAbsoluteURI({ imageUrl, baseUrl });
expect(result).toBe('http://localhost:3000/files/pic.png');
});
it('should handle queryParameters in the imageUrl', () => {
const imageUrl = '/pic.png?token=XXX';
const baseUrl = 'http://localhost:3000';
const result = getImageAbsoluteURI({ imageUrl, baseUrl });
expect(result).toBe('http://localhost:3000/files/pic.png?token=XXX');
});
});

View File

@ -0,0 +1,19 @@
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();
};

View File

@ -0,0 +1,24 @@
{
"compilerOptions": {
"allowJs": false,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"types": ["vite/client"],
"baseUrl": ".",
"paths": {
"twenty-shared": ["packages/twenty-shared/dist"]
}
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
],
"extends": "../../tsconfig.base.json"
}

View File

@ -0,0 +1,22 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../.cache/tsc",
"types": [
"node",
"vite/client"
]
},
"exclude": [
"**/*.spec.ts",
"**/*.test.ts",
"**/*.spec.tsx",
"**/*.test.tsx",
"**/*.spec.js",
"**/*.test.js",
"**/*.spec.jsx",
"**/*.test.jsx"
],
"include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"],
}

View File

@ -0,0 +1,17 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "commonjs",
"types": ["jest", "node"]
},
"include": [
"**/__mocks__/**/*",
"jest.config.ts",
"src/**/*.d.ts",
"src/**/*.spec.ts",
"src/**/*.spec.tsx",
"src/**/*.test.ts",
"src/**/*.test.tsx",
"vite.config.ts"
]
}

View File

@ -0,0 +1,33 @@
import * as path from 'path';
import { defineConfig } from 'vite';
import dts from 'vite-plugin-dts';
import tsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
root: __dirname,
cacheDir: '../../node_modules/.vite/packages/twenty-shared',
plugins: [
tsconfigPaths(),
dts({
entryRoot: 'src',
tsconfigPath: path.join(__dirname, 'tsconfig.lib.json'),
}),
],
// Configuration for building your library.
// See: https://vitejs.dev/guide/build.html#library-mode
build: {
outDir: './dist',
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
lib: {
entry: 'src/index.ts',
name: 'twenty-shared',
fileName: 'index',
formats: ['es', 'cjs'],
},
},
});

View File

@ -1,6 +1,6 @@
import { styled } from '@linaria/react';
import { isNonEmptyString, isUndefined } from '@sniptt/guards';
import { useContext, useMemo } from 'react';
import { useContext } from 'react';
import { useRecoilState } from 'recoil';
import { invalidAvatarUrlsState } from '@ui/display/avatar/components/states/isInvalidAvatarUrlState';
@ -9,12 +9,9 @@ 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,
isDefined,
stringToHslColor,
} from '@ui/utilities';
import { Nullable, stringToHslColor } from '@ui/utilities';
import { REACT_APP_SERVER_BASE_URL } from '@ui/utilities/config';
import { getImageAbsoluteURI } from 'twenty-shared';
const StyledAvatar = styled.div<{
size: AvatarSize;
@ -86,10 +83,12 @@ export const Avatar = ({
invalidAvatarUrlsState,
);
const avatarImageURI = useMemo(
() => (isDefined(avatarUrl) ? getImageAbsoluteURI(avatarUrl) : null),
[avatarUrl],
);
const avatarImageURI = isNonEmptyString(avatarUrl)
? getImageAbsoluteURI({
imageUrl: avatarUrl,
baseUrl: REACT_APP_SERVER_BASE_URL,
})
: null;
const noAvatarUrl = !isNonEmptyString(avatarImageURI);

View File

@ -1,21 +0,0 @@
import { getImageAbsoluteURI } from '../getImageAbsoluteURI';
describe('getImageAbsoluteURI', () => {
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);
expect(result).toBe('http://localhost:3000/files/XXX');
});
});

View File

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

View File

@ -6,7 +6,6 @@ export * from './animation/components/AnimatedTextWord';
export * from './animation/components/AnimatedTranslation';
export * from './color/utils/stringToHslColor';
export * from './dimensions/components/ComputeNodeDimensions';
export * from './image/getImageAbsoluteURI';
export * from './isDefined';
export * from './responsive/hooks/useIsMobile';
export * from './screen-size/hooks/useScreenSize';

View File

@ -10,7 +10,8 @@
"types": ["node"],
"outDir": "../../.cache/tsc",
"paths": {
"@ui/*": ["packages/twenty-ui/src/*"]
"@ui/*": ["packages/twenty-ui/src/*"],
"twenty-shared": ["packages/twenty-shared/dist"]
}
},
"files": [],

View File

@ -17,7 +17,8 @@
"baseUrl": ".",
"paths": {
"twenty-emails": ["packages/twenty-emails/src/index.ts"],
"twenty-ui": ["packages/twenty-ui/src/index.ts"]
"twenty-ui": ["packages/twenty-ui/src/index.ts"],
"twenty-shared": ["packages/twenty-shared/dist"]
}
},
"exclude": ["node_modules", "tmp"]

View File

@ -15925,16 +15925,6 @@ __metadata:
languageName: node
linkType: hard
"@types/jest@npm:^29.5.11":
version: 29.5.12
resolution: "@types/jest@npm:29.5.12"
dependencies:
expect: "npm:^29.0.0"
pretty-format: "npm:^29.0.0"
checksum: 10c0/25fc8e4c611fa6c4421e631432e9f0a6865a8cb07c9815ec9ac90d630271cad773b2ee5fe08066f7b95bebd18bb967f8ce05d018ee9ab0430f9dfd1d84665b6f
languageName: node
linkType: hard
"@types/js-cookie@npm:^2.2.6":
version: 2.2.7
resolution: "@types/js-cookie@npm:2.2.7"
@ -26153,7 +26143,7 @@ __metadata:
languageName: node
linkType: hard
"expect@npm:^29.0.0, expect@npm:^29.7.0":
"expect@npm:^29.7.0":
version: 29.7.0
resolution: "expect@npm:29.7.0"
dependencies:
@ -38284,7 +38274,7 @@ __metadata:
languageName: node
linkType: hard
"pretty-format@npm:^29.0.0, pretty-format@npm:^29.5.0, pretty-format@npm:^29.7.0":
"pretty-format@npm:^29.5.0, pretty-format@npm:^29.7.0":
version: 29.7.0
resolution: "pretty-format@npm:29.7.0"
dependencies:
@ -44020,6 +44010,12 @@ __metadata:
languageName: unknown
linkType: soft
"twenty-shared@workspace:packages/twenty-shared":
version: 0.0.0-use.local
resolution: "twenty-shared@workspace:packages/twenty-shared"
languageName: unknown
linkType: soft
"twenty-ui@workspace:packages/twenty-ui":
version: 0.0.0-use.local
resolution: "twenty-ui@workspace:packages/twenty-ui"
@ -44167,7 +44163,6 @@ __metadata:
"@types/facepaint": "npm:^1.2.5"
"@types/graphql-fields": "npm:^1.3.6"
"@types/graphql-upload": "npm:^8.0.12"
"@types/jest": "npm:^29.5.11"
"@types/js-cookie": "npm:^3.0.3"
"@types/js-levenshtein": "npm:^1.1.3"
"@types/lodash.camelcase": "npm:^4.3.7"