diff --git a/.github/workflows/ci-front.yaml b/.github/workflows/ci-front.yaml
index 910286ff1beb..95f18fc990fc 100644
--- a/.github/workflows/ci-front.yaml
+++ b/.github/workflows/ci-front.yaml
@@ -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'
diff --git a/.github/workflows/ci-server.yaml b/.github/workflows/ci-server.yaml
index 54e0fd112da1..5e71284fdc01 100644
--- a/.github/workflows/ci-server.yaml
+++ b/.github/workflows/ci-server.yaml
@@ -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'
diff --git a/.github/workflows/ci-shared.yaml b/.github/workflows/ci-shared.yaml
new file mode 100644
index 000000000000..41c334e054ed
--- /dev/null
+++ b/.github/workflows/ci-shared.yaml
@@ -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 }}
diff --git a/nx.json b/nx.json
index 4dad7946753f..d73a57ed16e1 100644
--- a/nx.json
+++ b/nx.json
@@ -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",
diff --git a/package.json b/package.json
index 28b28c383205..ce0115383e62 100644
--- a/package.json
+++ b/package.json
@@ -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"
]
}
diff --git a/packages/twenty-emails/src/emails/send-invite-link.email.tsx b/packages/twenty-emails/src/emails/send-invite-link.email.tsx
index 9468c1c26fe7..9d803a89a4fe 100644
--- a/packages/twenty-emails/src/emails/send-invite-link.email.tsx
+++ b/packages/twenty-emails/src/emails/send-invite-link.email.tsx
@@ -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 (
diff --git a/packages/twenty-emails/src/utils/getImageAbsoluteURI.ts b/packages/twenty-emails/src/utils/getImageAbsoluteURI.ts
deleted file mode 100644
index 2fb1c44c70fd..000000000000
--- a/packages/twenty-emails/src/utils/getImageAbsoluteURI.ts
+++ /dev/null
@@ -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}`;
-};
diff --git a/packages/twenty-emails/tsconfig.json b/packages/twenty-emails/tsconfig.json
index 463bf3689ab5..3340a4bd0449 100644
--- a/packages/twenty-emails/tsconfig.json
+++ b/packages/twenty-emails/tsconfig.json
@@ -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": [],
diff --git a/packages/twenty-front/index.html b/packages/twenty-front/index.html
index 644e29d74fe2..e144b28fb90b 100644
--- a/packages/twenty-front/index.html
+++ b/packages/twenty-front/index.html
@@ -3,7 +3,7 @@
-
+
diff --git a/packages/twenty-front/src/modules/auth/components/Logo.tsx b/packages/twenty-front/src/modules/auth/components/Logo.tsx
index d9ca7020182f..f1f5a01a6007 100644
--- a/packages/twenty-front/src/modules/auth/components/Logo.tsx
+++ b/packages/twenty-front/src/modules/auth/components/Logo.tsx
@@ -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 (
-
+
{secondaryLogoUrl && (
diff --git a/packages/twenty-front/src/modules/domain-manager/hooks/useGetPublicWorkspaceDataBySubdomain.ts b/packages/twenty-front/src/modules/domain-manager/hooks/useGetPublicWorkspaceDataBySubdomain.ts
index eff2046f2b9b..116ba20a0ec8 100644
--- a/packages/twenty-front/src/modules/domain-manager/hooks/useGetPublicWorkspaceDataBySubdomain.ts
+++ b/packages/twenty-front/src/modules/domain-manager/hooks/useGetPublicWorkspaceDataBySubdomain.ts
@@ -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,
};
};
diff --git a/packages/twenty-front/src/modules/navigation/components/AppNavigationDrawer.tsx b/packages/twenty-front/src/modules/navigation/components/AppNavigationDrawer.tsx
index 1c6fa34c06b3..93d392c7d080 100644
--- a/packages/twenty-front/src/modules/navigation/components/AppNavigationDrawer.tsx
+++ b/packages/twenty-front/src/modules/navigation/components/AppNavigationDrawer.tsx
@@ -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: ,
footer: ,
diff --git a/packages/twenty-front/src/modules/object-metadata/utils/getAvatarUrl.ts b/packages/twenty-front/src/modules/object-metadata/utils/getAvatarUrl.ts
index fa087f916a2c..00243cc5a94d 100644
--- a/packages/twenty-front/src/modules/object-metadata/utils/getAvatarUrl.ts
+++ b/packages/twenty-front/src/modules/object-metadata/utils/getAvatarUrl.ts
@@ -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,
+ })
: '';
}
diff --git a/packages/twenty-front/src/modules/ui/input/components/ImageInput.tsx b/packages/twenty-front/src/modules/ui/input/components/ImageInput.tsx
index 838238e443e7..e6cae6c5587a 100644
--- a/packages/twenty-front/src/modules/ui/input/components/ImageInput.tsx
+++ b/packages/twenty-front/src/modules/ui/input/components/ImageInput.tsx
@@ -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 (
diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdownButton.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdownButton.tsx
index cabda2f30402..fd8f69afd906 100644
--- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdownButton.tsx
+++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdownButton.tsx
@@ -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}
>
{currentWorkspace?.displayName ?? ''}
@@ -132,9 +136,12 @@ export const MultiWorkspaceDropdownButton = ({
text={workspace.displayName ?? '(No name)'}
avatar={
}
selected={currentWorkspace?.id === workspace.id}
diff --git a/packages/twenty-front/src/modules/ui/utilities/page-favicon/components/PageFavicon.tsx b/packages/twenty-front/src/modules/ui/utilities/page-favicon/components/PageFavicon.tsx
index c7913d67e11e..e72ff93a7df8 100644
--- a/packages/twenty-front/src/modules/ui/utilities/page-favicon/components/PageFavicon.tsx
+++ b/packages/twenty-front/src/modules/ui/utilities/page-favicon/components/PageFavicon.tsx
@@ -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 (
{workspacePublicData?.logo && (
)}
diff --git a/packages/twenty-front/src/modules/workspace/components/WorkspaceProviderEffect.tsx b/packages/twenty-front/src/modules/workspace/components/WorkspaceProviderEffect.tsx
index f7c67ba62c02..464153b60e5f 100644
--- a/packages/twenty-front/src/modules/workspace/components/WorkspaceProviderEffect.tsx
+++ b/packages/twenty-front/src/modules/workspace/components/WorkspaceProviderEffect.tsx
@@ -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(() => {
diff --git a/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdminFeatureFlags.tsx b/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdminFeatureFlags.tsx
index b90070e0c8e1..b8a7ab3d7675 100644
--- a/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdminFeatureFlags.tsx
+++ b/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdminFeatureFlags.tsx
@@ -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 = () => {
diff --git a/packages/twenty-front/src/pages/settings/serverless-functions/__stories__/SettingsServerlessFunctionDetail.stories.tsx b/packages/twenty-front/src/pages/settings/serverless-functions/__stories__/SettingsServerlessFunctionDetail.stories.tsx
index e03849b0dc4f..5a0fcf25d31a 100644
--- a/packages/twenty-front/src/pages/settings/serverless-functions/__stories__/SettingsServerlessFunctionDetail.stories.tsx
+++ b/packages/twenty-front/src/pages/settings/serverless-functions/__stories__/SettingsServerlessFunctionDetail.stories.tsx
@@ -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 = {
},
});
}),
- 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 = () => {}');
+ },
+ ),
],
},
},
diff --git a/packages/twenty-front/tsconfig.json b/packages/twenty-front/tsconfig.json
index 48b5bf3817a8..3e1173ac4bb3 100644
--- a/packages/twenty-front/tsconfig.json
+++ b/packages/twenty-front/tsconfig.json
@@ -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": [],
diff --git a/packages/twenty-shared/.eslintrc.cjs b/packages/twenty-shared/.eslintrc.cjs
new file mode 100644
index 000000000000..dba2eae3dc18
--- /dev/null
+++ b/packages/twenty-shared/.eslintrc.cjs
@@ -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',
+ },
+ },
+ ],
+};
diff --git a/packages/twenty-shared/.gitignore b/packages/twenty-shared/.gitignore
new file mode 100644
index 000000000000..1521c8b7652b
--- /dev/null
+++ b/packages/twenty-shared/.gitignore
@@ -0,0 +1 @@
+dist
diff --git a/packages/twenty-shared/jest.config.ts b/packages/twenty-shared/jest.config.ts
new file mode 100644
index 000000000000..e88407e7805f
--- /dev/null
+++ b/packages/twenty-shared/jest.config.ts
@@ -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)$':
+ '/__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;
diff --git a/packages/twenty-shared/package.json b/packages/twenty-shared/package.json
new file mode 100644
index 000000000000..8f6d615d972d
--- /dev/null
+++ b/packages/twenty-shared/package.json
@@ -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"
+ }
+}
diff --git a/packages/twenty-shared/project.json b/packages/twenty-shared/project.json
new file mode 100644
index 000000000000..acebb6f56841
--- /dev/null
+++ b/packages/twenty-shared/project.json
@@ -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": {}
+ }
+ }
+ }
+}
diff --git a/packages/twenty-shared/src/index.ts b/packages/twenty-shared/src/index.ts
new file mode 100644
index 000000000000..7665115392af
--- /dev/null
+++ b/packages/twenty-shared/src/index.ts
@@ -0,0 +1 @@
+export * from './utils/image/getImageAbsoluteURI';
diff --git a/packages/twenty-shared/src/utils/image/__tests__/getImageAbsoluteURI.test.ts b/packages/twenty-shared/src/utils/image/__tests__/getImageAbsoluteURI.test.ts
new file mode 100644
index 000000000000..b617a41164f1
--- /dev/null
+++ b/packages/twenty-shared/src/utils/image/__tests__/getImageAbsoluteURI.test.ts
@@ -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');
+ });
+});
diff --git a/packages/twenty-shared/src/utils/image/getImageAbsoluteURI.ts b/packages/twenty-shared/src/utils/image/getImageAbsoluteURI.ts
new file mode 100644
index 000000000000..8f041a4e4c8e
--- /dev/null
+++ b/packages/twenty-shared/src/utils/image/getImageAbsoluteURI.ts
@@ -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();
+};
diff --git a/packages/twenty-shared/tsconfig.json b/packages/twenty-shared/tsconfig.json
new file mode 100644
index 000000000000..bf6a9dfeb968
--- /dev/null
+++ b/packages/twenty-shared/tsconfig.json
@@ -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"
+}
diff --git a/packages/twenty-shared/tsconfig.lib.json b/packages/twenty-shared/tsconfig.lib.json
new file mode 100644
index 000000000000..3b7e0fe35ac4
--- /dev/null
+++ b/packages/twenty-shared/tsconfig.lib.json
@@ -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"],
+
+}
diff --git a/packages/twenty-shared/tsconfig.spec.json b/packages/twenty-shared/tsconfig.spec.json
new file mode 100644
index 000000000000..9f928c13c27f
--- /dev/null
+++ b/packages/twenty-shared/tsconfig.spec.json
@@ -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"
+ ]
+}
diff --git a/packages/twenty-shared/vite.config.ts b/packages/twenty-shared/vite.config.ts
new file mode 100644
index 000000000000..9a07cb0ca613
--- /dev/null
+++ b/packages/twenty-shared/vite.config.ts
@@ -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'],
+ },
+ },
+});
diff --git a/packages/twenty-ui/src/display/avatar/components/Avatar.tsx b/packages/twenty-ui/src/display/avatar/components/Avatar.tsx
index 532b7ab1e869..35941629b6b2 100644
--- a/packages/twenty-ui/src/display/avatar/components/Avatar.tsx
+++ b/packages/twenty-ui/src/display/avatar/components/Avatar.tsx
@@ -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);
diff --git a/packages/twenty-ui/src/utilities/image/__tests__/getImageAbsoluteURI.test.ts b/packages/twenty-ui/src/utilities/image/__tests__/getImageAbsoluteURI.test.ts
deleted file mode 100644
index d797d71bf4b1..000000000000
--- a/packages/twenty-ui/src/utilities/image/__tests__/getImageAbsoluteURI.test.ts
+++ /dev/null
@@ -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');
- });
-});
diff --git a/packages/twenty-ui/src/utilities/image/getImageAbsoluteURI.ts b/packages/twenty-ui/src/utilities/image/getImageAbsoluteURI.ts
deleted file mode 100644
index 13e4e2d45ffd..000000000000
--- a/packages/twenty-ui/src/utilities/image/getImageAbsoluteURI.ts
+++ /dev/null
@@ -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}`;
-};
diff --git a/packages/twenty-ui/src/utilities/index.ts b/packages/twenty-ui/src/utilities/index.ts
index f0cc4c26a4ec..d075abd62046 100644
--- a/packages/twenty-ui/src/utilities/index.ts
+++ b/packages/twenty-ui/src/utilities/index.ts
@@ -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';
diff --git a/packages/twenty-ui/tsconfig.json b/packages/twenty-ui/tsconfig.json
index 31fca817a180..437964eb1345 100644
--- a/packages/twenty-ui/tsconfig.json
+++ b/packages/twenty-ui/tsconfig.json
@@ -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": [],
diff --git a/tsconfig.base.json b/tsconfig.base.json
index 2a77eb672172..070d6c360d17 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -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"]
diff --git a/yarn.lock b/yarn.lock
index d94a503118c6..a280e481126d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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"