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  --------- Co-authored-by: Antoine Moreaux <moreaux.antoine@gmail.com> Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
committed by
GitHub
parent
4e329d08b0
commit
08a9db2df6
1
.github/workflows/ci-front.yaml
vendored
1
.github/workflows/ci-front.yaml
vendored
@ -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'
|
||||
|
||||
5
.github/workflows/ci-server.yaml
vendored
5
.github/workflows/ci-server.yaml
vendored
@ -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
48
.github/workflows/ci-shared.yaml
vendored
Normal 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
12
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",
|
||||
|
||||
@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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}`;
|
||||
};
|
||||
@ -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": [],
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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} />
|
||||
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@ -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 />,
|
||||
|
||||
@ -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,
|
||||
})
|
||||
: '';
|
||||
}
|
||||
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
@ -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 = () => {
|
||||
|
||||
@ -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) || '', () => {
|
||||
http.get(
|
||||
getImageAbsoluteURI({
|
||||
imageUrl: SOURCE_CODE_FULL_PATH,
|
||||
baseUrl: REACT_APP_SERVER_BASE_URL,
|
||||
}) || '',
|
||||
() => {
|
||||
return HttpResponse.text('export const handler = () => {}');
|
||||
}),
|
||||
},
|
||||
),
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@ -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": [],
|
||||
|
||||
15
packages/twenty-shared/.eslintrc.cjs
Normal file
15
packages/twenty-shared/.eslintrc.cjs
Normal 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
1
packages/twenty-shared/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
dist
|
||||
39
packages/twenty-shared/jest.config.ts
Normal file
39
packages/twenty-shared/jest.config.ts
Normal 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;
|
||||
20
packages/twenty-shared/package.json
Normal file
20
packages/twenty-shared/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
38
packages/twenty-shared/project.json
Normal file
38
packages/twenty-shared/project.json
Normal 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": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1
packages/twenty-shared/src/index.ts
Normal file
1
packages/twenty-shared/src/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './utils/image/getImageAbsoluteURI';
|
||||
@ -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');
|
||||
});
|
||||
});
|
||||
@ -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();
|
||||
};
|
||||
24
packages/twenty-shared/tsconfig.json
Normal file
24
packages/twenty-shared/tsconfig.json
Normal 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"
|
||||
}
|
||||
22
packages/twenty-shared/tsconfig.lib.json
Normal file
22
packages/twenty-shared/tsconfig.lib.json
Normal 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"],
|
||||
|
||||
}
|
||||
17
packages/twenty-shared/tsconfig.spec.json
Normal file
17
packages/twenty-shared/tsconfig.spec.json
Normal 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"
|
||||
]
|
||||
}
|
||||
33
packages/twenty-shared/vite.config.ts
Normal file
33
packages/twenty-shared/vite.config.ts
Normal 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'],
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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');
|
||||
});
|
||||
});
|
||||
@ -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}`;
|
||||
};
|
||||
@ -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';
|
||||
|
||||
@ -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": [],
|
||||
|
||||
@ -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"]
|
||||
|
||||
21
yarn.lock
21
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"
|
||||
|
||||
Reference in New Issue
Block a user