[BUGFIX] Account owner should not be clickable & [Refactor] Chip.tsx links (#10359)
# Introduction closes #10196 Initially fixing the `Account Owner` record field value should not be clickable and redirects on current page bug. This has been fixed computing whereas the current filed is a workspace member dynamically rendering a stale Chip components instead of an interactive one ## Refactor Refactored the `AvatarChip` `to` props logic to be scoped to lower level scope `Chip`. Now we have `LinkChip` `Chip`, `LinkAvatarChip` and `AvatarChip` all exported from twenty-ui. The caller has to determine which one to call from the design system ## New rule regarding chip links As discussed with @charlesBochet and @FelixMalfait A chip link will now ***always*** have `to` defined. ( and optionally an `onClick` ). `ChipLinks` cannot be used as buttons anymore ## Factorization Deleted the `RecordIndexRecordChip.tsx` file ( aka `RecordIdentifierChip` component ) that was duplicating some logic, refactored the `RecordChip` in order to handle what was covered by `RecordIdentifierChip` ## Conclusion As always any suggestions are more than welcomed ! Took few opinionated decision/refactor regarding nested long ternaries rendering `ReactNode` elements ## Misc https://github.com/user-attachments/assets/8ef11fb2-7ba6-4e96-bd59-b0be5a425156 --------- Co-authored-by: Mohammed Razak <mohammedrazak2001@gmail.com> Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
@ -125,7 +125,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>
|
||||||
|
|||||||
@ -51,16 +51,20 @@ export const MessageThreadSubscribersChip = ({
|
|||||||
<Chip
|
<Chip
|
||||||
label={label}
|
label={label}
|
||||||
variant={ChipVariant.Highlighted}
|
variant={ChipVariant.Highlighted}
|
||||||
leftComponent={
|
leftComponent={() => {
|
||||||
isOnlyOneSubscriber ? (
|
if (isOnlyOneSubscriber) {
|
||||||
<Avatar
|
return (
|
||||||
avatarUrl={firstAvatarUrl}
|
<Avatar
|
||||||
placeholderColorSeed={firstAvatarColorSeed}
|
avatarUrl={firstAvatarUrl}
|
||||||
placeholder={firstAvatarPlaceholder}
|
placeholderColorSeed={firstAvatarColorSeed}
|
||||||
size="md"
|
placeholder={firstAvatarPlaceholder}
|
||||||
type={'rounded'}
|
size="md"
|
||||||
/>
|
type={'rounded'}
|
||||||
) : (
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<AvatarGroup
|
<AvatarGroup
|
||||||
avatars={subscriberNames.map((name, index) => (
|
avatars={subscriberNames.map((name, index) => (
|
||||||
<Avatar
|
<Avatar
|
||||||
@ -71,9 +75,9 @@ export const MessageThreadSubscribersChip = ({
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}}
|
||||||
rightComponent={<IconChevronDown size={theme.icon.size.sm} />}
|
rightComponent={() => <IconChevronDown size={theme.icon.size.sm} />}
|
||||||
clickable
|
clickable
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -16,7 +16,11 @@ import { useTheme } from '@emotion/react';
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { isDefined } from 'twenty-shared';
|
import { isDefined } from 'twenty-shared';
|
||||||
import { IconCalendar, OverflowingTextWithTooltip } from 'twenty-ui';
|
import {
|
||||||
|
IconCalendar,
|
||||||
|
OverflowingTextWithTooltip,
|
||||||
|
isModifiedEvent,
|
||||||
|
} from 'twenty-ui';
|
||||||
|
|
||||||
import { formatToHumanReadableDate } from '~/utils/date-utils';
|
import { formatToHumanReadableDate } from '~/utils/date-utils';
|
||||||
import { getFileNameAndExtension } from '~/utils/file/getFileNameAndExtension';
|
import { getFileNameAndExtension } from '~/utils/file/getFileNameAndExtension';
|
||||||
@ -145,7 +149,7 @@ export const AttachmentRow = ({
|
|||||||
|
|
||||||
const handleOpenDocument = (e: React.MouseEvent) => {
|
const handleOpenDocument = (e: React.MouseEvent) => {
|
||||||
// Cmd/Ctrl+click opens new tab, right click opens context menu
|
// Cmd/Ctrl+click opens new tab, right click opens context menu
|
||||||
if (e.metaKey || e.ctrlKey || e.button === 2) {
|
if (isModifiedEvent(e) || e.button === 2) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,10 @@
|
|||||||
import { AvatarChip, AvatarChipVariant } from 'twenty-ui';
|
import {
|
||||||
|
AvatarChip,
|
||||||
|
AvatarChipVariant,
|
||||||
|
ChipSize,
|
||||||
|
LinkAvatarChip,
|
||||||
|
isModifiedEvent,
|
||||||
|
} from 'twenty-ui';
|
||||||
|
|
||||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||||
import { getLinkToShowPage } from '@/object-metadata/utils/getLinkToShowPage';
|
import { getLinkToShowPage } from '@/object-metadata/utils/getLinkToShowPage';
|
||||||
@ -6,14 +12,16 @@ import { useRecordChipData } from '@/object-record/hooks/useRecordChipData';
|
|||||||
import { recordIndexOpenRecordInSelector } from '@/object-record/record-index/states/selectors/recordIndexOpenRecordInSelector';
|
import { recordIndexOpenRecordInSelector } from '@/object-record/record-index/states/selectors/recordIndexOpenRecordInSelector';
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
|
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
|
||||||
import { MouseEvent } from 'react';
|
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
export type RecordChipProps = {
|
export type RecordChipProps = {
|
||||||
objectNameSingular: string;
|
objectNameSingular: string;
|
||||||
record: ObjectRecord;
|
record: ObjectRecord;
|
||||||
className?: string;
|
className?: string;
|
||||||
variant?: AvatarChipVariant;
|
variant?: AvatarChipVariant;
|
||||||
|
forceDisableClick?: boolean;
|
||||||
|
maxWidth?: number;
|
||||||
|
to?: string | undefined;
|
||||||
|
size?: ChipSize;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RecordChip = ({
|
export const RecordChip = ({
|
||||||
@ -21,6 +29,10 @@ export const RecordChip = ({
|
|||||||
record,
|
record,
|
||||||
className,
|
className,
|
||||||
variant,
|
variant,
|
||||||
|
maxWidth,
|
||||||
|
to,
|
||||||
|
size,
|
||||||
|
forceDisableClick = false,
|
||||||
}: RecordChipProps) => {
|
}: RecordChipProps) => {
|
||||||
const { recordChipData } = useRecordChipData({
|
const { recordChipData } = useRecordChipData({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
@ -33,30 +45,52 @@ export const RecordChip = ({
|
|||||||
recordIndexOpenRecordInSelector,
|
recordIndexOpenRecordInSelector,
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleClick = (e: MouseEvent<Element>) => {
|
// TODO temporary until we create a record show page for Workspaces members
|
||||||
e.stopPropagation();
|
if (forceDisableClick) {
|
||||||
if (recordIndexOpenRecordIn === ViewOpenRecordInType.SIDE_PANEL) {
|
return (
|
||||||
openRecordInCommandMenu({
|
<AvatarChip
|
||||||
recordId: record.id,
|
size={size}
|
||||||
objectNameSingular,
|
maxWidth={maxWidth}
|
||||||
});
|
placeholderColorSeed={record.id}
|
||||||
}
|
name={recordChipData.name}
|
||||||
};
|
avatarType={recordChipData.avatarType}
|
||||||
|
avatarUrl={recordChipData.avatarUrl ?? ''}
|
||||||
|
className={className}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSidePanelViewOpenRecordInType =
|
||||||
|
recordIndexOpenRecordIn === ViewOpenRecordInType.SIDE_PANEL;
|
||||||
|
const onClick = isSidePanelViewOpenRecordInType
|
||||||
|
? () =>
|
||||||
|
openRecordInCommandMenu({
|
||||||
|
recordId: record.id,
|
||||||
|
objectNameSingular,
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AvatarChip
|
<LinkAvatarChip
|
||||||
|
size={size}
|
||||||
|
maxWidth={maxWidth}
|
||||||
placeholderColorSeed={record.id}
|
placeholderColorSeed={record.id}
|
||||||
name={recordChipData.name}
|
name={recordChipData.name}
|
||||||
avatarType={recordChipData.avatarType}
|
avatarType={recordChipData.avatarType}
|
||||||
avatarUrl={recordChipData.avatarUrl ?? ''}
|
avatarUrl={recordChipData.avatarUrl ?? ''}
|
||||||
className={className}
|
className={className}
|
||||||
variant={variant}
|
variant={variant}
|
||||||
onClick={handleClick}
|
to={to ?? getLinkToShowPage(objectNameSingular, record)}
|
||||||
to={
|
onClick={(clickEvent) => {
|
||||||
recordIndexOpenRecordIn === ViewOpenRecordInType.RECORD_PAGE
|
// TODO refactor wrapper event listener to avoid colliding events
|
||||||
? getLinkToShowPage(objectNameSingular, record)
|
clickEvent.stopPropagation();
|
||||||
: undefined
|
|
||||||
}
|
const isModifiedEventResult = isModifiedEvent(clickEvent);
|
||||||
|
if (isSidePanelViewOpenRecordInType && !isModifiedEventResult) {
|
||||||
|
clickEvent.preventDefault();
|
||||||
|
onClick?.();
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -8,8 +8,11 @@ import { isRecordBoardCardSelectedComponentFamilyState } from '@/object-record/r
|
|||||||
import { isRecordBoardCompactModeActiveComponentState } from '@/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState';
|
import { isRecordBoardCompactModeActiveComponentState } from '@/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState';
|
||||||
import { recordBoardVisibleFieldDefinitionsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardVisibleFieldDefinitionsComponentSelector';
|
import { recordBoardVisibleFieldDefinitionsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardVisibleFieldDefinitionsComponentSelector';
|
||||||
|
|
||||||
|
import { RecordBoardCardBody } from '@/object-record/record-board/record-board-card/components/RecordBoardCardBody';
|
||||||
|
import { RecordBoardCardHeader } from '@/object-record/record-board/record-board-card/components/RecordBoardCardHeader';
|
||||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||||
import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect';
|
import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect';
|
||||||
|
import { AppPath } from '@/types/AppPath';
|
||||||
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
|
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
|
||||||
import { RecordBoardScrollWrapperContext } from '@/ui/utilities/scroll/contexts/ScrollWrapperContexts';
|
import { RecordBoardScrollWrapperContext } from '@/ui/utilities/scroll/contexts/ScrollWrapperContexts';
|
||||||
import { useRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyStateV2';
|
import { useRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyStateV2';
|
||||||
@ -19,12 +22,10 @@ import styled from '@emotion/styled';
|
|||||||
import { useContext, useState } from 'react';
|
import { useContext, useState } from 'react';
|
||||||
import { InView, useInView } from 'react-intersection-observer';
|
import { InView, useInView } from 'react-intersection-observer';
|
||||||
import { useSetRecoilState } from 'recoil';
|
import { useSetRecoilState } from 'recoil';
|
||||||
|
import { isDefined } from 'twenty-shared';
|
||||||
import { AnimatedEaseInOut } from 'twenty-ui';
|
import { AnimatedEaseInOut } from 'twenty-ui';
|
||||||
import { useDebouncedCallback } from 'use-debounce';
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
import { RecordBoardCardBody } from '@/object-record/record-board/record-board-card/components/RecordBoardCardBody';
|
|
||||||
import { RecordBoardCardHeader } from '@/object-record/record-board/record-board-card/components/RecordBoardCardHeader';
|
|
||||||
import { useNavigateApp } from '~/hooks/useNavigateApp';
|
import { useNavigateApp } from '~/hooks/useNavigateApp';
|
||||||
import { AppPath } from '@/types/AppPath';
|
|
||||||
|
|
||||||
const StyledBoardCard = styled.div<{ selected: boolean }>`
|
const StyledBoardCard = styled.div<{ selected: boolean }>`
|
||||||
background-color: ${({ theme, selected }) =>
|
background-color: ${({ theme, selected }) =>
|
||||||
@ -169,7 +170,7 @@ export const RecordBoardCard = ({
|
|||||||
onMouseLeave={onMouseLeaveBoard}
|
onMouseLeave={onMouseLeaveBoard}
|
||||||
onClick={handleCardClick}
|
onClick={handleCardClick}
|
||||||
>
|
>
|
||||||
{labelIdentifierField && (
|
{isDefined(labelIdentifierField) && (
|
||||||
<RecordBoardCardHeader
|
<RecordBoardCardHeader
|
||||||
identifierFieldDefinition={labelIdentifierField}
|
identifierFieldDefinition={labelIdentifierField}
|
||||||
isCreating={isCreating}
|
isCreating={isCreating}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
import { RecordChip } from '@/object-record/components/RecordChip';
|
||||||
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
|
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
|
||||||
import { useRecordBoardSelection } from '@/object-record/record-board/hooks/useRecordBoardSelection';
|
import { useRecordBoardSelection } from '@/object-record/record-board/hooks/useRecordBoardSelection';
|
||||||
import { RecordBoardCardHeaderContainer } from '@/object-record/record-board/record-board-card/components/RecordBoardCardHeaderContainer';
|
import { RecordBoardCardHeaderContainer } from '@/object-record/record-board/record-board-card/components/RecordBoardCardHeaderContainer';
|
||||||
@ -16,14 +16,12 @@ import {
|
|||||||
} from '@/object-record/record-field/contexts/FieldContext';
|
} from '@/object-record/record-field/contexts/FieldContext';
|
||||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
import { getFieldButtonIcon } from '@/object-record/record-field/utils/getFieldButtonIcon';
|
import { getFieldButtonIcon } from '@/object-record/record-field/utils/getFieldButtonIcon';
|
||||||
import { RecordIdentifierChip } from '@/object-record/record-index/components/RecordIndexRecordChip';
|
|
||||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||||
import { recordIndexOpenRecordInSelector } from '@/object-record/record-index/states/selectors/recordIndexOpenRecordInSelector';
|
import { recordIndexOpenRecordInSelector } from '@/object-record/record-index/states/selectors/recordIndexOpenRecordInSelector';
|
||||||
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
|
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
|
||||||
import { RecordInlineCellEditMode } from '@/object-record/record-inline-cell/components/RecordInlineCellEditMode';
|
import { RecordInlineCellEditMode } from '@/object-record/record-inline-cell/components/RecordInlineCellEditMode';
|
||||||
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
|
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
|
||||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
|
||||||
import { TextInput } from '@/ui/input/components/TextInput';
|
import { TextInput } from '@/ui/input/components/TextInput';
|
||||||
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
|
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
|
||||||
import { useRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyStateV2';
|
import { useRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyStateV2';
|
||||||
@ -32,6 +30,7 @@ import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { Dispatch, SetStateAction, useContext, useState } from 'react';
|
import { Dispatch, SetStateAction, useContext, useState } from 'react';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
import { isDefined } from 'twenty-shared';
|
||||||
import {
|
import {
|
||||||
AvatarChipVariant,
|
AvatarChipVariant,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
@ -123,8 +122,6 @@ export const RecordBoardCardHeader = ({
|
|||||||
recordIndexOpenRecordInSelector,
|
recordIndexOpenRecordInSelector,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { openRecordInCommandMenu } = useCommandMenu();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RecordBoardCardHeaderContainer showCompactView={showCompactView}>
|
<RecordBoardCardHeaderContainer showCompactView={showCompactView}>
|
||||||
<StopPropagationContainer>
|
<StopPropagationContainer>
|
||||||
@ -156,7 +153,7 @@ export const RecordBoardCardHeader = ({
|
|||||||
) : isIdentifierEmpty ? (
|
) : isIdentifierEmpty ? (
|
||||||
<FieldContext.Provider
|
<FieldContext.Provider
|
||||||
value={{
|
value={{
|
||||||
recordId: (record as ObjectRecord).id,
|
recordId,
|
||||||
maxWidth: 156,
|
maxWidth: 156,
|
||||||
recoilScopeId:
|
recoilScopeId:
|
||||||
(isCreating ? 'new' : recordId) +
|
(isCreating ? 'new' : recordId) +
|
||||||
@ -182,27 +179,19 @@ export const RecordBoardCardHeader = ({
|
|||||||
<RecordInlineCell />
|
<RecordInlineCell />
|
||||||
</FieldContext.Provider>
|
</FieldContext.Provider>
|
||||||
) : (
|
) : (
|
||||||
<RecordIdentifierChip
|
isDefined(record) && (
|
||||||
objectNameSingular={objectMetadataItem.nameSingular}
|
<RecordChip
|
||||||
record={record as ObjectRecord}
|
objectNameSingular={objectMetadataItem.nameSingular}
|
||||||
variant={AvatarChipVariant.Transparent}
|
record={record}
|
||||||
maxWidth={150}
|
variant={AvatarChipVariant.Transparent}
|
||||||
onClick={
|
maxWidth={150}
|
||||||
recordIndexOpenRecordIn === ViewOpenRecordInType.SIDE_PANEL
|
to={
|
||||||
? () => {
|
recordIndexOpenRecordIn === ViewOpenRecordInType.RECORD_PAGE
|
||||||
openRecordInCommandMenu({
|
? indexIdentifierUrl(recordId)
|
||||||
recordId,
|
: undefined
|
||||||
objectNameSingular: objectMetadataItem.nameSingular,
|
}
|
||||||
});
|
/>
|
||||||
}
|
)
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
to={
|
|
||||||
recordIndexOpenRecordIn === ViewOpenRecordInType.RECORD_PAGE
|
|
||||||
? indexIdentifierUrl(recordId)
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</StopPropagationContainer>
|
</StopPropagationContainer>
|
||||||
|
|
||||||
|
|||||||
@ -1,52 +1,22 @@
|
|||||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
|
||||||
import { RecordChip } from '@/object-record/components/RecordChip';
|
import { RecordChip } from '@/object-record/components/RecordChip';
|
||||||
import { useChipFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useChipFieldDisplay';
|
import { useChipFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useChipFieldDisplay';
|
||||||
import { RecordIdentifierChip } from '@/object-record/record-index/components/RecordIndexRecordChip';
|
import { isDefined } from 'twenty-shared';
|
||||||
import { recordIndexOpenRecordInSelector } from '@/object-record/record-index/states/selectors/recordIndexOpenRecordInSelector';
|
|
||||||
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
|
|
||||||
import { useRecoilValue } from 'recoil';
|
|
||||||
import { ChipSize } from 'twenty-ui';
|
import { ChipSize } from 'twenty-ui';
|
||||||
|
|
||||||
export const ChipFieldDisplay = () => {
|
export const ChipFieldDisplay = () => {
|
||||||
const {
|
const { recordValue, objectNameSingular, labelIdentifierLink } =
|
||||||
recordValue,
|
useChipFieldDisplay();
|
||||||
objectNameSingular,
|
|
||||||
isLabelIdentifier,
|
|
||||||
labelIdentifierLink,
|
|
||||||
} = useChipFieldDisplay();
|
|
||||||
|
|
||||||
const recordIndexOpenRecordIn = useRecoilValue(
|
if (!isDefined(recordValue)) {
|
||||||
recordIndexOpenRecordInSelector,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { openRecordInCommandMenu } = useCommandMenu();
|
|
||||||
|
|
||||||
if (!recordValue) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return isLabelIdentifier ? (
|
return (
|
||||||
<RecordIdentifierChip
|
<RecordChip
|
||||||
objectNameSingular={objectNameSingular}
|
objectNameSingular={objectNameSingular}
|
||||||
record={recordValue}
|
record={recordValue}
|
||||||
size={ChipSize.Small}
|
size={ChipSize.Small}
|
||||||
to={
|
to={labelIdentifierLink}
|
||||||
recordIndexOpenRecordIn === ViewOpenRecordInType.RECORD_PAGE
|
|
||||||
? labelIdentifierLink
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
onClick={
|
|
||||||
recordIndexOpenRecordIn === ViewOpenRecordInType.SIDE_PANEL
|
|
||||||
? () => {
|
|
||||||
openRecordInCommandMenu({
|
|
||||||
recordId: recordValue.id,
|
|
||||||
objectNameSingular,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
|
||||||
<RecordChip objectNameSingular={objectNameSingular} record={recordValue} />
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,17 +1,22 @@
|
|||||||
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { RecordChip } from '@/object-record/components/RecordChip';
|
import { RecordChip } from '@/object-record/components/RecordChip';
|
||||||
import { useRelationToOneFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useRelationToOneFieldDisplay';
|
import { useRelationToOneFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useRelationToOneFieldDisplay';
|
||||||
|
import { isDefined } from 'twenty-shared';
|
||||||
|
|
||||||
export const RelationToOneFieldDisplay = () => {
|
export const RelationToOneFieldDisplay = () => {
|
||||||
const { fieldValue, fieldDefinition, generateRecordChipData } =
|
const { fieldValue, fieldDefinition, generateRecordChipData } =
|
||||||
useRelationToOneFieldDisplay();
|
useRelationToOneFieldDisplay();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!fieldValue ||
|
!isDefined(fieldValue) ||
|
||||||
!fieldDefinition?.metadata.relationObjectMetadataNameSingular
|
!isDefined(fieldDefinition?.metadata.relationObjectMetadataNameSingular)
|
||||||
) {
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isWorkspaceMemberFieldMetadataRelation =
|
||||||
|
fieldDefinition.metadata.relationObjectMetadataNameSingular ===
|
||||||
|
CoreObjectNameSingular.WorkspaceMember;
|
||||||
const recordChipData = generateRecordChipData(fieldValue);
|
const recordChipData = generateRecordChipData(fieldValue);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -19,6 +24,7 @@ export const RelationToOneFieldDisplay = () => {
|
|||||||
key={recordChipData.recordId}
|
key={recordChipData.recordId}
|
||||||
objectNameSingular={recordChipData.objectNameSingular}
|
objectNameSingular={recordChipData.objectNameSingular}
|
||||||
record={fieldValue}
|
record={fieldValue}
|
||||||
|
forceDisableClick={isWorkspaceMemberFieldMetadataRelation}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,53 +0,0 @@
|
|||||||
import { useGetStandardObjectIcon } from '@/object-metadata/hooks/useGetStandardObjectIcon';
|
|
||||||
import { useRecordChipData } from '@/object-record/hooks/useRecordChipData';
|
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
|
||||||
import { isNonEmptyString } from '@sniptt/guards';
|
|
||||||
import { AvatarChip, AvatarChipVariant, ChipSize } from 'twenty-ui';
|
|
||||||
|
|
||||||
export type RecordIdentifierChipProps = {
|
|
||||||
objectNameSingular: string;
|
|
||||||
record: ObjectRecord;
|
|
||||||
variant?: AvatarChipVariant;
|
|
||||||
size?: ChipSize;
|
|
||||||
to?: string;
|
|
||||||
maxWidth?: number;
|
|
||||||
onClick?: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const RecordIdentifierChip = ({
|
|
||||||
objectNameSingular,
|
|
||||||
record,
|
|
||||||
variant,
|
|
||||||
size,
|
|
||||||
onClick,
|
|
||||||
to,
|
|
||||||
maxWidth,
|
|
||||||
}: RecordIdentifierChipProps) => {
|
|
||||||
const { recordChipData } = useRecordChipData({
|
|
||||||
objectNameSingular,
|
|
||||||
record,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { Icon: LeftIcon, IconColor: LeftIconColor } =
|
|
||||||
useGetStandardObjectIcon(objectNameSingular);
|
|
||||||
|
|
||||||
if (!isNonEmptyString(recordChipData.name.trim())) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AvatarChip
|
|
||||||
placeholderColorSeed={record.id}
|
|
||||||
name={recordChipData.name}
|
|
||||||
avatarType={recordChipData.avatarType}
|
|
||||||
avatarUrl={recordChipData.avatarUrl ?? ''}
|
|
||||||
to={to}
|
|
||||||
onClick={onClick}
|
|
||||||
variant={variant}
|
|
||||||
LeftIcon={LeftIcon}
|
|
||||||
LeftIconColor={LeftIconColor}
|
|
||||||
size={size}
|
|
||||||
maxWidth={maxWidth}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -4,7 +4,6 @@ import { ConnectedAccountProvider } from 'twenty-shared';
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
AvatarChip,
|
AvatarChip,
|
||||||
AvatarChipVariant,
|
|
||||||
IconApi,
|
IconApi,
|
||||||
IconCalendar,
|
IconCalendar,
|
||||||
IconCsv,
|
IconCsv,
|
||||||
@ -71,7 +70,6 @@ export const ActorDisplay = ({
|
|||||||
LeftIcon={LeftIcon}
|
LeftIcon={LeftIcon}
|
||||||
avatarUrl={avatarUrl ?? undefined}
|
avatarUrl={avatarUrl ?? undefined}
|
||||||
isIconInverted={isIconInverted}
|
isIconInverted={isIconInverted}
|
||||||
variant={AvatarChipVariant.Transparent}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { ReactElement, useCallback, useEffect, useRef, useState } from 'react';
|
import { ReactElement, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { AnimatedContainer, Chip, ChipVariant } from 'twenty-ui';
|
import {
|
||||||
|
AnimatedContainer,
|
||||||
|
ChipSize,
|
||||||
|
OverflowingTextWithTooltip,
|
||||||
|
} from 'twenty-ui';
|
||||||
|
|
||||||
import { ExpandedListDropdown } from '@/ui/layout/expandable-list/components/ExpandedListDropdown';
|
import { ExpandedListDropdown } from '@/ui/layout/expandable-list/components/ExpandedListDropdown';
|
||||||
import { isFirstOverflowingChildElement } from '@/ui/layout/expandable-list/utils/isFirstOverflowingChildElement';
|
import { isFirstOverflowingChildElement } from '@/ui/layout/expandable-list/utils/isFirstOverflowingChildElement';
|
||||||
@ -34,7 +38,7 @@ const StyledChildContainer = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledChipCount = styled(Chip)`
|
const StyledUnShrinkableContainer = styled.div`
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -150,11 +154,12 @@ export const ExpandableList = ({
|
|||||||
</StyledChildrenContainer>
|
</StyledChildrenContainer>
|
||||||
{canDisplayChipCount && (
|
{canDisplayChipCount && (
|
||||||
<AnimatedContainer>
|
<AnimatedContainer>
|
||||||
<StyledChipCount
|
<StyledUnShrinkableContainer onClick={handleChipCountClick}>
|
||||||
label={`+${hiddenChildrenCount}`}
|
<OverflowingTextWithTooltip
|
||||||
variant={ChipVariant.Highlighted}
|
text={`+${hiddenChildrenCount}`}
|
||||||
onClick={handleChipCountClick}
|
size={ChipSize.Small}
|
||||||
/>
|
/>
|
||||||
|
</StyledUnShrinkableContainer>
|
||||||
</AnimatedContainer>
|
</AnimatedContainer>
|
||||||
)}
|
)}
|
||||||
{isListExpanded && (
|
{isListExpanded && (
|
||||||
|
|||||||
@ -102,7 +102,7 @@ export const RightDrawerTopBar = () => {
|
|||||||
<Chip
|
<Chip
|
||||||
disabled={isNewViewableRecordLoading}
|
disabled={isNewViewableRecordLoading}
|
||||||
label={label}
|
label={label}
|
||||||
leftComponent={<Icon size={theme.icon.size.md} />}
|
leftComponent={() => <Icon size={theme.icon.size.md} />}
|
||||||
size={ChipSize.Large}
|
size={ChipSize.Large}
|
||||||
accent={ChipAccent.TextSecondary}
|
accent={ChipAccent.TextSecondary}
|
||||||
clickable={false}
|
clickable={false}
|
||||||
|
|||||||
@ -0,0 +1,37 @@
|
|||||||
|
import { AvatarChipsLeftComponent } from '@ui/display/avatar-chip/components/AvatarChipLeftComponent';
|
||||||
|
import { AvatarChipsCommonProps } from '@ui/display/avatar-chip/types/AvatarChipsCommonProps.type';
|
||||||
|
import { Chip, ChipVariant } from '@ui/display/chip/components/Chip';
|
||||||
|
|
||||||
|
export type AvatarChipProps = AvatarChipsCommonProps;
|
||||||
|
export const AvatarChip = ({
|
||||||
|
name,
|
||||||
|
LeftIcon,
|
||||||
|
LeftIconColor,
|
||||||
|
avatarType,
|
||||||
|
avatarUrl,
|
||||||
|
className,
|
||||||
|
isIconInverted,
|
||||||
|
maxWidth,
|
||||||
|
placeholderColorSeed,
|
||||||
|
size,
|
||||||
|
}: AvatarChipProps) => (
|
||||||
|
<Chip
|
||||||
|
label={name}
|
||||||
|
variant={ChipVariant.Transparent}
|
||||||
|
size={size}
|
||||||
|
leftComponent={() => (
|
||||||
|
<AvatarChipsLeftComponent
|
||||||
|
name={name}
|
||||||
|
LeftIcon={LeftIcon}
|
||||||
|
LeftIconColor={LeftIconColor}
|
||||||
|
avatarType={avatarType}
|
||||||
|
avatarUrl={avatarUrl}
|
||||||
|
isIconInverted={isIconInverted}
|
||||||
|
placeholderColorSeed={placeholderColorSeed}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
clickable={false}
|
||||||
|
className={className}
|
||||||
|
maxWidth={maxWidth}
|
||||||
|
/>
|
||||||
|
);
|
||||||
@ -0,0 +1,74 @@
|
|||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { Avatar } from '@ui/display/avatar/components/Avatar';
|
||||||
|
import { AvatarType } from '@ui/display/avatar/types/AvatarType';
|
||||||
|
import { IconComponent } from '@ui/display/icon/types/IconComponent';
|
||||||
|
import { isDefined } from 'twenty-shared';
|
||||||
|
import { Nullable } from 'vitest';
|
||||||
|
|
||||||
|
const StyledInvertedIconContainer = styled.div<{ backgroundColor: string }>`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: ${({ backgroundColor }) => backgroundColor};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export type AvatarChipsLeftComponentProps = {
|
||||||
|
name: string;
|
||||||
|
avatarUrl?: string;
|
||||||
|
avatarType?: Nullable<AvatarType>;
|
||||||
|
LeftIcon?: IconComponent;
|
||||||
|
LeftIconColor?: string;
|
||||||
|
isIconInverted?: boolean;
|
||||||
|
placeholderColorSeed?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AvatarChipsLeftComponent: React.FC<
|
||||||
|
AvatarChipsLeftComponentProps
|
||||||
|
> = ({
|
||||||
|
LeftIcon,
|
||||||
|
placeholderColorSeed,
|
||||||
|
avatarType,
|
||||||
|
avatarUrl,
|
||||||
|
name,
|
||||||
|
isIconInverted = false,
|
||||||
|
LeftIconColor,
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
if (!isDefined(LeftIcon)) {
|
||||||
|
return (
|
||||||
|
<Avatar
|
||||||
|
avatarUrl={avatarUrl}
|
||||||
|
placeholderColorSeed={placeholderColorSeed}
|
||||||
|
placeholder={name}
|
||||||
|
size="sm"
|
||||||
|
type={avatarType}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isIconInverted) {
|
||||||
|
return (
|
||||||
|
<StyledInvertedIconContainer
|
||||||
|
backgroundColor={theme.background.invertedSecondary}
|
||||||
|
>
|
||||||
|
<LeftIcon
|
||||||
|
color="white"
|
||||||
|
size={theme.icon.size.md}
|
||||||
|
stroke={theme.icon.stroke.sm}
|
||||||
|
/>
|
||||||
|
</StyledInvertedIconContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LeftIcon
|
||||||
|
size={theme.icon.size.md}
|
||||||
|
stroke={theme.icon.stroke.sm}
|
||||||
|
color={LeftIconColor || 'currentColor'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
import { AvatarChipsLeftComponent } from '@ui/display/avatar-chip/components/AvatarChipLeftComponent';
|
||||||
|
import { AvatarChipsCommonProps } from '@ui/display/avatar-chip/types/AvatarChipsCommonProps.type';
|
||||||
|
import { AvatarChipVariant } from '@ui/display/avatar-chip/types/AvatarChipsVariant.type';
|
||||||
|
import { ChipVariant } from '@ui/display/chip/components/Chip';
|
||||||
|
import { LinkChip, LinkChipProps } from '@ui/display/chip/components/LinkChip';
|
||||||
|
|
||||||
|
export type LinkAvatarChipProps = Omit<AvatarChipsCommonProps, 'clickable'> & {
|
||||||
|
to: string;
|
||||||
|
onClick?: LinkChipProps['onClick'];
|
||||||
|
variant?: AvatarChipVariant;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LinkAvatarChip = ({
|
||||||
|
to,
|
||||||
|
onClick,
|
||||||
|
name,
|
||||||
|
LeftIcon,
|
||||||
|
LeftIconColor,
|
||||||
|
avatarType,
|
||||||
|
avatarUrl,
|
||||||
|
className,
|
||||||
|
isIconInverted,
|
||||||
|
maxWidth,
|
||||||
|
placeholderColorSeed,
|
||||||
|
size,
|
||||||
|
variant,
|
||||||
|
}: LinkAvatarChipProps) => (
|
||||||
|
<LinkChip
|
||||||
|
to={to}
|
||||||
|
onClick={onClick}
|
||||||
|
label={name}
|
||||||
|
variant={
|
||||||
|
//Regular but Highlighted -> missleading
|
||||||
|
variant === AvatarChipVariant.Regular
|
||||||
|
? ChipVariant.Highlighted
|
||||||
|
: ChipVariant.Regular
|
||||||
|
}
|
||||||
|
size={size}
|
||||||
|
leftComponent={() => (
|
||||||
|
<AvatarChipsLeftComponent
|
||||||
|
name={name}
|
||||||
|
LeftIcon={LeftIcon}
|
||||||
|
LeftIconColor={LeftIconColor}
|
||||||
|
avatarType={avatarType}
|
||||||
|
avatarUrl={avatarUrl}
|
||||||
|
isIconInverted={isIconInverted}
|
||||||
|
placeholderColorSeed={placeholderColorSeed}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
className={className}
|
||||||
|
maxWidth={maxWidth}
|
||||||
|
/>
|
||||||
|
);
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
import { AvatarChipsLeftComponentProps } from '@ui/display/avatar-chip/components/AvatarChipLeftComponent';
|
||||||
|
import { ChipSize } from '@ui/display/chip/components/Chip';
|
||||||
|
|
||||||
|
export type AvatarChipsCommonProps = {
|
||||||
|
size?: ChipSize;
|
||||||
|
className?: string;
|
||||||
|
maxWidth?: number;
|
||||||
|
} & AvatarChipsLeftComponentProps;
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
export enum AvatarChipVariant {
|
||||||
|
Regular = 'regular',
|
||||||
|
Transparent = 'transparent',
|
||||||
|
}
|
||||||
@ -1,121 +0,0 @@
|
|||||||
import { styled } from '@linaria/react';
|
|
||||||
import { Avatar } from '@ui/display/avatar/components/Avatar';
|
|
||||||
import { AvatarType } from '@ui/display/avatar/types/AvatarType';
|
|
||||||
import { Chip, ChipSize, ChipVariant } from '@ui/display/chip/components/Chip';
|
|
||||||
import { IconComponent } from '@ui/display/icon/types/IconComponent';
|
|
||||||
import { ThemeContext } from '@ui/theme';
|
|
||||||
import { Nullable } from '@ui/utilities/types/Nullable';
|
|
||||||
import { MouseEvent, useContext } from 'react';
|
|
||||||
import { isDefined } from 'twenty-shared';
|
|
||||||
|
|
||||||
// Import Link from react-router-dom instead of UndecoratedLink
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
export type AvatarChipProps = {
|
|
||||||
name: string;
|
|
||||||
avatarUrl?: string;
|
|
||||||
avatarType?: Nullable<AvatarType>;
|
|
||||||
variant?: AvatarChipVariant;
|
|
||||||
size?: ChipSize;
|
|
||||||
LeftIcon?: IconComponent;
|
|
||||||
LeftIconColor?: string;
|
|
||||||
isIconInverted?: boolean;
|
|
||||||
className?: string;
|
|
||||||
placeholderColorSeed?: string;
|
|
||||||
onClick?: (event: MouseEvent) => void;
|
|
||||||
to?: string;
|
|
||||||
maxWidth?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export enum AvatarChipVariant {
|
|
||||||
Regular = 'regular',
|
|
||||||
Transparent = 'transparent',
|
|
||||||
}
|
|
||||||
|
|
||||||
const StyledInvertedIconContainer = styled.div<{ backgroundColor: string }>`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 14px;
|
|
||||||
height: 14px;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: ${({ backgroundColor }) => backgroundColor};
|
|
||||||
`;
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const AvatarChip = ({
|
|
||||||
name,
|
|
||||||
avatarUrl,
|
|
||||||
avatarType = 'rounded',
|
|
||||||
variant = AvatarChipVariant.Regular,
|
|
||||||
LeftIcon,
|
|
||||||
LeftIconColor,
|
|
||||||
isIconInverted,
|
|
||||||
className,
|
|
||||||
placeholderColorSeed,
|
|
||||||
onClick,
|
|
||||||
to,
|
|
||||||
size = ChipSize.Small,
|
|
||||||
maxWidth,
|
|
||||||
}: AvatarChipProps) => {
|
|
||||||
const { theme } = useContext(ThemeContext);
|
|
||||||
|
|
||||||
const chip = (
|
|
||||||
<Chip
|
|
||||||
label={name}
|
|
||||||
variant={
|
|
||||||
isDefined(onClick) || isDefined(to)
|
|
||||||
? variant === AvatarChipVariant.Regular
|
|
||||||
? ChipVariant.Highlighted
|
|
||||||
: ChipVariant.Regular
|
|
||||||
: ChipVariant.Transparent
|
|
||||||
}
|
|
||||||
size={size}
|
|
||||||
leftComponent={
|
|
||||||
isDefined(LeftIcon) ? (
|
|
||||||
isIconInverted === true ? (
|
|
||||||
<StyledInvertedIconContainer
|
|
||||||
backgroundColor={theme.background.invertedSecondary}
|
|
||||||
>
|
|
||||||
<LeftIcon
|
|
||||||
color="white"
|
|
||||||
size={theme.icon.size.sm}
|
|
||||||
stroke={theme.icon.stroke.sm}
|
|
||||||
/>
|
|
||||||
</StyledInvertedIconContainer>
|
|
||||||
) : (
|
|
||||||
<LeftIcon
|
|
||||||
size={theme.icon.size.sm}
|
|
||||||
stroke={theme.icon.stroke.sm}
|
|
||||||
color={LeftIconColor || 'currentColor'}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<Avatar
|
|
||||||
avatarUrl={avatarUrl}
|
|
||||||
placeholderColorSeed={placeholderColorSeed}
|
|
||||||
placeholder={name}
|
|
||||||
size="sm"
|
|
||||||
type={avatarType}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
clickable={isDefined(onClick) || isDefined(to)}
|
|
||||||
onClick={to ? undefined : onClick}
|
|
||||||
className={className}
|
|
||||||
maxWidth={maxWidth}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isDefined(to)) return chip;
|
|
||||||
return (
|
|
||||||
<StyledLink to={to} onClick={onClick}>
|
|
||||||
{chip}
|
|
||||||
</StyledLink>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { Theme, withTheme } from '@emotion/react';
|
import { Theme, withTheme } from '@emotion/react';
|
||||||
import { styled } from '@linaria/react';
|
import { styled } from '@linaria/react';
|
||||||
import { MouseEvent, ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
import { OverflowingTextWithTooltip } from '@ui/display/tooltip/OverflowingTextWithTooltip';
|
import { OverflowingTextWithTooltip } from '@ui/display/tooltip/OverflowingTextWithTooltip';
|
||||||
|
|
||||||
@ -21,7 +21,7 @@ export enum ChipVariant {
|
|||||||
Rounded = 'rounded',
|
Rounded = 'rounded',
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChipProps = {
|
export type ChipProps = {
|
||||||
size?: ChipSize;
|
size?: ChipSize;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
clickable?: boolean;
|
clickable?: boolean;
|
||||||
@ -29,10 +29,9 @@ type ChipProps = {
|
|||||||
maxWidth?: number;
|
maxWidth?: number;
|
||||||
variant?: ChipVariant;
|
variant?: ChipVariant;
|
||||||
accent?: ChipAccent;
|
accent?: ChipAccent;
|
||||||
leftComponent?: ReactNode;
|
leftComponent?: (() => ReactNode) | null;
|
||||||
rightComponent?: ReactNode;
|
rightComponent?: (() => ReactNode) | null;
|
||||||
className?: string;
|
className?: string;
|
||||||
onClick?: (event: MouseEvent<HTMLDivElement>) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledContainer = withTheme(styled.div<
|
const StyledContainer = withTheme(styled.div<
|
||||||
@ -128,10 +127,9 @@ export const Chip = ({
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
clickable = true,
|
clickable = true,
|
||||||
variant = ChipVariant.Regular,
|
variant = ChipVariant.Regular,
|
||||||
leftComponent,
|
leftComponent = null,
|
||||||
rightComponent,
|
rightComponent = null,
|
||||||
accent = ChipAccent.TextPrimary,
|
accent = ChipAccent.TextPrimary,
|
||||||
onClick,
|
|
||||||
className,
|
className,
|
||||||
maxWidth,
|
maxWidth,
|
||||||
}: ChipProps) => {
|
}: ChipProps) => {
|
||||||
@ -143,13 +141,12 @@ export const Chip = ({
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
size={size}
|
size={size}
|
||||||
variant={variant}
|
variant={variant}
|
||||||
onClick={onClick}
|
|
||||||
className={className}
|
className={className}
|
||||||
maxWidth={maxWidth}
|
maxWidth={maxWidth}
|
||||||
>
|
>
|
||||||
{leftComponent}
|
{leftComponent?.()}
|
||||||
<OverflowingTextWithTooltip size={size} text={label} />
|
<OverflowingTextWithTooltip size={size} text={label} />
|
||||||
{rightComponent}
|
{rightComponent?.()}
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
53
packages/twenty-ui/src/display/chip/components/LinkChip.tsx
Normal file
53
packages/twenty-ui/src/display/chip/components/LinkChip.tsx
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
import {
|
||||||
|
Chip,
|
||||||
|
ChipAccent,
|
||||||
|
ChipProps,
|
||||||
|
ChipSize,
|
||||||
|
ChipVariant,
|
||||||
|
} from '@ui/display/chip/components/Chip';
|
||||||
|
import { MouseEvent } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
export type LinkChipProps = Omit<
|
||||||
|
ChipProps,
|
||||||
|
'onClick' | 'disabled' | 'clickable'
|
||||||
|
> & {
|
||||||
|
to: string;
|
||||||
|
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;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const LinkChip = ({
|
||||||
|
to,
|
||||||
|
size = ChipSize.Small,
|
||||||
|
label,
|
||||||
|
variant = ChipVariant.Regular,
|
||||||
|
leftComponent = null,
|
||||||
|
rightComponent = null,
|
||||||
|
accent = ChipAccent.TextPrimary,
|
||||||
|
className,
|
||||||
|
maxWidth,
|
||||||
|
onClick,
|
||||||
|
}: LinkChipProps) => {
|
||||||
|
return (
|
||||||
|
<StyledLink to={to} onClick={onClick}>
|
||||||
|
<Chip
|
||||||
|
size={size}
|
||||||
|
label={label}
|
||||||
|
clickable={true}
|
||||||
|
variant={variant}
|
||||||
|
leftComponent={leftComponent}
|
||||||
|
rightComponent={rightComponent}
|
||||||
|
accent={accent}
|
||||||
|
className={className}
|
||||||
|
maxWidth={maxWidth}
|
||||||
|
/>
|
||||||
|
</StyledLink>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { Meta, StoryObj } from '@storybook/react';
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
import { AvatarChip } from '@ui/display/chip/components/AvatarChip';
|
import { AvatarChip } from '@ui/display/avatar-chip/components/AvatarChip';
|
||||||
|
|
||||||
import { ComponentDecorator, RouterDecorator } from '@ui/testing';
|
import { ComponentDecorator, RouterDecorator } from '@ui/testing';
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,8 @@
|
|||||||
|
export * from './avatar-chip/components/AvatarChip';
|
||||||
|
export * from './avatar-chip/components/AvatarChipLeftComponent';
|
||||||
|
export * from './avatar-chip/components/LinkAvatarChip';
|
||||||
|
export * from './avatar-chip/types/AvatarChipsCommonProps.type';
|
||||||
|
export * from './avatar-chip/types/AvatarChipsVariant.type';
|
||||||
export * from './avatar/components/Avatar';
|
export * from './avatar/components/Avatar';
|
||||||
export * from './avatar/components/AvatarGroup';
|
export * from './avatar/components/AvatarGroup';
|
||||||
export * from './avatar/components/states/isInvalidAvatarUrlState';
|
export * from './avatar/components/states/isInvalidAvatarUrlState';
|
||||||
@ -7,8 +12,8 @@ export * from './avatar/types/AvatarType';
|
|||||||
export * from './banner/components/Banner';
|
export * from './banner/components/Banner';
|
||||||
export * from './checkmark/components/AnimatedCheckmark';
|
export * from './checkmark/components/AnimatedCheckmark';
|
||||||
export * from './checkmark/components/Checkmark';
|
export * from './checkmark/components/Checkmark';
|
||||||
export * from './chip/components/AvatarChip';
|
|
||||||
export * from './chip/components/Chip';
|
export * from './chip/components/Chip';
|
||||||
|
export * from './chip/components/LinkChip';
|
||||||
export * from './color/components/ColorSample';
|
export * from './color/components/ColorSample';
|
||||||
export * from './icon/components/IconAddressBook';
|
export * from './icon/components/IconAddressBook';
|
||||||
export * from './icon/components/IconGmail';
|
export * from './icon/components/IconGmail';
|
||||||
|
|||||||
16
packages/twenty-ui/src/utilities/events/isModifiedEvent.ts
Normal file
16
packages/twenty-ui/src/utilities/events/isModifiedEvent.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
type LimitedMouseEvent = Pick<
|
||||||
|
MouseEvent,
|
||||||
|
'button' | 'metaKey' | 'altKey' | 'ctrlKey' | 'shiftKey'
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const isModifiedEvent = ({
|
||||||
|
altKey,
|
||||||
|
ctrlKey,
|
||||||
|
shiftKey,
|
||||||
|
metaKey,
|
||||||
|
button,
|
||||||
|
}: LimitedMouseEvent) => {
|
||||||
|
const pressedKey = [altKey, ctrlKey, shiftKey, metaKey].some((key) => key);
|
||||||
|
const isLeftClick = button === 0;
|
||||||
|
return pressedKey || !isLeftClick;
|
||||||
|
};
|
||||||
@ -10,6 +10,7 @@ export * from './device/getOsControlSymbol';
|
|||||||
export * from './device/getOsShortcutSeparator';
|
export * from './device/getOsShortcutSeparator';
|
||||||
export * from './device/getUserDevice';
|
export * from './device/getUserDevice';
|
||||||
export * from './dimensions/components/AutogrowWrapper';
|
export * from './dimensions/components/AutogrowWrapper';
|
||||||
|
export * from './events/isModifiedEvent';
|
||||||
export * from './responsive/hooks/useIsMobile';
|
export * from './responsive/hooks/useIsMobile';
|
||||||
export * from './screen-size/hooks/useScreenSize';
|
export * from './screen-size/hooks/useScreenSize';
|
||||||
export * from './state/utils/createState';
|
export * from './state/utils/createState';
|
||||||
|
|||||||
Reference in New Issue
Block a user