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:
Charles Bochet
2025-04-10 01:39:25 +02:00
committed by GitHub
parent 7bd68ad176
commit a7e6564017
28 changed files with 160 additions and 159 deletions

View File

@ -127,7 +127,7 @@ export const CalendarEventDetails = ({
size={ChipSize.Large}
variant={ChipVariant.Highlighted}
clickable={false}
leftComponent={() => <IconCalendarEvent size={theme.icon.size.md} />}
leftComponent={<IconCalendarEvent size={theme.icon.size.md} />}
label="Event"
/>
<StyledHeader>

View File

@ -1,8 +1,8 @@
import { MessageThreadSubscriber } from '@/activities/emails/types/MessageThreadSubscriber';
import { isNonEmptyString } from '@sniptt/guards';
import { useContext } from 'react';
import { Avatar, AvatarGroup, IconChevronDown } from 'twenty-ui/display';
import { Chip, ChipVariant } from 'twenty-ui/components';
import { Avatar, AvatarGroup, IconChevronDown } from 'twenty-ui/display';
import { ThemeContext } from 'twenty-ui/theme';
const MAX_NUMBER_OF_AVATARS = 3;
@ -46,20 +46,16 @@ export const MessageThreadSubscribersChip = ({
<Chip
label={label}
variant={ChipVariant.Highlighted}
leftComponent={() => {
if (isOnlyOneSubscriber) {
return (
<Avatar
avatarUrl={firstAvatarUrl}
placeholderColorSeed={firstAvatarColorSeed}
placeholder={firstAvatarPlaceholder}
size="md"
type={'rounded'}
/>
);
}
return (
leftComponent={
isOnlyOneSubscriber ? (
<Avatar
avatarUrl={firstAvatarUrl}
placeholderColorSeed={firstAvatarColorSeed}
placeholder={firstAvatarPlaceholder}
size="md"
type={'rounded'}
/>
) : (
<AvatarGroup
avatars={subscriberNames.map((name, index) => (
<Avatar
@ -70,8 +66,8 @@ export const MessageThreadSubscribersChip = ({
/>
))}
/>
);
}}
)
}
rightComponent={() => <IconChevronDown size={theme.icon.size.sm} />}
clickable
/>

View File

@ -16,8 +16,8 @@ import { useUpdateEffect } from '~/hooks/useUpdateEffect';
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
import { AppPath } from '@/types/AppPath';
import { ApolloFactory, Options } from '../services/apollo.factory';
import { isDefined } from 'twenty-shared/utils';
import { ApolloFactory, Options } from '../services/apollo.factory';
export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
// eslint-disable-next-line @nx/workspace-no-state-useref
@ -26,7 +26,7 @@ export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
const navigate = useNavigate();
const { isMatchingLocation } = useIsMatchingLocation();
const [tokenPair, setTokenPair] = useRecoilState(tokenPairState);
const setTokenPair = useSetRecoilState(tokenPairState);
const [currentWorkspace, setCurrentWorkspace] = useRecoilState(
currentWorkspaceState,
);
@ -62,8 +62,6 @@ export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
},
},
connectToDevTools: isDebugMode,
// We don't want to re-create the client on token change or it will cause infinite loop
initialTokenPair: tokenPair,
currentWorkspaceMember: currentWorkspaceMember,
onTokenPairChange: (tokenPair) => {
setTokenPair(tokenPair);
@ -104,12 +102,6 @@ export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
setPreviousUrl,
]);
useUpdateEffect(() => {
if (isDefined(apolloRef.current)) {
apolloRef.current.updateTokenPair(tokenPair);
}
}, [tokenPair]);
useUpdateEffect(() => {
if (isDefined(apolloRef.current)) {
apolloRef.current.updateWorkspaceMember(currentWorkspaceMember);

View File

@ -33,10 +33,6 @@ const mockWorkspaceMember = {
const createMockOptions = (): Options<any> => ({
uri: 'http://localhost:3000',
initialTokenPair: {
accessToken: { token: 'mockAccessToken', expiresAt: '' },
refreshToken: { token: 'mockRefreshToken', expiresAt: '' },
},
currentWorkspaceMember: mockWorkspaceMember,
cache: new InMemoryCache(),
isDebugMode: true,

View File

@ -18,9 +18,11 @@ import { logDebug } from '~/utils/logDebug';
import { i18n } from '@lingui/core';
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 { loggerLink } from '../utils/loggerLink';
import { isDefined } from 'twenty-shared/utils';
const logger = loggerLink(() => 'Twenty');
@ -29,7 +31,6 @@ export interface Options<TCacheShape> extends ApolloClientOptions<TCacheShape> {
onNetworkError?: (err: Error | ServerParseError | ServerError) => void;
onTokenPairChange?: (tokenPair: AuthTokenPair) => void;
onUnauthenticatedError?: () => void;
initialTokenPair: AuthTokenPair | null;
currentWorkspaceMember: CurrentWorkspaceMember | null;
extraLinks?: ApolloLink[];
isDebugMode?: boolean;
@ -37,7 +38,6 @@ export interface Options<TCacheShape> extends ApolloClientOptions<TCacheShape> {
export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
private client: ApolloClient<TCacheShape>;
private tokenPair: AuthTokenPair | null = null;
private currentWorkspaceMember: CurrentWorkspaceMember | null = null;
constructor(opts: Options<TCacheShape>) {
@ -47,28 +47,45 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
onNetworkError,
onTokenPairChange,
onUnauthenticatedError,
initialTokenPair,
currentWorkspaceMember,
extraLinks,
isDebugMode,
...options
} = opts;
this.tokenPair = initialTokenPair;
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 httpLink = createUploadLink({
uri,
});
const authLink = setContext(async (_, { headers }) => {
const tokenPair = getTokenPair();
if (isUndefinedOrNull(tokenPair)) {
return {
headers: {
...headers,
...options.headers,
},
};
}
return {
headers: {
...headers,
...options.headers,
authorization: this.tokenPair?.accessToken.token
? `Bearer ${this.tokenPair?.accessToken.token}`
authorization: tokenPair.accessToken.token
? `Bearer ${tokenPair.accessToken.token}`
: '',
...(this.currentWorkspaceMember?.locale
? { 'x-locale': this.currentWorkspaceMember.locale }
@ -93,7 +110,7 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
for (const graphQLError of graphQLErrors) {
if (graphQLError.message === 'Unauthorized') {
return fromPromise(
renewToken(uri, this.tokenPair)
renewToken(uri, getTokenPair())
.then((tokens) => {
if (isDefined(tokens)) {
onTokenPairChange?.(tokens);
@ -108,10 +125,14 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
switch (graphQLError?.extensions?.code) {
case 'UNAUTHENTICATED': {
return fromPromise(
renewToken(uri, this.tokenPair)
renewToken(uri, getTokenPair())
.then((tokens) => {
if (isDefined(tokens)) {
onTokenPairChange?.(tokens);
cookieStorage.setItem(
'tokenPair',
JSON.stringify(tokens),
);
}
})
.catch(() => {
@ -162,10 +183,6 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
});
}
updateTokenPair(tokenPair: AuthTokenPair | null) {
this.tokenPair = tokenPair;
}
updateWorkspaceMember(workspaceMember: CurrentWorkspaceMember | null) {
this.currentWorkspaceMember = workspaceMember;
}

View File

@ -1,10 +1,8 @@
import { ApolloClient } from '@apollo/client';
import { CurrentWorkspaceMember } from '@/auth/states/currentWorkspaceMemberState';
import { AuthTokenPair } from '~/generated/graphql';
export interface ApolloManager<TCacheShape> {
getClient(): ApolloClient<TCacheShape>;
updateTokenPair(tokenPair: AuthTokenPair | null): void;
updateWorkspaceMember(workspaceMember: CurrentWorkspaceMember | null): void;
}

View File

@ -63,6 +63,7 @@ import { useSearchParams } from 'react-router-dom';
import { APP_LOCALES } from 'twenty-shared/translations';
import { isDefined } from 'twenty-shared/utils';
import { iconsState } from 'twenty-ui/display';
import { cookieStorage } from '~/utils/cookie-storage';
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
import { dynamicActivate } from '~/utils/i18n/dynamicActivate';
@ -348,6 +349,12 @@ export const useAuth = () => {
setTokenPair(
getAuthTokensResult.data?.getAuthTokensFromLoginToken.tokens,
);
cookieStorage.setItem(
'tokenPair',
JSON.stringify(
getAuthTokensResult.data?.getAuthTokensFromLoginToken.tokens,
),
);
await refreshObjectMetadataItems();
await loadCurrentUser();

View File

@ -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';
export const EmailsFieldDisplay = () => {
const { fieldValue } = useEmailsField();
const { fieldValue } = useEmailsFieldDisplay();
return <EmailsDisplay value={fieldValue} />;
};

View File

@ -2,9 +2,7 @@ import { Theme, withTheme } from '@emotion/react';
import { styled } from '@linaria/react';
import { Ref } from 'react';
const StyledOuterContainer = styled.div<{
hasSoftFocus?: boolean;
}>`
const StyledOuterContainer = styled.div`
align-items: center;
display: flex;
height: 100%;
@ -50,7 +48,6 @@ export const RecordTableCellDisplayContainer = ({
}
onClick={onClick}
ref={scrollRef}
hasSoftFocus={softFocus}
onContextMenu={onContextMenu}
>
{placeholderForEmptyCell ? (

View File

@ -1,5 +1,3 @@
import { useMemo } from 'react';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
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 { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
import { useMemo } from 'react';
export const RecordTableCellWrapper = ({
children,

View File

@ -18,25 +18,19 @@ const StyledTd = styled.td<{
width?: number;
}>`
border-bottom: 1px solid
${({ borderColor, hasBottomBorder }) =>
hasBottomBorder ? borderColor : 'transparent'};
${({ borderColor, hasBottomBorder, isDragging }) =>
hasBottomBorder && !isDragging ? borderColor : 'transparent'};
color: ${({ fontColor }) => fontColor};
border-right: ${({ borderColor, hasRightBorder }) =>
hasRightBorder ? `1px solid ${borderColor}` : 'none'};
border-right: ${({ borderColor, hasRightBorder, isDragging }) =>
hasRightBorder && !isDragging ? `1px solid ${borderColor}` : 'none'};
padding: 0;
transition: 0.3s ease;
text-align: left;
background: ${({ backgroundColor }) => backgroundColor};
${({ isDragging }) =>
isDragging
? `
background-color: transparent;
border-color: transparent;
`
: ''}
background: ${({ backgroundColor, isDragging }) =>
isDragging ? 'transparent' : backgroundColor};
${({ freezeFirstColumns }) =>
freezeFirstColumns

View File

@ -6,10 +6,9 @@ import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { onToggleColumnFilterComponentState } from '@/object-record/record-table/states/onToggleColumnFilterComponentState';
import { onToggleColumnSortComponentState } from '@/object-record/record-table/states/onToggleColumnSortComponentState';
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 { useLingui } from '@lingui/react/macro';
import { useTableColumns } from '../../hooks/useTableColumns';
import { ColumnDefinition } from '../../types/ColumnDefinition';
import {
IconArrowLeft,
IconArrowRight,
@ -18,6 +17,8 @@ import {
IconSortDescending,
} from 'twenty-ui/display';
import { MenuItem } from 'twenty-ui/navigation';
import { useTableColumns } from '../../hooks/useTableColumns';
import { ColumnDefinition } from '../../types/ColumnDefinition';
export type RecordTableColumnHeadDropdownMenuProps = {
column: ColumnDefinition<FieldMetadata>;
@ -28,6 +29,9 @@ export const RecordTableColumnHeadDropdownMenu = ({
}: RecordTableColumnHeadDropdownMenuProps) => {
const { t } = useLingui();
const { toggleScrollXWrapper, toggleScrollYWrapper } =
useToggleScrollWrapper();
const visibleTableColumns = useRecoilComponentValueV2(
visibleTableColumnsComponentSelector,
);
@ -46,16 +50,21 @@ export const RecordTableColumnHeadDropdownMenu = ({
const { closeDropdown } = useDropdown(column.fieldMetadataId + '-header');
const handleColumnMoveLeft = () => {
const closeDropdownAndToggleScroll = () => {
closeDropdown();
toggleScrollXWrapper(true);
toggleScrollYWrapper(false);
};
const handleColumnMoveLeft = () => {
closeDropdownAndToggleScroll();
if (!canMoveLeft) return;
handleMoveTableColumn('left', column);
};
const handleColumnMoveRight = () => {
closeDropdown();
closeDropdownAndToggleScroll();
if (!canMoveRight) return;
@ -63,7 +72,7 @@ export const RecordTableColumnHeadDropdownMenu = ({
};
const handleColumnVisibility = () => {
closeDropdown();
closeDropdownAndToggleScroll();
handleColumnVisibilityChange(column);
};
@ -75,13 +84,13 @@ export const RecordTableColumnHeadDropdownMenu = ({
);
const handleSortClick = () => {
closeDropdown();
closeDropdownAndToggleScroll();
onToggleColumnSort?.(column.fieldMetadataId);
};
const handleFilterClick = () => {
closeDropdown();
closeDropdownAndToggleScroll();
onToggleColumnFilter?.(column.fieldMetadataId);
};

View File

@ -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 { useScrollWrapperElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperElement';
import { useSetRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentFamilyStateV2';
@ -9,7 +8,6 @@ type RecordTableTrEffectProps = {
};
export const RecordTableTrEffect = ({ recordId }: RecordTableTrEffectProps) => {
const { onIndexRecordsLoaded } = useRecordIndexContextOrThrow();
const { scrollWrapperHTMLElement } = useScrollWrapperElement();
const setIsRowVisible = useSetRecoilComponentFamilyStateV2(
@ -29,7 +27,6 @@ export const RecordTableTrEffect = ({ recordId }: RecordTableTrEffectProps) => {
const isIntersecting = entry.isIntersecting;
if (isIntersecting) {
onIndexRecordsLoaded?.();
setIsRowVisible(true);
}
@ -50,12 +47,7 @@ export const RecordTableTrEffect = ({ recordId }: RecordTableTrEffectProps) => {
return () => {
observer.disconnect();
};
}, [
onIndexRecordsLoaded,
recordId,
scrollWrapperHTMLElement,
setIsRowVisible,
]);
}, [recordId, scrollWrapperHTMLElement, setIsRowVisible]);
return <></>;
};

View File

@ -2,7 +2,7 @@ import { useMemo } from 'react';
import { FieldEmailsValue } from '@/object-record/record-field/types/FieldMetadata';
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 { RoundedLink } from 'twenty-ui/navigation';
import { THEME_COMMON } from 'twenty-ui/theme';

View File

@ -1,7 +1,7 @@
import { FieldMultiSelectValue } from '@/object-record/record-field/types/FieldMetadata';
import styled from '@emotion/styled';
import { SelectOption } from 'twenty-ui/input';
import { styled } from '@linaria/react';
import { Tag } from 'twenty-ui/components';
import { SelectOption } from 'twenty-ui/input';
import { THEME_COMMON } from 'twenty-ui/theme';
const spacing1 = THEME_COMMON.spacing(1);

View File

@ -1,15 +1,15 @@
import styled from '@emotion/styled';
import React, { useMemo } from 'react';
import { FieldPhonesValue } from '@/object-record/record-field/types/FieldMetadata';
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 { styled } from '@linaria/react';
import { parsePhoneNumber } from 'libphonenumber-js';
import { logError } from '~/utils/logError';
import { isDefined } from 'twenty-shared/utils';
import { RoundedLink } from 'twenty-ui/navigation';
import { THEME_COMMON } from 'twenty-ui/theme';
import { logError } from '~/utils/logError';
type PhonesDisplayProps = {
value?: FieldPhonesValue;

View File

@ -1,4 +1,3 @@
import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell';
import { OverflowingTextWithTooltip } from 'twenty-ui/display';
type TextDisplayProps = {
@ -7,13 +6,11 @@ type TextDisplayProps = {
};
export const TextDisplay = ({ text, displayedMaxRows }: TextDisplayProps) => {
const { isInlineCellInEditMode } = useInlineCell();
return (
<OverflowingTextWithTooltip
text={text}
displayedMaxRows={displayedMaxRows}
isTooltipMultiline={true}
hideTooltip={isInlineCellInEditMode}
/>
);
};

View File

@ -1,19 +1,8 @@
import styled from '@emotion/styled';
import { MouseEvent } from 'react';
import { LinkType, RoundedLink, SocialLink } from 'twenty-ui/navigation';
import { checkUrlType } from '~/utils/checkUrlType';
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 = {
value: string | null;
@ -48,7 +37,7 @@ export const URLDisplay = ({ value }: URLDisplayProps) => {
}
return (
<EllipsisDisplay>
<StyledRawLink
<RoundedLink
href={absoluteUrl}
onClick={handleClick}
label={displayedValue}

View File

@ -23,6 +23,7 @@ import { useAreViewFiltersDifferentFromRecordFilters } from '@/views/hooks/useAr
import { useAreViewSortsDifferentFromRecordSorts } from '@/views/hooks/useAreViewSortsDifferentFromRecordSorts';
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 { useAreViewFilterGroupsDifferentFromRecordFilterGroups } from '@/views/hooks/useAreViewFilterGroupsDifferentFromRecordFilterGroups';
import { isViewBarExpandedComponentState } from '@/views/states/isViewBarExpandedComponentState';
@ -54,11 +55,10 @@ const StyledBar = styled.div`
z-index: 4;
`;
const StyledChipcontainer = styled.div`
const StyledChipContainer = styled.div`
align-items: center;
display: flex;
flex-direction: row;
overflow: scroll;
gap: ${({ theme }) => theme.spacing(2)};
z-index: 1;
`;
@ -195,51 +195,56 @@ export const ViewBarDetails = ({
return (
<StyledBar>
<StyledFilterContainer>
<StyledChipcontainer>
{isDefined(softDeleteFilter) && (
<SoftDeleteFilterChip
key={softDeleteFilter.fieldMetadataId}
recordFilter={softDeleteFilter}
viewBarId={viewBarId}
/>
)}
{isDefined(softDeleteFilter) && (
<StyledSeperatorContainer>
<StyledSeperator />
</StyledSeperatorContainer>
)}
{currentRecordSorts.map((recordSort) => (
<EditableSortChip
key={recordSort.fieldMetadataId}
recordSort={recordSort}
/>
))}
{isNonEmptyArray(recordFilters) &&
isNonEmptyArray(currentRecordSorts) && (
<ScrollWrapper
componentInstanceId={viewBarId}
defaultEnableYScroll={false}
>
<StyledChipContainer>
{isDefined(softDeleteFilter) && (
<SoftDeleteFilterChip
key={softDeleteFilter.fieldMetadataId}
recordFilter={softDeleteFilter}
viewBarId={viewBarId}
/>
)}
{isDefined(softDeleteFilter) && (
<StyledSeperatorContainer>
<StyledSeperator />
</StyledSeperatorContainer>
)}
{shouldShowAdvancedFilterDropdownButton && (
<AdvancedFilterDropdownButton />
)}
{recordFilters.map((recordFilter) => (
<ObjectFilterDropdownComponentInstanceContext.Provider
key={recordFilter.id}
value={{ instanceId: recordFilter.id }}
>
<DropdownScope dropdownScopeId={recordFilter.id}>
<ViewBarFilterEffect filterDropdownId={recordFilter.id} />
<EditableFilterDropdownButton
recordFilter={recordFilter}
hotkeyScope={{
scope: recordFilter.id,
}}
/>
</DropdownScope>
</ObjectFilterDropdownComponentInstanceContext.Provider>
))}
</StyledChipcontainer>
{currentRecordSorts.map((recordSort) => (
<EditableSortChip
key={recordSort.fieldMetadataId}
recordSort={recordSort}
/>
))}
{isNonEmptyArray(recordFilters) &&
isNonEmptyArray(currentRecordSorts) && (
<StyledSeperatorContainer>
<StyledSeperator />
</StyledSeperatorContainer>
)}
{shouldShowAdvancedFilterDropdownButton && (
<AdvancedFilterDropdownButton />
)}
{recordFilters.map((recordFilter) => (
<ObjectFilterDropdownComponentInstanceContext.Provider
key={recordFilter.id}
value={{ instanceId: recordFilter.id }}
>
<DropdownScope dropdownScopeId={recordFilter.id}>
<ViewBarFilterEffect filterDropdownId={recordFilter.id} />
<EditableFilterDropdownButton
recordFilter={recordFilter}
hotkeyScope={{
scope: recordFilter.id,
}}
/>
</DropdownScope>
</ObjectFilterDropdownComponentInstanceContext.Provider>
))}
</StyledChipContainer>
</ScrollWrapper>
{hasFilterButton && (
<StyledAddFilterContainer>
<AddObjectFilterFromDetailsButton

View File

@ -132,6 +132,11 @@ export default defineConfig(({ command, mode }) => {
'**/RecordTableHeaderDragDropColumn.tsx',
'**/ActorDisplay.tsx',
'**/AvatarChip.tsx',
'**/URLDisplay.tsx',
'**/EmailsDisplay.tsx',
'**/PhonesDisplay.tsx',
'**/MultiSelectDisplay.tsx',
],
babelOptions: {
presets: ['@babel/preset-typescript', '@babel/preset-react'],

View File

@ -19,7 +19,7 @@ export const AvatarChip = ({
label={name}
variant={ChipVariant.Transparent}
size={size}
leftComponent={() => (
leftComponent={
<AvatarChipsLeftComponent
name={name}
LeftIcon={LeftIcon}
@ -29,7 +29,7 @@ export const AvatarChip = ({
isIconInverted={isIconInverted}
placeholderColorSeed={placeholderColorSeed}
/>
)}
}
clickable={false}
className={className}
maxWidth={maxWidth}

View File

@ -1,5 +1,5 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { styled } from '@linaria/react';
import { Avatar } from '@ui/display/avatar/components/Avatar';
import { AvatarType } from '@ui/display/avatar/types/AvatarType';
import { IconComponent } from '@ui/display/icon/types/IconComponent';

View File

@ -39,7 +39,7 @@ export const LinkAvatarChip = ({
: ChipVariant.Regular
}
size={size}
leftComponent={() => (
leftComponent={
<AvatarChipsLeftComponent
name={name}
LeftIcon={LeftIcon}
@ -49,7 +49,7 @@ export const LinkAvatarChip = ({
isIconInverted={isIconInverted}
placeholderColorSeed={placeholderColorSeed}
/>
)}
}
className={className}
maxWidth={maxWidth}
/>

View File

@ -30,7 +30,7 @@ export type ChipProps = {
maxWidth?: number;
variant?: ChipVariant;
accent?: ChipAccent;
leftComponent?: (() => ReactNode) | null;
leftComponent?: ReactNode | null;
rightComponent?: (() => ReactNode) | null;
className?: string;
};
@ -146,7 +146,7 @@ export const Chip = ({
className={className}
maxWidth={maxWidth}
>
{leftComponent?.()}
{leftComponent}
{!isLabelHidden && (
<OverflowingTextWithTooltip size={size} text={label} />
)}

View File

@ -1,4 +1,4 @@
import styled from '@emotion/styled';
import { styled } from '@linaria/react';
import {
Chip,
ChipAccent,
@ -17,8 +17,6 @@ export type LinkChipProps = Omit<
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)`
text-decoration: none;
`;

View File

@ -1,7 +1,6 @@
import { styled } from '@linaria/react';
import { isNonEmptyString, isNull, isUndefined } from '@sniptt/guards';
import { useContext } from 'react';
import { useRecoilState } from 'recoil';
import { invalidAvatarUrlsState } from '@ui/display/avatar/components/states/isInvalidAvatarUrlState';
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 { Nullable, stringToHslColor } from '@ui/utilities';
import { REACT_APP_SERVER_BASE_URL } from '@ui/utilities/config';
import { useRecoilState } from 'recoil';
import { getImageAbsoluteURI } from 'twenty-shared/utils';
const StyledAvatar = styled.div<{

View File

@ -7,6 +7,7 @@ type RoundedLinkProps = {
href: string;
label?: string;
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
className?: string;
};
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 background = theme.background.transparent.lighter;
@ -89,6 +95,7 @@ export const RoundedLink = ({ label, href, onClick }: RoundedLinkProps) => {
backgroundHover={backgroundHover}
backgroundActive={backgroundActive}
border={border}
className={className}
>
{label}
</StyledLink>

View File

@ -84,6 +84,9 @@ export default defineConfig(({ command }) => {
'**/Tag.tsx',
'**/Avatar.tsx',
'**/Chip.tsx',
'**/LinkChip.tsx',
'**/Avatar.tsx',
'**/AvatarChipLeftComponent.tsx',
'**/ContactLink.tsx',
'**/RoundedLink.tsx',
],