Make token update synchronous on FE (#11486)
1. Removing tokenPair internal variable of ApolloFactory. We will relay on cookieStorage 2. setting the cookie explicitely instead of only relaying on recoil cookieEffect which is too late
This commit is contained in:
@ -127,7 +127,7 @@ export const CalendarEventDetails = ({
|
|||||||
size={ChipSize.Large}
|
size={ChipSize.Large}
|
||||||
variant={ChipVariant.Highlighted}
|
variant={ChipVariant.Highlighted}
|
||||||
clickable={false}
|
clickable={false}
|
||||||
leftComponent={() => <IconCalendarEvent size={theme.icon.size.md} />}
|
leftComponent={<IconCalendarEvent size={theme.icon.size.md} />}
|
||||||
label="Event"
|
label="Event"
|
||||||
/>
|
/>
|
||||||
<StyledHeader>
|
<StyledHeader>
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { MessageThreadSubscriber } from '@/activities/emails/types/MessageThreadSubscriber';
|
import { MessageThreadSubscriber } from '@/activities/emails/types/MessageThreadSubscriber';
|
||||||
import { isNonEmptyString } from '@sniptt/guards';
|
import { isNonEmptyString } from '@sniptt/guards';
|
||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import { Avatar, AvatarGroup, IconChevronDown } from 'twenty-ui/display';
|
|
||||||
import { Chip, ChipVariant } from 'twenty-ui/components';
|
import { Chip, ChipVariant } from 'twenty-ui/components';
|
||||||
|
import { Avatar, AvatarGroup, IconChevronDown } from 'twenty-ui/display';
|
||||||
import { ThemeContext } from 'twenty-ui/theme';
|
import { ThemeContext } from 'twenty-ui/theme';
|
||||||
|
|
||||||
const MAX_NUMBER_OF_AVATARS = 3;
|
const MAX_NUMBER_OF_AVATARS = 3;
|
||||||
@ -46,20 +46,16 @@ export const MessageThreadSubscribersChip = ({
|
|||||||
<Chip
|
<Chip
|
||||||
label={label}
|
label={label}
|
||||||
variant={ChipVariant.Highlighted}
|
variant={ChipVariant.Highlighted}
|
||||||
leftComponent={() => {
|
leftComponent={
|
||||||
if (isOnlyOneSubscriber) {
|
isOnlyOneSubscriber ? (
|
||||||
return (
|
<Avatar
|
||||||
<Avatar
|
avatarUrl={firstAvatarUrl}
|
||||||
avatarUrl={firstAvatarUrl}
|
placeholderColorSeed={firstAvatarColorSeed}
|
||||||
placeholderColorSeed={firstAvatarColorSeed}
|
placeholder={firstAvatarPlaceholder}
|
||||||
placeholder={firstAvatarPlaceholder}
|
size="md"
|
||||||
size="md"
|
type={'rounded'}
|
||||||
type={'rounded'}
|
/>
|
||||||
/>
|
) : (
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AvatarGroup
|
<AvatarGroup
|
||||||
avatars={subscriberNames.map((name, index) => (
|
avatars={subscriberNames.map((name, index) => (
|
||||||
<Avatar
|
<Avatar
|
||||||
@ -70,8 +66,8 @@ export const MessageThreadSubscribersChip = ({
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
}}
|
}
|
||||||
rightComponent={() => <IconChevronDown size={theme.icon.size.sm} />}
|
rightComponent={() => <IconChevronDown size={theme.icon.size.sm} />}
|
||||||
clickable
|
clickable
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -16,8 +16,8 @@ import { useUpdateEffect } from '~/hooks/useUpdateEffect';
|
|||||||
|
|
||||||
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
|
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
|
||||||
import { AppPath } from '@/types/AppPath';
|
import { AppPath } from '@/types/AppPath';
|
||||||
import { ApolloFactory, Options } from '../services/apollo.factory';
|
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
import { ApolloFactory, Options } from '../services/apollo.factory';
|
||||||
|
|
||||||
export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
|
export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
|
||||||
// eslint-disable-next-line @nx/workspace-no-state-useref
|
// eslint-disable-next-line @nx/workspace-no-state-useref
|
||||||
@ -26,7 +26,7 @@ export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
|
|||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { isMatchingLocation } = useIsMatchingLocation();
|
const { isMatchingLocation } = useIsMatchingLocation();
|
||||||
const [tokenPair, setTokenPair] = useRecoilState(tokenPairState);
|
const setTokenPair = useSetRecoilState(tokenPairState);
|
||||||
const [currentWorkspace, setCurrentWorkspace] = useRecoilState(
|
const [currentWorkspace, setCurrentWorkspace] = useRecoilState(
|
||||||
currentWorkspaceState,
|
currentWorkspaceState,
|
||||||
);
|
);
|
||||||
@ -62,8 +62,6 @@ export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
connectToDevTools: isDebugMode,
|
connectToDevTools: isDebugMode,
|
||||||
// We don't want to re-create the client on token change or it will cause infinite loop
|
|
||||||
initialTokenPair: tokenPair,
|
|
||||||
currentWorkspaceMember: currentWorkspaceMember,
|
currentWorkspaceMember: currentWorkspaceMember,
|
||||||
onTokenPairChange: (tokenPair) => {
|
onTokenPairChange: (tokenPair) => {
|
||||||
setTokenPair(tokenPair);
|
setTokenPair(tokenPair);
|
||||||
@ -104,12 +102,6 @@ export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
|
|||||||
setPreviousUrl,
|
setPreviousUrl,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useUpdateEffect(() => {
|
|
||||||
if (isDefined(apolloRef.current)) {
|
|
||||||
apolloRef.current.updateTokenPair(tokenPair);
|
|
||||||
}
|
|
||||||
}, [tokenPair]);
|
|
||||||
|
|
||||||
useUpdateEffect(() => {
|
useUpdateEffect(() => {
|
||||||
if (isDefined(apolloRef.current)) {
|
if (isDefined(apolloRef.current)) {
|
||||||
apolloRef.current.updateWorkspaceMember(currentWorkspaceMember);
|
apolloRef.current.updateWorkspaceMember(currentWorkspaceMember);
|
||||||
|
|||||||
@ -33,10 +33,6 @@ const mockWorkspaceMember = {
|
|||||||
|
|
||||||
const createMockOptions = (): Options<any> => ({
|
const createMockOptions = (): Options<any> => ({
|
||||||
uri: 'http://localhost:3000',
|
uri: 'http://localhost:3000',
|
||||||
initialTokenPair: {
|
|
||||||
accessToken: { token: 'mockAccessToken', expiresAt: '' },
|
|
||||||
refreshToken: { token: 'mockRefreshToken', expiresAt: '' },
|
|
||||||
},
|
|
||||||
currentWorkspaceMember: mockWorkspaceMember,
|
currentWorkspaceMember: mockWorkspaceMember,
|
||||||
cache: new InMemoryCache(),
|
cache: new InMemoryCache(),
|
||||||
isDebugMode: true,
|
isDebugMode: true,
|
||||||
|
|||||||
@ -18,9 +18,11 @@ import { logDebug } from '~/utils/logDebug';
|
|||||||
|
|
||||||
import { i18n } from '@lingui/core';
|
import { i18n } from '@lingui/core';
|
||||||
import { GraphQLFormattedError } from 'graphql';
|
import { GraphQLFormattedError } from 'graphql';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
import { cookieStorage } from '~/utils/cookie-storage';
|
||||||
|
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||||
import { ApolloManager } from '../types/apolloManager.interface';
|
import { ApolloManager } from '../types/apolloManager.interface';
|
||||||
import { loggerLink } from '../utils/loggerLink';
|
import { loggerLink } from '../utils/loggerLink';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
|
||||||
|
|
||||||
const logger = loggerLink(() => 'Twenty');
|
const logger = loggerLink(() => 'Twenty');
|
||||||
|
|
||||||
@ -29,7 +31,6 @@ export interface Options<TCacheShape> extends ApolloClientOptions<TCacheShape> {
|
|||||||
onNetworkError?: (err: Error | ServerParseError | ServerError) => void;
|
onNetworkError?: (err: Error | ServerParseError | ServerError) => void;
|
||||||
onTokenPairChange?: (tokenPair: AuthTokenPair) => void;
|
onTokenPairChange?: (tokenPair: AuthTokenPair) => void;
|
||||||
onUnauthenticatedError?: () => void;
|
onUnauthenticatedError?: () => void;
|
||||||
initialTokenPair: AuthTokenPair | null;
|
|
||||||
currentWorkspaceMember: CurrentWorkspaceMember | null;
|
currentWorkspaceMember: CurrentWorkspaceMember | null;
|
||||||
extraLinks?: ApolloLink[];
|
extraLinks?: ApolloLink[];
|
||||||
isDebugMode?: boolean;
|
isDebugMode?: boolean;
|
||||||
@ -37,7 +38,6 @@ export interface Options<TCacheShape> extends ApolloClientOptions<TCacheShape> {
|
|||||||
|
|
||||||
export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
|
export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
|
||||||
private client: ApolloClient<TCacheShape>;
|
private client: ApolloClient<TCacheShape>;
|
||||||
private tokenPair: AuthTokenPair | null = null;
|
|
||||||
private currentWorkspaceMember: CurrentWorkspaceMember | null = null;
|
private currentWorkspaceMember: CurrentWorkspaceMember | null = null;
|
||||||
|
|
||||||
constructor(opts: Options<TCacheShape>) {
|
constructor(opts: Options<TCacheShape>) {
|
||||||
@ -47,28 +47,45 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
|
|||||||
onNetworkError,
|
onNetworkError,
|
||||||
onTokenPairChange,
|
onTokenPairChange,
|
||||||
onUnauthenticatedError,
|
onUnauthenticatedError,
|
||||||
initialTokenPair,
|
|
||||||
currentWorkspaceMember,
|
currentWorkspaceMember,
|
||||||
extraLinks,
|
extraLinks,
|
||||||
isDebugMode,
|
isDebugMode,
|
||||||
...options
|
...options
|
||||||
} = opts;
|
} = opts;
|
||||||
|
|
||||||
this.tokenPair = initialTokenPair;
|
|
||||||
this.currentWorkspaceMember = currentWorkspaceMember;
|
this.currentWorkspaceMember = currentWorkspaceMember;
|
||||||
|
|
||||||
|
const getTokenPair = () => {
|
||||||
|
const stringTokenPair = cookieStorage.getItem('tokenPair');
|
||||||
|
const tokenPair = isDefined(stringTokenPair)
|
||||||
|
? (JSON.parse(stringTokenPair) as AuthTokenPair)
|
||||||
|
: undefined;
|
||||||
|
return tokenPair;
|
||||||
|
};
|
||||||
|
|
||||||
const buildApolloLink = (): ApolloLink => {
|
const buildApolloLink = (): ApolloLink => {
|
||||||
const httpLink = createUploadLink({
|
const httpLink = createUploadLink({
|
||||||
uri,
|
uri,
|
||||||
});
|
});
|
||||||
|
|
||||||
const authLink = setContext(async (_, { headers }) => {
|
const authLink = setContext(async (_, { headers }) => {
|
||||||
|
const tokenPair = getTokenPair();
|
||||||
|
|
||||||
|
if (isUndefinedOrNull(tokenPair)) {
|
||||||
|
return {
|
||||||
|
headers: {
|
||||||
|
...headers,
|
||||||
|
...options.headers,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
headers: {
|
headers: {
|
||||||
...headers,
|
...headers,
|
||||||
...options.headers,
|
...options.headers,
|
||||||
authorization: this.tokenPair?.accessToken.token
|
authorization: tokenPair.accessToken.token
|
||||||
? `Bearer ${this.tokenPair?.accessToken.token}`
|
? `Bearer ${tokenPair.accessToken.token}`
|
||||||
: '',
|
: '',
|
||||||
...(this.currentWorkspaceMember?.locale
|
...(this.currentWorkspaceMember?.locale
|
||||||
? { 'x-locale': this.currentWorkspaceMember.locale }
|
? { 'x-locale': this.currentWorkspaceMember.locale }
|
||||||
@ -93,7 +110,7 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
|
|||||||
for (const graphQLError of graphQLErrors) {
|
for (const graphQLError of graphQLErrors) {
|
||||||
if (graphQLError.message === 'Unauthorized') {
|
if (graphQLError.message === 'Unauthorized') {
|
||||||
return fromPromise(
|
return fromPromise(
|
||||||
renewToken(uri, this.tokenPair)
|
renewToken(uri, getTokenPair())
|
||||||
.then((tokens) => {
|
.then((tokens) => {
|
||||||
if (isDefined(tokens)) {
|
if (isDefined(tokens)) {
|
||||||
onTokenPairChange?.(tokens);
|
onTokenPairChange?.(tokens);
|
||||||
@ -108,10 +125,14 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
|
|||||||
switch (graphQLError?.extensions?.code) {
|
switch (graphQLError?.extensions?.code) {
|
||||||
case 'UNAUTHENTICATED': {
|
case 'UNAUTHENTICATED': {
|
||||||
return fromPromise(
|
return fromPromise(
|
||||||
renewToken(uri, this.tokenPair)
|
renewToken(uri, getTokenPair())
|
||||||
.then((tokens) => {
|
.then((tokens) => {
|
||||||
if (isDefined(tokens)) {
|
if (isDefined(tokens)) {
|
||||||
onTokenPairChange?.(tokens);
|
onTokenPairChange?.(tokens);
|
||||||
|
cookieStorage.setItem(
|
||||||
|
'tokenPair',
|
||||||
|
JSON.stringify(tokens),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@ -162,10 +183,6 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTokenPair(tokenPair: AuthTokenPair | null) {
|
|
||||||
this.tokenPair = tokenPair;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateWorkspaceMember(workspaceMember: CurrentWorkspaceMember | null) {
|
updateWorkspaceMember(workspaceMember: CurrentWorkspaceMember | null) {
|
||||||
this.currentWorkspaceMember = workspaceMember;
|
this.currentWorkspaceMember = workspaceMember;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
import { ApolloClient } from '@apollo/client';
|
import { ApolloClient } from '@apollo/client';
|
||||||
|
|
||||||
import { CurrentWorkspaceMember } from '@/auth/states/currentWorkspaceMemberState';
|
import { CurrentWorkspaceMember } from '@/auth/states/currentWorkspaceMemberState';
|
||||||
import { AuthTokenPair } from '~/generated/graphql';
|
|
||||||
|
|
||||||
export interface ApolloManager<TCacheShape> {
|
export interface ApolloManager<TCacheShape> {
|
||||||
getClient(): ApolloClient<TCacheShape>;
|
getClient(): ApolloClient<TCacheShape>;
|
||||||
updateTokenPair(tokenPair: AuthTokenPair | null): void;
|
|
||||||
updateWorkspaceMember(workspaceMember: CurrentWorkspaceMember | null): void;
|
updateWorkspaceMember(workspaceMember: CurrentWorkspaceMember | null): void;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -63,6 +63,7 @@ import { useSearchParams } from 'react-router-dom';
|
|||||||
import { APP_LOCALES } from 'twenty-shared/translations';
|
import { APP_LOCALES } from 'twenty-shared/translations';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { iconsState } from 'twenty-ui/display';
|
import { iconsState } from 'twenty-ui/display';
|
||||||
|
import { cookieStorage } from '~/utils/cookie-storage';
|
||||||
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
|
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
|
||||||
import { dynamicActivate } from '~/utils/i18n/dynamicActivate';
|
import { dynamicActivate } from '~/utils/i18n/dynamicActivate';
|
||||||
|
|
||||||
@ -348,6 +349,12 @@ export const useAuth = () => {
|
|||||||
setTokenPair(
|
setTokenPair(
|
||||||
getAuthTokensResult.data?.getAuthTokensFromLoginToken.tokens,
|
getAuthTokensResult.data?.getAuthTokensFromLoginToken.tokens,
|
||||||
);
|
);
|
||||||
|
cookieStorage.setItem(
|
||||||
|
'tokenPair',
|
||||||
|
JSON.stringify(
|
||||||
|
getAuthTokensResult.data?.getAuthTokensFromLoginToken.tokens,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
await refreshObjectMetadataItems();
|
await refreshObjectMetadataItems();
|
||||||
await loadCurrentUser();
|
await loadCurrentUser();
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { useEmailsField } from '@/object-record/record-field/meta-types/hooks/useEmailsField';
|
import { useEmailsFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useEmailsFieldDisplay';
|
||||||
import { EmailsDisplay } from '@/ui/field/display/components/EmailsDisplay';
|
import { EmailsDisplay } from '@/ui/field/display/components/EmailsDisplay';
|
||||||
|
|
||||||
export const EmailsFieldDisplay = () => {
|
export const EmailsFieldDisplay = () => {
|
||||||
const { fieldValue } = useEmailsField();
|
const { fieldValue } = useEmailsFieldDisplay();
|
||||||
|
|
||||||
return <EmailsDisplay value={fieldValue} />;
|
return <EmailsDisplay value={fieldValue} />;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,9 +2,7 @@ import { Theme, withTheme } from '@emotion/react';
|
|||||||
import { styled } from '@linaria/react';
|
import { styled } from '@linaria/react';
|
||||||
import { Ref } from 'react';
|
import { Ref } from 'react';
|
||||||
|
|
||||||
const StyledOuterContainer = styled.div<{
|
const StyledOuterContainer = styled.div`
|
||||||
hasSoftFocus?: boolean;
|
|
||||||
}>`
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@ -50,7 +48,6 @@ export const RecordTableCellDisplayContainer = ({
|
|||||||
}
|
}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
ref={scrollRef}
|
ref={scrollRef}
|
||||||
hasSoftFocus={softFocus}
|
|
||||||
onContextMenu={onContextMenu}
|
onContextMenu={onContextMenu}
|
||||||
>
|
>
|
||||||
{placeholderForEmptyCell ? (
|
{placeholderForEmptyCell ? (
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
import { useMemo } from 'react';
|
|
||||||
|
|
||||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
|
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
|
||||||
import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
||||||
@ -9,6 +7,7 @@ import { isTableCellInEditModeComponentFamilyState } from '@/object-record/recor
|
|||||||
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
|
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
|
||||||
import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
|
import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
|
||||||
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
|
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
export const RecordTableCellWrapper = ({
|
export const RecordTableCellWrapper = ({
|
||||||
children,
|
children,
|
||||||
|
|||||||
@ -18,25 +18,19 @@ const StyledTd = styled.td<{
|
|||||||
width?: number;
|
width?: number;
|
||||||
}>`
|
}>`
|
||||||
border-bottom: 1px solid
|
border-bottom: 1px solid
|
||||||
${({ borderColor, hasBottomBorder }) =>
|
${({ borderColor, hasBottomBorder, isDragging }) =>
|
||||||
hasBottomBorder ? borderColor : 'transparent'};
|
hasBottomBorder && !isDragging ? borderColor : 'transparent'};
|
||||||
color: ${({ fontColor }) => fontColor};
|
color: ${({ fontColor }) => fontColor};
|
||||||
border-right: ${({ borderColor, hasRightBorder }) =>
|
border-right: ${({ borderColor, hasRightBorder, isDragging }) =>
|
||||||
hasRightBorder ? `1px solid ${borderColor}` : 'none'};
|
hasRightBorder && !isDragging ? `1px solid ${borderColor}` : 'none'};
|
||||||
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
transition: 0.3s ease;
|
transition: 0.3s ease;
|
||||||
|
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
||||||
background: ${({ backgroundColor }) => backgroundColor};
|
background: ${({ backgroundColor, isDragging }) =>
|
||||||
${({ isDragging }) =>
|
isDragging ? 'transparent' : backgroundColor};
|
||||||
isDragging
|
|
||||||
? `
|
|
||||||
background-color: transparent;
|
|
||||||
border-color: transparent;
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
|
|
||||||
${({ freezeFirstColumns }) =>
|
${({ freezeFirstColumns }) =>
|
||||||
freezeFirstColumns
|
freezeFirstColumns
|
||||||
|
|||||||
@ -6,10 +6,9 @@ import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
|||||||
import { onToggleColumnFilterComponentState } from '@/object-record/record-table/states/onToggleColumnFilterComponentState';
|
import { onToggleColumnFilterComponentState } from '@/object-record/record-table/states/onToggleColumnFilterComponentState';
|
||||||
import { onToggleColumnSortComponentState } from '@/object-record/record-table/states/onToggleColumnSortComponentState';
|
import { onToggleColumnSortComponentState } from '@/object-record/record-table/states/onToggleColumnSortComponentState';
|
||||||
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
|
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
|
||||||
|
import { useToggleScrollWrapper } from '@/ui/utilities/scroll/hooks/useToggleScrollWrapper';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
import { useLingui } from '@lingui/react/macro';
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import { useTableColumns } from '../../hooks/useTableColumns';
|
|
||||||
import { ColumnDefinition } from '../../types/ColumnDefinition';
|
|
||||||
import {
|
import {
|
||||||
IconArrowLeft,
|
IconArrowLeft,
|
||||||
IconArrowRight,
|
IconArrowRight,
|
||||||
@ -18,6 +17,8 @@ import {
|
|||||||
IconSortDescending,
|
IconSortDescending,
|
||||||
} from 'twenty-ui/display';
|
} from 'twenty-ui/display';
|
||||||
import { MenuItem } from 'twenty-ui/navigation';
|
import { MenuItem } from 'twenty-ui/navigation';
|
||||||
|
import { useTableColumns } from '../../hooks/useTableColumns';
|
||||||
|
import { ColumnDefinition } from '../../types/ColumnDefinition';
|
||||||
|
|
||||||
export type RecordTableColumnHeadDropdownMenuProps = {
|
export type RecordTableColumnHeadDropdownMenuProps = {
|
||||||
column: ColumnDefinition<FieldMetadata>;
|
column: ColumnDefinition<FieldMetadata>;
|
||||||
@ -28,6 +29,9 @@ export const RecordTableColumnHeadDropdownMenu = ({
|
|||||||
}: RecordTableColumnHeadDropdownMenuProps) => {
|
}: RecordTableColumnHeadDropdownMenuProps) => {
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
|
|
||||||
|
const { toggleScrollXWrapper, toggleScrollYWrapper } =
|
||||||
|
useToggleScrollWrapper();
|
||||||
|
|
||||||
const visibleTableColumns = useRecoilComponentValueV2(
|
const visibleTableColumns = useRecoilComponentValueV2(
|
||||||
visibleTableColumnsComponentSelector,
|
visibleTableColumnsComponentSelector,
|
||||||
);
|
);
|
||||||
@ -46,16 +50,21 @@ export const RecordTableColumnHeadDropdownMenu = ({
|
|||||||
|
|
||||||
const { closeDropdown } = useDropdown(column.fieldMetadataId + '-header');
|
const { closeDropdown } = useDropdown(column.fieldMetadataId + '-header');
|
||||||
|
|
||||||
const handleColumnMoveLeft = () => {
|
const closeDropdownAndToggleScroll = () => {
|
||||||
closeDropdown();
|
closeDropdown();
|
||||||
|
toggleScrollXWrapper(true);
|
||||||
|
toggleScrollYWrapper(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleColumnMoveLeft = () => {
|
||||||
|
closeDropdownAndToggleScroll();
|
||||||
if (!canMoveLeft) return;
|
if (!canMoveLeft) return;
|
||||||
|
|
||||||
handleMoveTableColumn('left', column);
|
handleMoveTableColumn('left', column);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleColumnMoveRight = () => {
|
const handleColumnMoveRight = () => {
|
||||||
closeDropdown();
|
closeDropdownAndToggleScroll();
|
||||||
|
|
||||||
if (!canMoveRight) return;
|
if (!canMoveRight) return;
|
||||||
|
|
||||||
@ -63,7 +72,7 @@ export const RecordTableColumnHeadDropdownMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleColumnVisibility = () => {
|
const handleColumnVisibility = () => {
|
||||||
closeDropdown();
|
closeDropdownAndToggleScroll();
|
||||||
handleColumnVisibilityChange(column);
|
handleColumnVisibilityChange(column);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -75,13 +84,13 @@ export const RecordTableColumnHeadDropdownMenu = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleSortClick = () => {
|
const handleSortClick = () => {
|
||||||
closeDropdown();
|
closeDropdownAndToggleScroll();
|
||||||
|
|
||||||
onToggleColumnSort?.(column.fieldMetadataId);
|
onToggleColumnSort?.(column.fieldMetadataId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFilterClick = () => {
|
const handleFilterClick = () => {
|
||||||
closeDropdown();
|
closeDropdownAndToggleScroll();
|
||||||
|
|
||||||
onToggleColumnFilter?.(column.fieldMetadataId);
|
onToggleColumnFilter?.(column.fieldMetadataId);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
|
||||||
import { isRowVisibleComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowVisibleComponentFamilyState';
|
import { isRowVisibleComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowVisibleComponentFamilyState';
|
||||||
import { useScrollWrapperElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperElement';
|
import { useScrollWrapperElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperElement';
|
||||||
import { useSetRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentFamilyStateV2';
|
import { useSetRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentFamilyStateV2';
|
||||||
@ -9,7 +8,6 @@ type RecordTableTrEffectProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const RecordTableTrEffect = ({ recordId }: RecordTableTrEffectProps) => {
|
export const RecordTableTrEffect = ({ recordId }: RecordTableTrEffectProps) => {
|
||||||
const { onIndexRecordsLoaded } = useRecordIndexContextOrThrow();
|
|
||||||
const { scrollWrapperHTMLElement } = useScrollWrapperElement();
|
const { scrollWrapperHTMLElement } = useScrollWrapperElement();
|
||||||
|
|
||||||
const setIsRowVisible = useSetRecoilComponentFamilyStateV2(
|
const setIsRowVisible = useSetRecoilComponentFamilyStateV2(
|
||||||
@ -29,7 +27,6 @@ export const RecordTableTrEffect = ({ recordId }: RecordTableTrEffectProps) => {
|
|||||||
const isIntersecting = entry.isIntersecting;
|
const isIntersecting = entry.isIntersecting;
|
||||||
|
|
||||||
if (isIntersecting) {
|
if (isIntersecting) {
|
||||||
onIndexRecordsLoaded?.();
|
|
||||||
setIsRowVisible(true);
|
setIsRowVisible(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,12 +47,7 @@ export const RecordTableTrEffect = ({ recordId }: RecordTableTrEffectProps) => {
|
|||||||
return () => {
|
return () => {
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
};
|
};
|
||||||
}, [
|
}, [recordId, scrollWrapperHTMLElement, setIsRowVisible]);
|
||||||
onIndexRecordsLoaded,
|
|
||||||
recordId,
|
|
||||||
scrollWrapperHTMLElement,
|
|
||||||
setIsRowVisible,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return <></>;
|
return <></>;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { useMemo } from 'react';
|
|||||||
|
|
||||||
import { FieldEmailsValue } from '@/object-record/record-field/types/FieldMetadata';
|
import { FieldEmailsValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
|
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
|
||||||
import styled from '@emotion/styled';
|
import { styled } from '@linaria/react';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { RoundedLink } from 'twenty-ui/navigation';
|
import { RoundedLink } from 'twenty-ui/navigation';
|
||||||
import { THEME_COMMON } from 'twenty-ui/theme';
|
import { THEME_COMMON } from 'twenty-ui/theme';
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { FieldMultiSelectValue } from '@/object-record/record-field/types/FieldMetadata';
|
import { FieldMultiSelectValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
import styled from '@emotion/styled';
|
import { styled } from '@linaria/react';
|
||||||
import { SelectOption } from 'twenty-ui/input';
|
|
||||||
import { Tag } from 'twenty-ui/components';
|
import { Tag } from 'twenty-ui/components';
|
||||||
|
import { SelectOption } from 'twenty-ui/input';
|
||||||
import { THEME_COMMON } from 'twenty-ui/theme';
|
import { THEME_COMMON } from 'twenty-ui/theme';
|
||||||
|
|
||||||
const spacing1 = THEME_COMMON.spacing(1);
|
const spacing1 = THEME_COMMON.spacing(1);
|
||||||
|
|||||||
@ -1,15 +1,15 @@
|
|||||||
import styled from '@emotion/styled';
|
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
import { FieldPhonesValue } from '@/object-record/record-field/types/FieldMetadata';
|
import { FieldPhonesValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
|
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
|
||||||
|
|
||||||
import { DEFAULT_PHONE_CALLING_CODE } from '@/object-record/record-field/meta-types/input/components/PhonesFieldInput';
|
import { DEFAULT_PHONE_CALLING_CODE } from '@/object-record/record-field/meta-types/input/components/PhonesFieldInput';
|
||||||
|
import { styled } from '@linaria/react';
|
||||||
import { parsePhoneNumber } from 'libphonenumber-js';
|
import { parsePhoneNumber } from 'libphonenumber-js';
|
||||||
import { logError } from '~/utils/logError';
|
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { RoundedLink } from 'twenty-ui/navigation';
|
import { RoundedLink } from 'twenty-ui/navigation';
|
||||||
import { THEME_COMMON } from 'twenty-ui/theme';
|
import { THEME_COMMON } from 'twenty-ui/theme';
|
||||||
|
import { logError } from '~/utils/logError';
|
||||||
|
|
||||||
type PhonesDisplayProps = {
|
type PhonesDisplayProps = {
|
||||||
value?: FieldPhonesValue;
|
value?: FieldPhonesValue;
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell';
|
|
||||||
import { OverflowingTextWithTooltip } from 'twenty-ui/display';
|
import { OverflowingTextWithTooltip } from 'twenty-ui/display';
|
||||||
|
|
||||||
type TextDisplayProps = {
|
type TextDisplayProps = {
|
||||||
@ -7,13 +6,11 @@ type TextDisplayProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const TextDisplay = ({ text, displayedMaxRows }: TextDisplayProps) => {
|
export const TextDisplay = ({ text, displayedMaxRows }: TextDisplayProps) => {
|
||||||
const { isInlineCellInEditMode } = useInlineCell();
|
|
||||||
return (
|
return (
|
||||||
<OverflowingTextWithTooltip
|
<OverflowingTextWithTooltip
|
||||||
text={text}
|
text={text}
|
||||||
displayedMaxRows={displayedMaxRows}
|
displayedMaxRows={displayedMaxRows}
|
||||||
isTooltipMultiline={true}
|
isTooltipMultiline={true}
|
||||||
hideTooltip={isInlineCellInEditMode}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,19 +1,8 @@
|
|||||||
import styled from '@emotion/styled';
|
|
||||||
import { MouseEvent } from 'react';
|
import { MouseEvent } from 'react';
|
||||||
|
|
||||||
|
import { LinkType, RoundedLink, SocialLink } from 'twenty-ui/navigation';
|
||||||
import { checkUrlType } from '~/utils/checkUrlType';
|
import { checkUrlType } from '~/utils/checkUrlType';
|
||||||
import { EllipsisDisplay } from './EllipsisDisplay';
|
import { EllipsisDisplay } from './EllipsisDisplay';
|
||||||
import { LinkType, RoundedLink, SocialLink } from 'twenty-ui/navigation';
|
|
||||||
|
|
||||||
const StyledRawLink = styled(RoundedLink)`
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
a {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
type URLDisplayProps = {
|
type URLDisplayProps = {
|
||||||
value: string | null;
|
value: string | null;
|
||||||
@ -48,7 +37,7 @@ export const URLDisplay = ({ value }: URLDisplayProps) => {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<EllipsisDisplay>
|
<EllipsisDisplay>
|
||||||
<StyledRawLink
|
<RoundedLink
|
||||||
href={absoluteUrl}
|
href={absoluteUrl}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
label={displayedValue}
|
label={displayedValue}
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import { useAreViewFiltersDifferentFromRecordFilters } from '@/views/hooks/useAr
|
|||||||
import { useAreViewSortsDifferentFromRecordSorts } from '@/views/hooks/useAreViewSortsDifferentFromRecordSorts';
|
import { useAreViewSortsDifferentFromRecordSorts } from '@/views/hooks/useAreViewSortsDifferentFromRecordSorts';
|
||||||
|
|
||||||
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
|
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
|
||||||
|
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
||||||
import { useApplyCurrentViewFilterGroupsToCurrentRecordFilterGroups } from '@/views/hooks/useApplyCurrentViewFilterGroupsToCurrentRecordFilterGroups';
|
import { useApplyCurrentViewFilterGroupsToCurrentRecordFilterGroups } from '@/views/hooks/useApplyCurrentViewFilterGroupsToCurrentRecordFilterGroups';
|
||||||
import { useAreViewFilterGroupsDifferentFromRecordFilterGroups } from '@/views/hooks/useAreViewFilterGroupsDifferentFromRecordFilterGroups';
|
import { useAreViewFilterGroupsDifferentFromRecordFilterGroups } from '@/views/hooks/useAreViewFilterGroupsDifferentFromRecordFilterGroups';
|
||||||
import { isViewBarExpandedComponentState } from '@/views/states/isViewBarExpandedComponentState';
|
import { isViewBarExpandedComponentState } from '@/views/states/isViewBarExpandedComponentState';
|
||||||
@ -54,11 +55,10 @@ const StyledBar = styled.div`
|
|||||||
z-index: 4;
|
z-index: 4;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledChipcontainer = styled.div`
|
const StyledChipContainer = styled.div`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
overflow: scroll;
|
|
||||||
gap: ${({ theme }) => theme.spacing(2)};
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
`;
|
`;
|
||||||
@ -195,51 +195,56 @@ export const ViewBarDetails = ({
|
|||||||
return (
|
return (
|
||||||
<StyledBar>
|
<StyledBar>
|
||||||
<StyledFilterContainer>
|
<StyledFilterContainer>
|
||||||
<StyledChipcontainer>
|
<ScrollWrapper
|
||||||
{isDefined(softDeleteFilter) && (
|
componentInstanceId={viewBarId}
|
||||||
<SoftDeleteFilterChip
|
defaultEnableYScroll={false}
|
||||||
key={softDeleteFilter.fieldMetadataId}
|
>
|
||||||
recordFilter={softDeleteFilter}
|
<StyledChipContainer>
|
||||||
viewBarId={viewBarId}
|
{isDefined(softDeleteFilter) && (
|
||||||
/>
|
<SoftDeleteFilterChip
|
||||||
)}
|
key={softDeleteFilter.fieldMetadataId}
|
||||||
{isDefined(softDeleteFilter) && (
|
recordFilter={softDeleteFilter}
|
||||||
<StyledSeperatorContainer>
|
viewBarId={viewBarId}
|
||||||
<StyledSeperator />
|
/>
|
||||||
</StyledSeperatorContainer>
|
)}
|
||||||
)}
|
{isDefined(softDeleteFilter) && (
|
||||||
{currentRecordSorts.map((recordSort) => (
|
|
||||||
<EditableSortChip
|
|
||||||
key={recordSort.fieldMetadataId}
|
|
||||||
recordSort={recordSort}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{isNonEmptyArray(recordFilters) &&
|
|
||||||
isNonEmptyArray(currentRecordSorts) && (
|
|
||||||
<StyledSeperatorContainer>
|
<StyledSeperatorContainer>
|
||||||
<StyledSeperator />
|
<StyledSeperator />
|
||||||
</StyledSeperatorContainer>
|
</StyledSeperatorContainer>
|
||||||
)}
|
)}
|
||||||
{shouldShowAdvancedFilterDropdownButton && (
|
{currentRecordSorts.map((recordSort) => (
|
||||||
<AdvancedFilterDropdownButton />
|
<EditableSortChip
|
||||||
)}
|
key={recordSort.fieldMetadataId}
|
||||||
{recordFilters.map((recordFilter) => (
|
recordSort={recordSort}
|
||||||
<ObjectFilterDropdownComponentInstanceContext.Provider
|
/>
|
||||||
key={recordFilter.id}
|
))}
|
||||||
value={{ instanceId: recordFilter.id }}
|
{isNonEmptyArray(recordFilters) &&
|
||||||
>
|
isNonEmptyArray(currentRecordSorts) && (
|
||||||
<DropdownScope dropdownScopeId={recordFilter.id}>
|
<StyledSeperatorContainer>
|
||||||
<ViewBarFilterEffect filterDropdownId={recordFilter.id} />
|
<StyledSeperator />
|
||||||
<EditableFilterDropdownButton
|
</StyledSeperatorContainer>
|
||||||
recordFilter={recordFilter}
|
)}
|
||||||
hotkeyScope={{
|
{shouldShowAdvancedFilterDropdownButton && (
|
||||||
scope: recordFilter.id,
|
<AdvancedFilterDropdownButton />
|
||||||
}}
|
)}
|
||||||
/>
|
{recordFilters.map((recordFilter) => (
|
||||||
</DropdownScope>
|
<ObjectFilterDropdownComponentInstanceContext.Provider
|
||||||
</ObjectFilterDropdownComponentInstanceContext.Provider>
|
key={recordFilter.id}
|
||||||
))}
|
value={{ instanceId: recordFilter.id }}
|
||||||
</StyledChipcontainer>
|
>
|
||||||
|
<DropdownScope dropdownScopeId={recordFilter.id}>
|
||||||
|
<ViewBarFilterEffect filterDropdownId={recordFilter.id} />
|
||||||
|
<EditableFilterDropdownButton
|
||||||
|
recordFilter={recordFilter}
|
||||||
|
hotkeyScope={{
|
||||||
|
scope: recordFilter.id,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</DropdownScope>
|
||||||
|
</ObjectFilterDropdownComponentInstanceContext.Provider>
|
||||||
|
))}
|
||||||
|
</StyledChipContainer>
|
||||||
|
</ScrollWrapper>
|
||||||
{hasFilterButton && (
|
{hasFilterButton && (
|
||||||
<StyledAddFilterContainer>
|
<StyledAddFilterContainer>
|
||||||
<AddObjectFilterFromDetailsButton
|
<AddObjectFilterFromDetailsButton
|
||||||
|
|||||||
@ -132,6 +132,11 @@ export default defineConfig(({ command, mode }) => {
|
|||||||
'**/RecordTableHeaderDragDropColumn.tsx',
|
'**/RecordTableHeaderDragDropColumn.tsx',
|
||||||
'**/ActorDisplay.tsx',
|
'**/ActorDisplay.tsx',
|
||||||
'**/AvatarChip.tsx',
|
'**/AvatarChip.tsx',
|
||||||
|
'**/URLDisplay.tsx',
|
||||||
|
'**/EmailsDisplay.tsx',
|
||||||
|
'**/PhonesDisplay.tsx',
|
||||||
|
'**/MultiSelectDisplay.tsx',
|
||||||
|
|
||||||
],
|
],
|
||||||
babelOptions: {
|
babelOptions: {
|
||||||
presets: ['@babel/preset-typescript', '@babel/preset-react'],
|
presets: ['@babel/preset-typescript', '@babel/preset-react'],
|
||||||
|
|||||||
@ -19,7 +19,7 @@ export const AvatarChip = ({
|
|||||||
label={name}
|
label={name}
|
||||||
variant={ChipVariant.Transparent}
|
variant={ChipVariant.Transparent}
|
||||||
size={size}
|
size={size}
|
||||||
leftComponent={() => (
|
leftComponent={
|
||||||
<AvatarChipsLeftComponent
|
<AvatarChipsLeftComponent
|
||||||
name={name}
|
name={name}
|
||||||
LeftIcon={LeftIcon}
|
LeftIcon={LeftIcon}
|
||||||
@ -29,7 +29,7 @@ export const AvatarChip = ({
|
|||||||
isIconInverted={isIconInverted}
|
isIconInverted={isIconInverted}
|
||||||
placeholderColorSeed={placeholderColorSeed}
|
placeholderColorSeed={placeholderColorSeed}
|
||||||
/>
|
/>
|
||||||
)}
|
}
|
||||||
clickable={false}
|
clickable={false}
|
||||||
className={className}
|
className={className}
|
||||||
maxWidth={maxWidth}
|
maxWidth={maxWidth}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import { styled } from '@linaria/react';
|
||||||
import { Avatar } from '@ui/display/avatar/components/Avatar';
|
import { Avatar } from '@ui/display/avatar/components/Avatar';
|
||||||
import { AvatarType } from '@ui/display/avatar/types/AvatarType';
|
import { AvatarType } from '@ui/display/avatar/types/AvatarType';
|
||||||
import { IconComponent } from '@ui/display/icon/types/IconComponent';
|
import { IconComponent } from '@ui/display/icon/types/IconComponent';
|
||||||
|
|||||||
@ -39,7 +39,7 @@ export const LinkAvatarChip = ({
|
|||||||
: ChipVariant.Regular
|
: ChipVariant.Regular
|
||||||
}
|
}
|
||||||
size={size}
|
size={size}
|
||||||
leftComponent={() => (
|
leftComponent={
|
||||||
<AvatarChipsLeftComponent
|
<AvatarChipsLeftComponent
|
||||||
name={name}
|
name={name}
|
||||||
LeftIcon={LeftIcon}
|
LeftIcon={LeftIcon}
|
||||||
@ -49,7 +49,7 @@ export const LinkAvatarChip = ({
|
|||||||
isIconInverted={isIconInverted}
|
isIconInverted={isIconInverted}
|
||||||
placeholderColorSeed={placeholderColorSeed}
|
placeholderColorSeed={placeholderColorSeed}
|
||||||
/>
|
/>
|
||||||
)}
|
}
|
||||||
className={className}
|
className={className}
|
||||||
maxWidth={maxWidth}
|
maxWidth={maxWidth}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -30,7 +30,7 @@ export type ChipProps = {
|
|||||||
maxWidth?: number;
|
maxWidth?: number;
|
||||||
variant?: ChipVariant;
|
variant?: ChipVariant;
|
||||||
accent?: ChipAccent;
|
accent?: ChipAccent;
|
||||||
leftComponent?: (() => ReactNode) | null;
|
leftComponent?: ReactNode | null;
|
||||||
rightComponent?: (() => ReactNode) | null;
|
rightComponent?: (() => ReactNode) | null;
|
||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
@ -146,7 +146,7 @@ export const Chip = ({
|
|||||||
className={className}
|
className={className}
|
||||||
maxWidth={maxWidth}
|
maxWidth={maxWidth}
|
||||||
>
|
>
|
||||||
{leftComponent?.()}
|
{leftComponent}
|
||||||
{!isLabelHidden && (
|
{!isLabelHidden && (
|
||||||
<OverflowingTextWithTooltip size={size} text={label} />
|
<OverflowingTextWithTooltip size={size} text={label} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import styled from '@emotion/styled';
|
import { styled } from '@linaria/react';
|
||||||
import {
|
import {
|
||||||
Chip,
|
Chip,
|
||||||
ChipAccent,
|
ChipAccent,
|
||||||
@ -17,8 +17,6 @@ export type LinkChipProps = Omit<
|
|||||||
onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;
|
onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Ideally we would use the UndecoratedLink component from @ui/navigation
|
|
||||||
// but it led to a bug probably linked to circular dependencies, which was hard to solve
|
|
||||||
const StyledLink = styled(Link)`
|
const StyledLink = styled(Link)`
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { styled } from '@linaria/react';
|
import { styled } from '@linaria/react';
|
||||||
import { isNonEmptyString, isNull, isUndefined } from '@sniptt/guards';
|
import { isNonEmptyString, isNull, isUndefined } from '@sniptt/guards';
|
||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import { useRecoilState } from 'recoil';
|
|
||||||
|
|
||||||
import { invalidAvatarUrlsState } from '@ui/display/avatar/components/states/isInvalidAvatarUrlState';
|
import { invalidAvatarUrlsState } from '@ui/display/avatar/components/states/isInvalidAvatarUrlState';
|
||||||
import { AVATAR_PROPERTIES_BY_SIZE } from '@ui/display/avatar/constants/AvatarPropertiesBySize';
|
import { AVATAR_PROPERTIES_BY_SIZE } from '@ui/display/avatar/constants/AvatarPropertiesBySize';
|
||||||
@ -11,6 +10,7 @@ import { IconComponent } from '@ui/display/icon/types/IconComponent';
|
|||||||
import { ThemeContext } from '@ui/theme';
|
import { ThemeContext } from '@ui/theme';
|
||||||
import { Nullable, stringToHslColor } from '@ui/utilities';
|
import { Nullable, stringToHslColor } from '@ui/utilities';
|
||||||
import { REACT_APP_SERVER_BASE_URL } from '@ui/utilities/config';
|
import { REACT_APP_SERVER_BASE_URL } from '@ui/utilities/config';
|
||||||
|
import { useRecoilState } from 'recoil';
|
||||||
import { getImageAbsoluteURI } from 'twenty-shared/utils';
|
import { getImageAbsoluteURI } from 'twenty-shared/utils';
|
||||||
|
|
||||||
const StyledAvatar = styled.div<{
|
const StyledAvatar = styled.div<{
|
||||||
|
|||||||
@ -7,6 +7,7 @@ type RoundedLinkProps = {
|
|||||||
href: string;
|
href: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
|
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
|
||||||
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const fontSizeMd = FONT_COMMON.size.md;
|
const fontSizeMd = FONT_COMMON.size.md;
|
||||||
@ -59,7 +60,12 @@ const StyledLink = styled.a<{
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const RoundedLink = ({ label, href, onClick }: RoundedLinkProps) => {
|
export const RoundedLink = ({
|
||||||
|
label,
|
||||||
|
href,
|
||||||
|
onClick,
|
||||||
|
className,
|
||||||
|
}: RoundedLinkProps) => {
|
||||||
const { theme } = useContext(ThemeContext);
|
const { theme } = useContext(ThemeContext);
|
||||||
|
|
||||||
const background = theme.background.transparent.lighter;
|
const background = theme.background.transparent.lighter;
|
||||||
@ -89,6 +95,7 @@ export const RoundedLink = ({ label, href, onClick }: RoundedLinkProps) => {
|
|||||||
backgroundHover={backgroundHover}
|
backgroundHover={backgroundHover}
|
||||||
backgroundActive={backgroundActive}
|
backgroundActive={backgroundActive}
|
||||||
border={border}
|
border={border}
|
||||||
|
className={className}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</StyledLink>
|
</StyledLink>
|
||||||
|
|||||||
@ -84,6 +84,9 @@ export default defineConfig(({ command }) => {
|
|||||||
'**/Tag.tsx',
|
'**/Tag.tsx',
|
||||||
'**/Avatar.tsx',
|
'**/Avatar.tsx',
|
||||||
'**/Chip.tsx',
|
'**/Chip.tsx',
|
||||||
|
'**/LinkChip.tsx',
|
||||||
|
'**/Avatar.tsx',
|
||||||
|
'**/AvatarChipLeftComponent.tsx',
|
||||||
'**/ContactLink.tsx',
|
'**/ContactLink.tsx',
|
||||||
'**/RoundedLink.tsx',
|
'**/RoundedLink.tsx',
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user