From 08a9db2df6608cf3bf33f27a22addc4be9a8c595 Mon Sep 17 00:00:00 2001 From: Mohammed Abdul Razak Wahab <60781022+mdrazak2001@users.noreply.github.com> Date: Tue, 17 Dec 2024 13:54:21 +0530 Subject: [PATCH] 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 Co-authored-by: Charles Bochet --- .github/workflows/ci-front.yaml | 1 + .github/workflows/ci-server.yaml | 5 ++ .github/workflows/ci-shared.yaml | 48 +++++++++++++++++++ nx.json | 12 +++-- package.json | 2 +- .../src/emails/send-invite-link.email.tsx | 4 +- .../src/utils/getImageAbsoluteURI.ts | 9 ---- packages/twenty-emails/tsconfig.json | 6 ++- packages/twenty-front/index.html | 2 +- .../src/modules/auth/components/Logo.tsx | 19 +++++--- .../useGetPublicWorkspaceDataBySubdomain.ts | 4 +- .../components/AppNavigationDrawer.tsx | 17 ++++--- .../object-metadata/utils/getAvatarUrl.ts | 13 +++-- .../ui/input/components/ImageInput.tsx | 16 +++---- .../MultiWorkspaceDropdownButton.tsx | 21 +++++--- .../page-favicon/components/PageFavicon.tsx | 11 +++-- .../components/WorkspaceProviderEffect.tsx | 13 ++--- .../admin-panel/SettingsAdminFeatureFlags.tsx | 13 +++-- ...ttingsServerlessFunctionDetail.stories.tsx | 15 ++++-- packages/twenty-front/tsconfig.json | 3 +- packages/twenty-shared/.eslintrc.cjs | 15 ++++++ packages/twenty-shared/.gitignore | 1 + packages/twenty-shared/jest.config.ts | 39 +++++++++++++++ packages/twenty-shared/package.json | 20 ++++++++ packages/twenty-shared/project.json | 38 +++++++++++++++ packages/twenty-shared/src/index.ts | 1 + .../__tests__/getImageAbsoluteURI.test.ts | 38 +++++++++++++++ .../src/utils/image/getImageAbsoluteURI.ts | 19 ++++++++ packages/twenty-shared/tsconfig.json | 24 ++++++++++ packages/twenty-shared/tsconfig.lib.json | 22 +++++++++ packages/twenty-shared/tsconfig.spec.json | 17 +++++++ packages/twenty-shared/vite.config.ts | 33 +++++++++++++ .../src/display/avatar/components/Avatar.tsx | 21 ++++---- .../__tests__/getImageAbsoluteURI.test.ts | 21 -------- .../utilities/image/getImageAbsoluteURI.ts | 11 ----- packages/twenty-ui/src/utilities/index.ts | 1 - packages/twenty-ui/tsconfig.json | 3 +- tsconfig.base.json | 3 +- yarn.lock | 21 ++++---- 39 files changed, 453 insertions(+), 129 deletions(-) create mode 100644 .github/workflows/ci-shared.yaml delete mode 100644 packages/twenty-emails/src/utils/getImageAbsoluteURI.ts create mode 100644 packages/twenty-shared/.eslintrc.cjs create mode 100644 packages/twenty-shared/.gitignore create mode 100644 packages/twenty-shared/jest.config.ts create mode 100644 packages/twenty-shared/package.json create mode 100644 packages/twenty-shared/project.json create mode 100644 packages/twenty-shared/src/index.ts create mode 100644 packages/twenty-shared/src/utils/image/__tests__/getImageAbsoluteURI.test.ts create mode 100644 packages/twenty-shared/src/utils/image/getImageAbsoluteURI.ts create mode 100644 packages/twenty-shared/tsconfig.json create mode 100644 packages/twenty-shared/tsconfig.lib.json create mode 100644 packages/twenty-shared/tsconfig.spec.json create mode 100644 packages/twenty-shared/vite.config.ts delete mode 100644 packages/twenty-ui/src/utilities/image/__tests__/getImageAbsoluteURI.test.ts delete mode 100644 packages/twenty-ui/src/utilities/image/getImageAbsoluteURI.ts 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"