4900 multi select field front implement expanded cells (#5151)
Add expanded cell https://github.com/twentyhq/twenty/assets/29927851/363f2b44-7b3c-4771-a651-dfc4014da6ac 
This commit is contained in:
@ -11,11 +11,11 @@ import { PageTitle } from '@/ui/utilities/page-title/PageTitle';
|
||||
import { CommandMenuEffect } from '~/effect-components/CommandMenuEffect';
|
||||
import { GotoHotkeysEffect } from '~/effect-components/GotoHotkeysEffect';
|
||||
import { Authorize } from '~/pages/auth/Authorize';
|
||||
import { ChooseYourPlan } from '~/pages/auth/ChooseYourPlan.tsx';
|
||||
import { ChooseYourPlan } from '~/pages/auth/ChooseYourPlan';
|
||||
import { CreateProfile } from '~/pages/auth/CreateProfile';
|
||||
import { CreateWorkspace } from '~/pages/auth/CreateWorkspace';
|
||||
import { PasswordReset } from '~/pages/auth/PasswordReset';
|
||||
import { PaymentSuccess } from '~/pages/auth/PaymentSuccess.tsx';
|
||||
import { PaymentSuccess } from '~/pages/auth/PaymentSuccess';
|
||||
import { SignInUp } from '~/pages/auth/SignInUp';
|
||||
import { DefaultHomePage } from '~/pages/DefaultHomePage';
|
||||
import { ImpersonateEffect } from '~/pages/impersonate/ImpersonateEffect';
|
||||
@ -46,7 +46,7 @@ import { SettingsIntegrationNewDatabaseConnection } from '~/pages/settings/integ
|
||||
import { SettingsIntegrations } from '~/pages/settings/integrations/SettingsIntegrations';
|
||||
import { SettingsIntegrationShowDatabaseConnection } from '~/pages/settings/integrations/SettingsIntegrationShowDatabaseConnection';
|
||||
import { SettingsAppearance } from '~/pages/settings/SettingsAppearance';
|
||||
import { SettingsBilling } from '~/pages/settings/SettingsBilling.tsx';
|
||||
import { SettingsBilling } from '~/pages/settings/SettingsBilling';
|
||||
import { SettingsProfile } from '~/pages/settings/SettingsProfile';
|
||||
import { SettingsWorkspace } from '~/pages/settings/SettingsWorkspace';
|
||||
import { SettingsWorkspaceMembers } from '~/pages/settings/SettingsWorkspaceMembers';
|
||||
|
||||
@ -1,14 +1,15 @@
|
||||
import { useRef } from 'react';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { IconCheck, IconQuestionMark, IconX } from 'twenty-ui';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { CalendarEventParticipant } from '@/activities/calendar/types/CalendarEventParticipant';
|
||||
import { ParticipantChip } from '@/activities/components/ParticipantChip';
|
||||
import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox';
|
||||
import { ExpandableList } from '@/ui/display/expandable-list/ExpandableList';
|
||||
import { EllipsisDisplay } from '@/ui/field/display/components/EllipsisDisplay';
|
||||
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
|
||||
import { isRightDrawerAnimationCompletedState } from '@/ui/layout/right-drawer/states/isRightDrawerAnimationCompleted';
|
||||
|
||||
const StyledInlineCellBaseContainer = styled.div`
|
||||
align-items: center;
|
||||
@ -55,6 +56,9 @@ const StyledLabelContainer = styled.div<{ width?: number }>`
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
width: ${({ width }) => width}px;
|
||||
`;
|
||||
const StyledDiv = styled.div`
|
||||
max-width: 70%;
|
||||
`;
|
||||
|
||||
export const CalendarEventParticipantsResponseStatusField = ({
|
||||
responseStatus,
|
||||
@ -64,6 +68,9 @@ export const CalendarEventParticipantsResponseStatusField = ({
|
||||
participants: CalendarEventParticipant[];
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const isRightDrawerAnimationCompleted = useRecoilValue(
|
||||
isRightDrawerAnimationCompletedState,
|
||||
);
|
||||
|
||||
const Icon = {
|
||||
Yes: <IconCheck stroke={theme.icon.stroke.sm} />,
|
||||
@ -81,9 +88,9 @@ export const CalendarEventParticipantsResponseStatusField = ({
|
||||
];
|
||||
|
||||
const participantsContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const StyledChips = orderedParticipants.map((participant) => (
|
||||
<ParticipantChip participant={participant} />
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const styledChips = orderedParticipants.map((participant, index) => (
|
||||
<ParticipantChip key={index} participant={participant} />
|
||||
));
|
||||
|
||||
return (
|
||||
@ -96,12 +103,21 @@ export const CalendarEventParticipantsResponseStatusField = ({
|
||||
<EllipsisDisplay>{responseStatus}</EllipsisDisplay>
|
||||
</StyledLabelContainer>
|
||||
</StyledLabelAndIconContainer>
|
||||
|
||||
<ExpandableList
|
||||
listItems={StyledChips}
|
||||
id={v4()}
|
||||
rootRef={participantsContainerRef}
|
||||
/>
|
||||
<StyledDiv
|
||||
ref={participantsContainerRef}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
{isRightDrawerAnimationCompleted && (
|
||||
<ExpandableList
|
||||
isHovered={isHovered}
|
||||
reference={participantsContainerRef.current || undefined}
|
||||
forceDisplayHiddenCount
|
||||
>
|
||||
{styledChips}
|
||||
</ExpandableList>
|
||||
)}
|
||||
</StyledDiv>
|
||||
</StyledInlineCellBaseContainer>
|
||||
</StyledPropertyBox>
|
||||
);
|
||||
|
||||
@ -14,6 +14,7 @@ import {
|
||||
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
|
||||
import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { isRightDrawerAnimationCompletedState } from '@/ui/layout/right-drawer/states/isRightDrawerAnimationCompleted';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
const StyledPropertyBox = styled(PropertyBox)`
|
||||
@ -27,6 +28,10 @@ export const ActivityEditorFields = ({
|
||||
}) => {
|
||||
const { upsertActivity } = useUpsertActivity();
|
||||
|
||||
const isRightDrawerAnimationCompleted = useRecoilValue(
|
||||
isRightDrawerAnimationCompletedState,
|
||||
);
|
||||
|
||||
const getRecordFromCache = useGetRecordFromCache({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
});
|
||||
@ -93,11 +98,16 @@ export const ActivityEditorFields = ({
|
||||
</AssigneeFieldContextProvider>
|
||||
</>
|
||||
)}
|
||||
{ActivityTargetsContextProvider && isDefined(activityFromCache) && (
|
||||
<ActivityTargetsContextProvider>
|
||||
<ActivityTargetsInlineCell activity={activityFromCache} />
|
||||
</ActivityTargetsContextProvider>
|
||||
)}
|
||||
{ActivityTargetsContextProvider &&
|
||||
isDefined(activityFromCache) &&
|
||||
isRightDrawerAnimationCompleted && (
|
||||
<ActivityTargetsContextProvider>
|
||||
<ActivityTargetsInlineCell
|
||||
activity={activityFromCache}
|
||||
maxWidth={340}
|
||||
/>
|
||||
</ActivityTargetsContextProvider>
|
||||
)}
|
||||
</StyledPropertyBox>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,91 +1,47 @@
|
||||
import { useMemo } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { Chip, ChipVariant } from 'twenty-ui';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { ActivityTargetWithTargetRecord } from '@/activities/types/ActivityTargetObject';
|
||||
import { RecordChip } from '@/object-record/components/RecordChip';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import {
|
||||
ExpandableList,
|
||||
ExpandableListProps,
|
||||
} from '@/ui/layout/expandable-list/components/ExpandableList';
|
||||
|
||||
const MAX_RECORD_CHIPS_DISPLAY = 2;
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
const StyledContainer = styled.div<{ maxWidth?: number }>`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
max-width: ${({ maxWidth }) => `${maxWidth}px` || 'none'};
|
||||
`;
|
||||
|
||||
const StyledRelationsListContainer = styled(StyledContainer)`
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
border-radius: ${({ theme }) => theme.spacing(1)};
|
||||
background-color: ${({ theme }) => theme.background.secondary};
|
||||
box-shadow: '0px 2px 4px ${({ theme }) =>
|
||||
theme.boxShadow.light}, 2px 4px 16px ${({ theme }) =>
|
||||
theme.boxShadow.strong}';
|
||||
backdrop-filter: ${({ theme }) => theme.blur.strong};
|
||||
`;
|
||||
|
||||
const showMoreRelationsHandler = (event?: React.MouseEvent<HTMLDivElement>) => {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
};
|
||||
|
||||
export const ActivityTargetChips = ({
|
||||
activityTargetObjectRecords,
|
||||
isHovered,
|
||||
reference,
|
||||
maxWidth,
|
||||
}: {
|
||||
activityTargetObjectRecords: ActivityTargetWithTargetRecord[];
|
||||
}) => {
|
||||
const dropdownId = useMemo(() => `multiple-relations-dropdown-${v4()}`, []);
|
||||
|
||||
maxWidth?: number;
|
||||
} & ExpandableListProps) => {
|
||||
return (
|
||||
<StyledContainer>
|
||||
{activityTargetObjectRecords
|
||||
?.slice(0, MAX_RECORD_CHIPS_DISPLAY)
|
||||
.map((activityTargetObjectRecord) => (
|
||||
<RecordChip
|
||||
key={activityTargetObjectRecord.targetObject.id}
|
||||
record={activityTargetObjectRecord.targetObject}
|
||||
objectNameSingular={
|
||||
activityTargetObjectRecord.targetObjectMetadataItem.nameSingular
|
||||
}
|
||||
/>
|
||||
))}
|
||||
|
||||
{activityTargetObjectRecords.length > MAX_RECORD_CHIPS_DISPLAY && (
|
||||
<div onClick={showMoreRelationsHandler}>
|
||||
<Dropdown
|
||||
dropdownId={dropdownId}
|
||||
dropdownHotkeyScope={{
|
||||
scope: dropdownId,
|
||||
}}
|
||||
clickableComponent={
|
||||
<Chip
|
||||
label={`+${
|
||||
activityTargetObjectRecords.length - MAX_RECORD_CHIPS_DISPLAY
|
||||
}`}
|
||||
variant={ChipVariant.Highlighted}
|
||||
/>
|
||||
}
|
||||
dropdownOffset={{ x: 0, y: -20 }}
|
||||
dropdownComponents={
|
||||
<StyledRelationsListContainer>
|
||||
{activityTargetObjectRecords.map(
|
||||
(activityTargetObjectRecord) => (
|
||||
<RecordChip
|
||||
key={activityTargetObjectRecord.targetObject.id}
|
||||
record={activityTargetObjectRecord.targetObject}
|
||||
objectNameSingular={
|
||||
activityTargetObjectRecord.targetObjectMetadataItem
|
||||
.nameSingular
|
||||
}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</StyledRelationsListContainer>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<StyledContainer maxWidth={maxWidth}>
|
||||
<ExpandableList
|
||||
isHovered={isHovered}
|
||||
reference={reference}
|
||||
forceDisplayHiddenCount
|
||||
>
|
||||
{activityTargetObjectRecords.map(
|
||||
(activityTargetObjectRecord, index) => (
|
||||
<RecordChip
|
||||
key={index}
|
||||
record={activityTargetObjectRecord.targetObject}
|
||||
objectNameSingular={
|
||||
activityTargetObjectRecord.targetObjectMetadataItem.nameSingular
|
||||
}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</ExpandableList>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@ -39,6 +39,7 @@ const StyledChip = styled.div`
|
||||
padding: ${({ theme }) => theme.spacing(1)};
|
||||
height: 20px;
|
||||
box-sizing: border-box;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
type ParticipantChipVariant = 'default' | 'bold';
|
||||
|
||||
@ -13,10 +13,16 @@ import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
|
||||
type ActivityTargetsInlineCellProps = {
|
||||
activity: Activity;
|
||||
showLabel?: boolean;
|
||||
maxWidth?: number;
|
||||
readonly?: boolean;
|
||||
};
|
||||
|
||||
export const ActivityTargetsInlineCell = ({
|
||||
activity,
|
||||
showLabel = true,
|
||||
maxWidth,
|
||||
readonly,
|
||||
}: ActivityTargetsInlineCellProps) => {
|
||||
const { activityTargetObjectRecords } =
|
||||
useActivityTargetObjectRecords(activity);
|
||||
@ -37,8 +43,9 @@ export const ActivityTargetsInlineCell = ({
|
||||
customEditHotkeyScope={{
|
||||
scope: ActivityEditorHotkeyScope.ActivityTargets,
|
||||
}}
|
||||
IconLabel={IconArrowUpRight}
|
||||
showLabel={true}
|
||||
IconLabel={showLabel ? IconArrowUpRight : undefined}
|
||||
showLabel={showLabel}
|
||||
readonly={readonly}
|
||||
editModeContent={
|
||||
<ActivityTargetInlineCellEditMode
|
||||
activity={activity}
|
||||
@ -49,6 +56,7 @@ export const ActivityTargetsInlineCell = ({
|
||||
displayModeContent={
|
||||
<ActivityTargetChips
|
||||
activityTargetObjectRecords={activityTargetObjectRecords}
|
||||
maxWidth={maxWidth}
|
||||
/>
|
||||
}
|
||||
isDisplayModeContentEmpty={activityTargetObjectRecords.length === 0}
|
||||
|
||||
@ -100,7 +100,7 @@ export const NoteCard = ({
|
||||
<StyledCardContent>{body}</StyledCardContent>
|
||||
</StyledCardDetailsContainer>
|
||||
<StyledFooter>
|
||||
<ActivityTargetsInlineCell activity={note} />
|
||||
<ActivityTargetsInlineCell activity={note} readonly />
|
||||
{note.comments && note.comments.length > 0 && (
|
||||
<StyledCommentIcon>
|
||||
<IconComment size={theme.icon.size.md} />
|
||||
|
||||
@ -6,9 +6,8 @@ import {
|
||||
OverflowingTextWithTooltip,
|
||||
} from 'twenty-ui';
|
||||
|
||||
import { ActivityTargetChips } from '@/activities/components/ActivityTargetChips';
|
||||
import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTargetObjectRecords';
|
||||
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
|
||||
import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { getActivitySummary } from '@/activities/utils/getActivitySummary';
|
||||
import { Checkbox, CheckboxShape } from '@/ui/input/components/Checkbox';
|
||||
@ -18,7 +17,7 @@ import { useCompleteTask } from '../hooks/useCompleteTask';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
@ -34,9 +33,6 @@ const StyledContainer = styled.div`
|
||||
const StyledTaskBody = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-grow: 1;
|
||||
width: 1px;
|
||||
`;
|
||||
|
||||
const StyledTaskTitle = styled.div<{
|
||||
@ -64,9 +60,10 @@ const StyledDueDate = styled.div<{
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const StyledFieldsContainer = styled.div`
|
||||
const StyledRightSideContainer = styled.div`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
@ -74,6 +71,14 @@ const StyledPlaceholder = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
`;
|
||||
|
||||
const StyledLeftSideContainer = styled.div`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const StyledCheckboxContainer = styled.div`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
export const TaskRow = ({ task }: { task: Activity }) => {
|
||||
const theme = useTheme();
|
||||
const openActivityRightDrawer = useOpenActivityRightDrawer();
|
||||
@ -81,39 +86,42 @@ export const TaskRow = ({ task }: { task: Activity }) => {
|
||||
const body = getActivitySummary(task.body);
|
||||
const { completeTask } = useCompleteTask(task);
|
||||
|
||||
const { activityTargetObjectRecords } = useActivityTargetObjectRecords(task);
|
||||
|
||||
return (
|
||||
<StyledContainer
|
||||
onClick={() => {
|
||||
openActivityRightDrawer(task.id);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={!!task.completedAt}
|
||||
shape={CheckboxShape.Rounded}
|
||||
onCheckedChange={completeTask}
|
||||
/>
|
||||
</div>
|
||||
<StyledTaskTitle completed={task.completedAt !== null}>
|
||||
{task.title || <StyledPlaceholder>Task title</StyledPlaceholder>}
|
||||
</StyledTaskTitle>
|
||||
<StyledTaskBody>
|
||||
<OverflowingTextWithTooltip text={body} />
|
||||
{task.comments && task.comments.length > 0 && (
|
||||
<StyledCommentIcon>
|
||||
<IconComment size={theme.icon.size.md} />
|
||||
</StyledCommentIcon>
|
||||
)}
|
||||
</StyledTaskBody>
|
||||
<StyledFieldsContainer>
|
||||
<ActivityTargetChips
|
||||
activityTargetObjectRecords={activityTargetObjectRecords}
|
||||
<StyledLeftSideContainer>
|
||||
<StyledCheckboxContainer
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={!!task.completedAt}
|
||||
shape={CheckboxShape.Rounded}
|
||||
onCheckedChange={completeTask}
|
||||
/>
|
||||
</StyledCheckboxContainer>
|
||||
<StyledTaskTitle completed={task.completedAt !== null}>
|
||||
{task.title || <StyledPlaceholder>Task title</StyledPlaceholder>}
|
||||
</StyledTaskTitle>
|
||||
<StyledTaskBody>
|
||||
<OverflowingTextWithTooltip text={body} />
|
||||
{task.comments && task.comments.length > 0 && (
|
||||
<StyledCommentIcon>
|
||||
<IconComment size={theme.icon.size.md} />
|
||||
</StyledCommentIcon>
|
||||
)}
|
||||
</StyledTaskBody>
|
||||
</StyledLeftSideContainer>
|
||||
<StyledRightSideContainer>
|
||||
<ActivityTargetsInlineCell
|
||||
activity={task}
|
||||
showLabel={false}
|
||||
maxWidth={200}
|
||||
readonly
|
||||
/>
|
||||
<StyledDueDate
|
||||
isPast={
|
||||
@ -123,7 +131,7 @@ export const TaskRow = ({ task }: { task: Activity }) => {
|
||||
<IconCalendar size={theme.icon.size.md} />
|
||||
{task.dueAt && beautifyExactDate(task.dueAt)}
|
||||
</StyledDueDate>
|
||||
</StyledFieldsContainer>
|
||||
</StyledRightSideContainer>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@ -70,7 +70,7 @@ export const EventsGroup = ({
|
||||
{group.items.map((event, index) => (
|
||||
<EventRow
|
||||
mainObjectMetadataItem={mainObjectMetadataItem}
|
||||
key={event.id}
|
||||
key={index}
|
||||
event={event}
|
||||
isLastEvent={index === group.items.length - 1}
|
||||
/>
|
||||
|
||||
@ -17,7 +17,7 @@ import { captchaProviderState } from '@/client-config/states/captchaProviderStat
|
||||
import { Loader } from '@/ui/feedback/loader/components/Loader';
|
||||
import { MainButton } from '@/ui/input/button/components/MainButton';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
import { ActionLink } from '@/ui/navigation/link/components/ActionLink.tsx';
|
||||
import { ActionLink } from '@/ui/navigation/link/components/ActionLink';
|
||||
import { AnimatedEaseIn } from '@/ui/utilities/animation/components/AnimatedEaseIn';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useEmailPasswordResetLinkMutation } from '~/generated/graphql.tsx';
|
||||
import { useEmailPasswordResetLinkMutation } from '~/generated/graphql';
|
||||
|
||||
export const useHandleResetPassword = () => {
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
|
||||
@ -6,7 +6,7 @@ import { CurrentWorkspace } from '@/auth/states/currentWorkspaceState';
|
||||
import { previousUrlState } from '@/auth/states/previousUrlState';
|
||||
import { billingState } from '@/client-config/states/billingState';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { WorkspaceMember } from '~/generated/graphql.tsx';
|
||||
import { WorkspaceMember } from '~/generated/graphql';
|
||||
|
||||
export const useNavigateAfterSignInUp = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { useAuth } from '@/auth/hooks/useAuth.ts';
|
||||
import { useAuth } from '@/auth/hooks/useAuth';
|
||||
|
||||
export const useSignInWithMicrosoft = () => {
|
||||
const workspaceInviteHash = useParams().workspaceInviteHash;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { useGetWorkspaceFromInviteHashQuery } from '~/generated/graphql.tsx';
|
||||
import { useGetWorkspaceFromInviteHashQuery } from '~/generated/graphql';
|
||||
|
||||
export const useWorkspaceFromInviteHash = () => {
|
||||
const workspaceInviteHash = useParams().workspaceInviteHash;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { SubscriptionCardPrice } from '@/billing/components/SubscriptionCardPrice.tsx';
|
||||
import { SubscriptionCardPrice } from '@/billing/components/SubscriptionCardPrice';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
type SubscriptionCardProps = {
|
||||
|
||||
@ -2,6 +2,7 @@ import { useContext } from 'react';
|
||||
|
||||
import { LinksFieldDisplay } from '@/object-record/record-field/meta-types/display/components/LinksFieldDisplay';
|
||||
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
|
||||
import { ExpandableListProps } from '@/ui/layout/expandable-list/components/ExpandableList';
|
||||
|
||||
import { FieldContext } from '../contexts/FieldContext';
|
||||
import { AddressFieldDisplay } from '../meta-types/display/components/AddressFieldDisplay';
|
||||
@ -13,7 +14,7 @@ import { EmailFieldDisplay } from '../meta-types/display/components/EmailFieldDi
|
||||
import { FullNameFieldDisplay } from '../meta-types/display/components/FullNameFieldDisplay';
|
||||
import { JsonFieldDisplay } from '../meta-types/display/components/JsonFieldDisplay';
|
||||
import { LinkFieldDisplay } from '../meta-types/display/components/LinkFieldDisplay';
|
||||
import { MultiSelectFieldDisplay } from '../meta-types/display/components/MultiSelectFieldDisplay.tsx';
|
||||
import { MultiSelectFieldDisplay } from '../meta-types/display/components/MultiSelectFieldDisplay';
|
||||
import { NumberFieldDisplay } from '../meta-types/display/components/NumberFieldDisplay';
|
||||
import { PhoneFieldDisplay } from '../meta-types/display/components/PhoneFieldDisplay';
|
||||
import { RelationFieldDisplay } from '../meta-types/display/components/RelationFieldDisplay';
|
||||
@ -36,7 +37,13 @@ import { isFieldSelect } from '../types/guards/isFieldSelect';
|
||||
import { isFieldText } from '../types/guards/isFieldText';
|
||||
import { isFieldUuid } from '../types/guards/isFieldUuid';
|
||||
|
||||
export const FieldDisplay = () => {
|
||||
type FieldDisplayProps = ExpandableListProps;
|
||||
|
||||
export const FieldDisplay = ({
|
||||
isHovered,
|
||||
reference,
|
||||
fromTableCell,
|
||||
}: FieldDisplayProps & { fromTableCell?: boolean }) => {
|
||||
const { fieldDefinition, isLabelIdentifier } = useContext(FieldContext);
|
||||
|
||||
const isChipDisplay =
|
||||
@ -74,7 +81,11 @@ export const FieldDisplay = () => {
|
||||
) : isFieldSelect(fieldDefinition) ? (
|
||||
<SelectFieldDisplay />
|
||||
) : isFieldMultiSelect(fieldDefinition) ? (
|
||||
<MultiSelectFieldDisplay />
|
||||
<MultiSelectFieldDisplay
|
||||
isHovered={isHovered}
|
||||
reference={reference}
|
||||
withDropDownBorder={fromTableCell}
|
||||
/>
|
||||
) : isFieldAddress(fieldDefinition) ? (
|
||||
<AddressFieldDisplay />
|
||||
) : isFieldRawJson(fieldDefinition) ? (
|
||||
|
||||
@ -4,7 +4,7 @@ import { AddressFieldInput } from '@/object-record/record-field/meta-types/input
|
||||
import { DateFieldInput } from '@/object-record/record-field/meta-types/input/components/DateFieldInput';
|
||||
import { FullNameFieldInput } from '@/object-record/record-field/meta-types/input/components/FullNameFieldInput';
|
||||
import { LinksFieldInput } from '@/object-record/record-field/meta-types/input/components/LinksFieldInput';
|
||||
import { MultiSelectFieldInput } from '@/object-record/record-field/meta-types/input/components/MultiSelectFieldInput.tsx';
|
||||
import { MultiSelectFieldInput } from '@/object-record/record-field/meta-types/input/components/MultiSelectFieldInput';
|
||||
import { RawJsonFieldInput } from '@/object-record/record-field/meta-types/input/components/RawJsonFieldInput';
|
||||
import { SelectFieldInput } from '@/object-record/record-field/meta-types/input/components/SelectFieldInput';
|
||||
import { RecordFieldInputScope } from '@/object-record/record-field/scopes/RecordFieldInputScope';
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { useContext } from 'react';
|
||||
import { IconComponent, IconPencil } from 'twenty-ui';
|
||||
|
||||
import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect';
|
||||
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
|
||||
@ -17,7 +18,8 @@ export const useGetButtonIcon = (): IconComponent | undefined => {
|
||||
if (
|
||||
isFieldLink(fieldDefinition) ||
|
||||
isFieldEmail(fieldDefinition) ||
|
||||
isFieldPhone(fieldDefinition)
|
||||
isFieldPhone(fieldDefinition) ||
|
||||
isFieldMultiSelect(fieldDefinition)
|
||||
) {
|
||||
return IconPencil;
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ import { isFieldFullNameValue } from '@/object-record/record-field/types/guards/
|
||||
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
|
||||
import { isFieldLinksValue } from '@/object-record/record-field/types/guards/isFieldLinksValue';
|
||||
import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect';
|
||||
import { isFieldMultiSelectValue } from '@/object-record/record-field/types/guards/isFieldMultiSelectValue.ts';
|
||||
import { isFieldMultiSelectValue } from '@/object-record/record-field/types/guards/isFieldMultiSelectValue';
|
||||
import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
|
||||
import { isFieldRawJsonValue } from '@/object-record/record-field/types/guards/isFieldRawJsonValue';
|
||||
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';
|
||||
|
||||
@ -1,13 +1,16 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { useMultiSelectField } from '@/object-record/record-field/meta-types/hooks/useMultiSelectField';
|
||||
import { Tag } from '@/ui/display/tag/components/Tag';
|
||||
import {
|
||||
ExpandableList,
|
||||
ExpandableListProps,
|
||||
} from '@/ui/layout/expandable-list/components/ExpandableList';
|
||||
|
||||
const StyledTagContainer = styled.div`
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
export const MultiSelectFieldDisplay = () => {
|
||||
type MultiSelectFieldDisplayProps = ExpandableListProps;
|
||||
export const MultiSelectFieldDisplay = ({
|
||||
isHovered,
|
||||
reference,
|
||||
withDropDownBorder,
|
||||
}: MultiSelectFieldDisplayProps) => {
|
||||
const { fieldValues, fieldDefinition } = useMultiSelectField();
|
||||
|
||||
const selectedOptions = fieldValues
|
||||
@ -17,7 +20,11 @@ export const MultiSelectFieldDisplay = () => {
|
||||
: [];
|
||||
|
||||
return selectedOptions ? (
|
||||
<StyledTagContainer>
|
||||
<ExpandableList
|
||||
isHovered={isHovered}
|
||||
reference={reference}
|
||||
withDropDownBorder={withDropDownBorder}
|
||||
>
|
||||
{selectedOptions.map((selectedOption, index) => (
|
||||
<Tag
|
||||
key={index}
|
||||
@ -25,7 +32,7 @@ export const MultiSelectFieldDisplay = () => {
|
||||
text={selectedOption.label}
|
||||
/>
|
||||
))}
|
||||
</StyledTagContainer>
|
||||
</ExpandableList>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
|
||||
@ -9,7 +9,7 @@ import { assertFieldMetadata } from '@/object-record/record-field/types/guards/a
|
||||
import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect';
|
||||
import { isFieldMultiSelectValue } from '@/object-record/record-field/types/guards/isFieldMultiSelectValue';
|
||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||
import { FieldMetadataType } from '~/generated/graphql.tsx';
|
||||
import { FieldMetadataType } from '~/generated/graphql';
|
||||
|
||||
export const useMultiSelectField = () => {
|
||||
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
|
||||
|
||||
@ -7,7 +7,7 @@ import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { MenuItemMultiSelectTag } from '@/ui/navigation/menu-item/components/MenuItemMultiSelectTag.tsx';
|
||||
import { MenuItemMultiSelectTag } from '@/ui/navigation/menu-item/components/MenuItemMultiSelectTag';
|
||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import { useContext, useState } from 'react';
|
||||
import React, { useContext, useEffect, useRef, useState } from 'react';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import { css, useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { motion } from 'framer-motion';
|
||||
import { IconComponent } from 'twenty-ui';
|
||||
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { EllipsisDisplay } from '@/ui/field/display/components/EllipsisDisplay';
|
||||
import { ExpandableListProps } from '@/ui/layout/expandable-list/components/ExpandableList';
|
||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||
|
||||
import { useInlineCell } from '../hooks/useInlineCell';
|
||||
@ -48,11 +48,6 @@ const StyledLabelContainer = styled.div<{ width?: number }>`
|
||||
width: ${({ width }) => width}px;
|
||||
`;
|
||||
|
||||
const StyledEditButtonContainer = styled(motion.div)`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const StyledClickableContainer = styled.div<{ readonly?: boolean }>`
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
@ -119,18 +114,24 @@ export const RecordInlineCellContainer = ({
|
||||
disableHoverEffect,
|
||||
}: RecordInlineCellContainerProps) => {
|
||||
const { entityId, fieldDefinition } = useContext(FieldContext);
|
||||
const reference = useRef<HTMLDivElement>(null);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const [isHoveredForDisplayMode, setIsHoveredForDisplayMode] = useState(false);
|
||||
const [newDisplayModeContent, setNewDisplayModeContent] =
|
||||
useState<React.ReactNode>(displayModeContent);
|
||||
|
||||
const handleContainerMouseEnter = () => {
|
||||
if (!readonly) {
|
||||
setIsHovered(true);
|
||||
}
|
||||
setIsHoveredForDisplayMode(true);
|
||||
};
|
||||
|
||||
const handleContainerMouseLeave = () => {
|
||||
if (!readonly) {
|
||||
setIsHovered(false);
|
||||
}
|
||||
setIsHoveredForDisplayMode(false);
|
||||
};
|
||||
|
||||
const { isInlineCellInEditMode, openInlineCell } = useInlineCell();
|
||||
@ -151,6 +152,17 @@ export const RecordInlineCellContainer = ({
|
||||
const theme = useTheme();
|
||||
const labelId = `label-${entityId}-${fieldDefinition?.metadata?.fieldName}`;
|
||||
|
||||
useEffect(() => {
|
||||
if (React.isValidElement<ExpandableListProps>(displayModeContent)) {
|
||||
setNewDisplayModeContent(
|
||||
React.cloneElement(displayModeContent, {
|
||||
isHovered: isHoveredForDisplayMode,
|
||||
reference: reference.current || undefined,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}, [isHoveredForDisplayMode, displayModeContent, reference]);
|
||||
|
||||
return (
|
||||
<StyledInlineCellBaseContainer
|
||||
onMouseEnter={handleContainerMouseEnter}
|
||||
@ -181,7 +193,7 @@ export const RecordInlineCellContainer = ({
|
||||
)}
|
||||
</StyledLabelAndIconContainer>
|
||||
)}
|
||||
<StyledValueContainer>
|
||||
<StyledValueContainer ref={reference}>
|
||||
{!readonly && isInlineCellInEditMode ? (
|
||||
<RecordInlineCellEditMode>{editModeContent}</RecordInlineCellEditMode>
|
||||
) : editModeContentOnly ? (
|
||||
@ -208,18 +220,9 @@ export const RecordInlineCellContainer = ({
|
||||
isHovered={isHovered}
|
||||
emptyPlaceholder={showLabel ? 'Empty' : label}
|
||||
>
|
||||
{displayModeContent}
|
||||
{newDisplayModeContent}
|
||||
</RecordInlineCellDisplayMode>
|
||||
{showEditButton && (
|
||||
<StyledEditButtonContainer
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.1 }}
|
||||
whileHover={{ scale: 1.04 }}
|
||||
>
|
||||
<RecordInlineCellButton Icon={buttonIcon} />
|
||||
</StyledEditButtonContainer>
|
||||
)}
|
||||
{showEditButton && <RecordInlineCellButton Icon={buttonIcon} />}
|
||||
</StyledClickableContainer>
|
||||
)}
|
||||
</StyledValueContainer>
|
||||
|
||||
@ -1,13 +1,23 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { IconComponent } from 'twenty-ui';
|
||||
|
||||
import { AnimatedContainer } from '@/object-record/record-table/components/AnimatedContainer';
|
||||
import { FloatingIconButton } from '@/ui/input/button/components/FloatingIconButton';
|
||||
|
||||
const StyledInlineCellButtonContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
`;
|
||||
export const RecordInlineCellButton = ({ Icon }: { Icon: IconComponent }) => {
|
||||
return (
|
||||
<FloatingIconButton
|
||||
size="small"
|
||||
Icon={Icon}
|
||||
data-testid="inline-cell-edit-mode-container"
|
||||
/>
|
||||
<AnimatedContainer>
|
||||
<StyledInlineCellButtonContainer>
|
||||
<FloatingIconButton
|
||||
size="small"
|
||||
Icon={Icon}
|
||||
data-testid="inline-cell-edit-mode-container"
|
||||
/>
|
||||
</StyledInlineCellButtonContainer>
|
||||
</AnimatedContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
const StyledAnimatedChipContainer = styled(motion.div)``;
|
||||
|
||||
export const AnimatedContainer = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) => (
|
||||
<StyledAnimatedChipContainer
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.1 }}
|
||||
whileHover={{ scale: 1.04 }}
|
||||
>
|
||||
{children}
|
||||
</StyledAnimatedChipContainer>
|
||||
);
|
||||
@ -102,7 +102,7 @@ export const RecordTableCell = ({
|
||||
isReadOnly={isReadOnly}
|
||||
/>
|
||||
}
|
||||
nonEditModeContent={<FieldDisplay />}
|
||||
nonEditModeContent={<FieldDisplay fromTableCell />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { motion } from 'framer-motion';
|
||||
import { IconComponent } from 'twenty-ui';
|
||||
|
||||
import { AnimatedContainer } from '@/object-record/record-table/components/AnimatedContainer';
|
||||
import { FloatingIconButton } from '@/ui/input/button/components/FloatingIconButton';
|
||||
|
||||
const StyledEditButtonContainer = styled(motion.div)`
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
const StyledButtonContainer = styled.div`
|
||||
margin: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
type RecordTableCellButtonProps = {
|
||||
@ -18,12 +17,9 @@ export const RecordTableCellButton = ({
|
||||
onClick,
|
||||
Icon,
|
||||
}: RecordTableCellButtonProps) => (
|
||||
<StyledEditButtonContainer
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.1 }}
|
||||
whileHover={{ scale: 1.04 }}
|
||||
>
|
||||
<FloatingIconButton size="small" onClick={onClick} Icon={Icon} />
|
||||
</StyledEditButtonContainer>
|
||||
<AnimatedContainer>
|
||||
<StyledButtonContainer>
|
||||
<FloatingIconButton size="small" onClick={onClick} Icon={Icon} />
|
||||
</StyledButtonContainer>
|
||||
</AnimatedContainer>
|
||||
);
|
||||
|
||||
@ -1,4 +1,10 @@
|
||||
import React, { ReactElement, useContext, useState } from 'react';
|
||||
import React, {
|
||||
ReactElement,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { IconArrowUpRight } from 'twenty-ui';
|
||||
@ -14,6 +20,7 @@ import { useOpenRecordTableCellFromCell } from '@/object-record/record-table/rec
|
||||
import { RecordTableScopeInternalContext } from '@/object-record/record-table/scopes/scope-internal-context/RecordTableScopeInternalContext';
|
||||
import { isSoftFocusOnTableCellComponentFamilyState } from '@/object-record/record-table/states/isSoftFocusOnTableCellComponentFamilyState';
|
||||
import { isTableCellInEditModeComponentFamilyState } from '@/object-record/record-table/states/isTableCellInEditModeComponentFamilyState';
|
||||
import { ExpandableListProps } from '@/ui/layout/expandable-list/components/ExpandableList';
|
||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
|
||||
import { getScopeIdOrUndefinedFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdOrUndefinedFromComponentId';
|
||||
@ -33,7 +40,7 @@ const StyledTd = styled.td<{ isSelected: boolean; isInEditMode: boolean }>`
|
||||
z-index: ${({ isInEditMode }) => (isInEditMode ? '4 !important' : '3')};
|
||||
`;
|
||||
|
||||
const StyledCellBaseContainer = styled.div`
|
||||
const StyledCellBaseContainer = styled.div<{ softFocus: boolean }>`
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
@ -41,6 +48,12 @@ const StyledCellBaseContainer = styled.div`
|
||||
height: 32px;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
${(props) =>
|
||||
props.softFocus
|
||||
? `background: ${props.theme.background.transparent.secondary};
|
||||
border-radius: ${props.theme.border.radius.sm};
|
||||
outline: 1px solid ${props.theme.font.color.extraLight};`
|
||||
: ''}
|
||||
`;
|
||||
|
||||
export type RecordTableCellContainerProps = {
|
||||
@ -63,6 +76,10 @@ export const RecordTableCellContainer = ({
|
||||
editHotkeyScope,
|
||||
}: RecordTableCellContainerProps) => {
|
||||
const { columnIndex } = useContext(RecordTableCellContext);
|
||||
const reference = useRef<HTMLTableCellElement>(null);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const [newNonEditModeContent, setNewNonEditModeContent] =
|
||||
useState<ReactElement>(nonEditModeContent);
|
||||
const { isReadOnly, isSelected, recordId } = useContext(
|
||||
RecordTableRowContext,
|
||||
);
|
||||
@ -71,8 +88,6 @@ export const RecordTableCellContainer = ({
|
||||
|
||||
const cellPosition = useCurrentTableCellPosition();
|
||||
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
const { openTableCell } = useOpenRecordTableCellFromCell();
|
||||
|
||||
const tableScopeId = useAvailableScopeIdOrThrow(
|
||||
@ -135,8 +150,20 @@ export const RecordTableCellContainer = ({
|
||||
(!isFirstColumn || !isEmpty) &&
|
||||
!isReadOnly;
|
||||
|
||||
useEffect(() => {
|
||||
if (React.isValidElement<ExpandableListProps>(nonEditModeContent)) {
|
||||
setNewNonEditModeContent(
|
||||
React.cloneElement(nonEditModeContent, {
|
||||
isHovered: showButton,
|
||||
reference: reference.current || undefined,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}, [nonEditModeContent, showButton, reference]);
|
||||
|
||||
return (
|
||||
<StyledTd
|
||||
ref={reference}
|
||||
isSelected={isSelected}
|
||||
onContextMenu={handleContextMenu}
|
||||
isInEditMode={isCurrentTableCellInEditMode}
|
||||
@ -147,34 +174,37 @@ export const RecordTableCellContainer = ({
|
||||
<StyledCellBaseContainer
|
||||
onMouseEnter={handleContainerMouseEnter}
|
||||
onMouseLeave={handleContainerMouseLeave}
|
||||
softFocus={hasSoftFocus}
|
||||
>
|
||||
{isCurrentTableCellInEditMode ? (
|
||||
<RecordTableCellEditMode>{editModeContent}</RecordTableCellEditMode>
|
||||
) : hasSoftFocus ? (
|
||||
<>
|
||||
<RecordTableCellSoftFocusMode>
|
||||
{editModeContentOnly ? editModeContent : newNonEditModeContent}
|
||||
</RecordTableCellSoftFocusMode>
|
||||
{showButton && (
|
||||
<RecordTableCellButton
|
||||
onClick={handleButtonClick}
|
||||
Icon={buttonIcon}
|
||||
/>
|
||||
)}
|
||||
<RecordTableCellSoftFocusMode>
|
||||
{editModeContentOnly ? editModeContent : nonEditModeContent}
|
||||
</RecordTableCellSoftFocusMode>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{!isEmpty && (
|
||||
<RecordTableCellDisplayMode>
|
||||
{editModeContentOnly
|
||||
? editModeContent
|
||||
: newNonEditModeContent}
|
||||
</RecordTableCellDisplayMode>
|
||||
)}
|
||||
{showButton && (
|
||||
<RecordTableCellButton
|
||||
onClick={handleButtonClick}
|
||||
Icon={buttonIcon}
|
||||
/>
|
||||
)}
|
||||
{!isEmpty && (
|
||||
<RecordTableCellDisplayMode>
|
||||
{editModeContentOnly ? editModeContent : nonEditModeContent}
|
||||
</RecordTableCellDisplayMode>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</StyledCellBaseContainer>
|
||||
|
||||
@ -1,29 +0,0 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { motion } from 'framer-motion';
|
||||
import { IconComponent } from 'twenty-ui';
|
||||
|
||||
import { FloatingIconButton } from '@/ui/input/button/components/FloatingIconButton';
|
||||
|
||||
const StyledEditButtonContainer = styled(motion.div)`
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
`;
|
||||
|
||||
type RecordTableCellEditButtonProps = {
|
||||
onClick?: () => void;
|
||||
Icon: IconComponent;
|
||||
};
|
||||
|
||||
export const RecordTableCellEditButton = ({
|
||||
onClick,
|
||||
Icon,
|
||||
}: RecordTableCellEditButtonProps) => (
|
||||
<StyledEditButtonContainer
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.1 }}
|
||||
whileHover={{ scale: 1.04 }}
|
||||
>
|
||||
<FloatingIconButton size="small" onClick={onClick} Icon={Icon} />
|
||||
</StyledEditButtonContainer>
|
||||
);
|
||||
@ -101,7 +101,6 @@ export const RecordTableCellSoftFocusMode = ({
|
||||
return (
|
||||
<RecordTableCellDisplayContainer
|
||||
onClick={handleClick}
|
||||
softFocus
|
||||
scrollRef={scrollRef}
|
||||
>
|
||||
{children}
|
||||
|
||||
@ -1,104 +0,0 @@
|
||||
import React, { ReactElement, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import styled from '@emotion/styled';
|
||||
import { Chip, ChipVariant } from 'twenty-ui';
|
||||
|
||||
import { IntersectionObserverWrapper } from '@/ui/display/expandable-list/IntersectionObserverWrapper';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
box-sizing: border-box;
|
||||
white-space: nowrap;
|
||||
overflow-x: hidden;
|
||||
`;
|
||||
|
||||
const StyledExpendableCell = styled.div`
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
backdrop-filter: ${({ theme }) => theme.blur.strong};
|
||||
background: ${({ theme }) => theme.background.secondary};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
box-shadow: ${({ theme }) => theme.boxShadow.light};
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
export const ExpandableList = ({
|
||||
listItems,
|
||||
rootRef,
|
||||
id,
|
||||
}: {
|
||||
listItems: ReactElement[];
|
||||
rootRef: React.RefObject<HTMLElement>;
|
||||
id: string;
|
||||
}) => {
|
||||
const [listItemsInView, setListItemsInView] = useState(new Set<number>());
|
||||
|
||||
const firstListItem = listItems[0];
|
||||
|
||||
const dropdownId = `expandable-list-dropdown-${id}`;
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const divRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<StyledContainer ref={containerRef}>
|
||||
{firstListItem}
|
||||
{listItems.slice(1).map((listItem, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<IntersectionObserverWrapper
|
||||
set={setListItemsInView}
|
||||
id={index}
|
||||
rootRef={rootRef}
|
||||
>
|
||||
{listItem}
|
||||
</IntersectionObserverWrapper>
|
||||
{index === listItemsInView.size - 1 &&
|
||||
listItems.length - listItemsInView.size - 1 !== 0 && (
|
||||
<Dropdown
|
||||
dropdownId={dropdownId}
|
||||
dropdownHotkeyScope={{
|
||||
scope: dropdownId,
|
||||
}}
|
||||
clickableComponent={
|
||||
<Chip
|
||||
label={`+${listItems.length - listItemsInView.size - 1}`}
|
||||
variant={ChipVariant.Highlighted}
|
||||
/>
|
||||
}
|
||||
dropdownComponents={
|
||||
<>
|
||||
{divRef.current &&
|
||||
createPortal(
|
||||
<StyledExpendableCell>
|
||||
{listItems}
|
||||
</StyledExpendableCell>,
|
||||
divRef.current as HTMLElement,
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
<div
|
||||
ref={divRef}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '100%',
|
||||
zIndex: 1,
|
||||
boxSizing: 'border-box',
|
||||
}}
|
||||
></div>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
@ -1,47 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const StyledDiv = styled.div<{ inView?: boolean }>`
|
||||
opacity: ${({ inView }) => (inView === undefined || inView ? 1 : 0)};
|
||||
`;
|
||||
|
||||
export const IntersectionObserverWrapper = ({
|
||||
set,
|
||||
id,
|
||||
rootRef,
|
||||
children,
|
||||
}: {
|
||||
set: React.Dispatch<React.SetStateAction<Set<number>>>;
|
||||
id: number;
|
||||
rootRef?: React.RefObject<HTMLElement>;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const { ref, inView } = useInView({
|
||||
threshold: 1,
|
||||
onChange: (inView) => {
|
||||
if (inView) {
|
||||
set((prev: Set<number>) => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.add(id);
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
if (!inView) {
|
||||
set((prev: Set<number>) => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(id);
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
},
|
||||
root: rootRef?.current,
|
||||
rootMargin: '0px 0px -50px 0px',
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledDiv ref={ref} inView={inView}>
|
||||
{children}
|
||||
</StyledDiv>
|
||||
);
|
||||
};
|
||||
@ -3,7 +3,7 @@ import { css, useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { IconInfoCircle } from 'twenty-ui';
|
||||
|
||||
import { Button } from '@/ui/input/button/components/Button.tsx';
|
||||
import { Button } from '@/ui/input/button/components/Button';
|
||||
|
||||
export type InfoAccent = 'blue' | 'danger';
|
||||
export type InfoProps = {
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { Radio } from '@/ui/input/components/Radio.tsx';
|
||||
import { Radio } from '@/ui/input/components/Radio';
|
||||
|
||||
const StyledSubscriptionCardContainer = styled.button`
|
||||
background-color: ${({ theme }) => theme.background.secondary};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
|
||||
@ -0,0 +1,57 @@
|
||||
import { Dispatch, ReactElement, SetStateAction } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { ChildrenProperty } from '@/ui/layout/expandable-list/components/ExpandableList';
|
||||
|
||||
const StyledChildContainer = styled.div<{
|
||||
shrink?: number;
|
||||
isVisible?: boolean;
|
||||
displayHiddenCount?: boolean;
|
||||
}>`
|
||||
display: ${({ isVisible = true }) => (isVisible ? 'flex' : 'none')};
|
||||
flex-shrink: ${({ shrink = 1 }) => shrink};
|
||||
overflow: ${({ displayHiddenCount }) =>
|
||||
displayHiddenCount ? 'hidden' : 'none'};
|
||||
`;
|
||||
|
||||
const StyledChildrenContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
overflow: hidden;
|
||||
`;
|
||||
export const ChildrenContainer = ({
|
||||
children,
|
||||
childrenProperties,
|
||||
setChildrenWidths,
|
||||
isFocusedMode,
|
||||
}: {
|
||||
children: ReactElement[];
|
||||
childrenProperties: Record<number, ChildrenProperty>;
|
||||
setChildrenWidths: Dispatch<SetStateAction<Record<number, number>>>;
|
||||
isFocusedMode: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<StyledChildrenContainer>
|
||||
{children.map((child, index) => {
|
||||
return (
|
||||
<StyledChildContainer
|
||||
ref={(el) => {
|
||||
if (!el || isFocusedMode) return;
|
||||
setChildrenWidths((prevState) => {
|
||||
prevState[index] = el.getBoundingClientRect().width;
|
||||
return prevState;
|
||||
});
|
||||
}}
|
||||
key={index}
|
||||
displayHiddenCount={isFocusedMode}
|
||||
isVisible={childrenProperties[index]?.isVisible}
|
||||
shrink={childrenProperties[index]?.shrink}
|
||||
>
|
||||
{child}
|
||||
</StyledChildContainer>
|
||||
);
|
||||
})}
|
||||
</StyledChildrenContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,147 @@
|
||||
import { ReactElement, useEffect, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { offset, useFloating } from '@floating-ui/react';
|
||||
import { Chip, ChipVariant } from 'twenty-ui';
|
||||
|
||||
import { AnimatedContainer } from '@/object-record/record-table/components/AnimatedContainer';
|
||||
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||
import { ChildrenContainer } from '@/ui/layout/expandable-list/components/ChildrenContainer';
|
||||
import { getChildrenProperties } from '@/ui/layout/expandable-list/utils/getChildProperties';
|
||||
import { getChipContentWidth } from '@/ui/layout/expandable-list/utils/getChipContentWidth';
|
||||
|
||||
export const GAP_WIDTH = 4;
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledRelationsListContainer = styled.div<{
|
||||
withDropDownBorder?: boolean;
|
||||
}>`
|
||||
backdrop-filter: ${({ theme }) => theme.blur.strong};
|
||||
background-color: ${({ theme }) => theme.background.secondary};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
box-shadow: '0px 2px 4px ${({ theme }) =>
|
||||
theme.boxShadow.light}, 2px 4px 16px ${({ theme }) =>
|
||||
theme.boxShadow.strong}';
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
outline: ${(props) =>
|
||||
props.withDropDownBorder
|
||||
? `1px solid ${props.theme.font.color.extraLight}`
|
||||
: 'none'};
|
||||
`;
|
||||
|
||||
export type ExpandableListProps = {
|
||||
isHovered?: boolean;
|
||||
reference?: HTMLDivElement;
|
||||
forceDisplayHiddenCount?: boolean;
|
||||
withDropDownBorder?: boolean;
|
||||
};
|
||||
|
||||
export type ChildrenProperty = {
|
||||
shrink: number;
|
||||
isVisible: boolean;
|
||||
};
|
||||
|
||||
export const ExpandableList = ({
|
||||
children,
|
||||
isHovered,
|
||||
reference,
|
||||
forceDisplayHiddenCount = false,
|
||||
withDropDownBorder = false,
|
||||
}: {
|
||||
children: ReactElement[];
|
||||
} & ExpandableListProps) => {
|
||||
const [containerWidth, setContainerWidth] = useState(0);
|
||||
const [isDropdownMenuOpen, setIsDropdownMenuOpen] = useState(false);
|
||||
const [childrenWidths, setChildrenWidths] = useState<Record<number, number>>(
|
||||
{},
|
||||
);
|
||||
|
||||
// Because Chip width depends on the number of hidden children which depends on the Chip width, we have a circular dependency
|
||||
// To avoid it, we set the Chip width and make sure it can display its content (a number greater than 1)
|
||||
const chipContentWidth = getChipContentWidth(children.length);
|
||||
const chipContainerWidth = chipContentWidth + 2 * GAP_WIDTH; // Because Chip component has 4px padding-left and right
|
||||
const availableWidth = containerWidth - (chipContainerWidth + GAP_WIDTH); // Because there is a 4px gap between ChildrenContainer and ChipContainer
|
||||
const isFocusedMode =
|
||||
(isHovered || forceDisplayHiddenCount) &&
|
||||
Object.values(childrenWidths).length > 0;
|
||||
|
||||
const childrenProperties = getChildrenProperties(
|
||||
isFocusedMode,
|
||||
availableWidth,
|
||||
childrenWidths,
|
||||
);
|
||||
|
||||
const hiddenChildrenCount = Object.values(childrenProperties).filter(
|
||||
(childProperties) => !childProperties.isVisible,
|
||||
).length;
|
||||
|
||||
const displayHiddenCountChip = isFocusedMode && hiddenChildrenCount > 0;
|
||||
|
||||
const { refs, floatingStyles } = useFloating({
|
||||
// @ts-expect-error placement accepts 'start' as value even if the typing does not permit it
|
||||
placement: 'start',
|
||||
middleware: [offset({ mainAxis: -1, crossAxis: -1 })],
|
||||
elements: { reference },
|
||||
});
|
||||
|
||||
const openDropdownMenu = (event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
setIsDropdownMenuOpen(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isHovered) {
|
||||
setIsDropdownMenuOpen(false);
|
||||
}
|
||||
}, [isHovered]);
|
||||
|
||||
return (
|
||||
<StyledContainer
|
||||
ref={(el) => {
|
||||
if (!el) return;
|
||||
setContainerWidth(el.getBoundingClientRect().width);
|
||||
}}
|
||||
>
|
||||
<ChildrenContainer
|
||||
childrenProperties={childrenProperties}
|
||||
setChildrenWidths={setChildrenWidths}
|
||||
isFocusedMode={isFocusedMode}
|
||||
>
|
||||
{children}
|
||||
</ChildrenContainer>
|
||||
{displayHiddenCountChip && (
|
||||
<AnimatedContainer>
|
||||
<Chip
|
||||
label={`+${hiddenChildrenCount}`}
|
||||
variant={ChipVariant.Highlighted}
|
||||
onClick={openDropdownMenu}
|
||||
/>
|
||||
</AnimatedContainer>
|
||||
)}
|
||||
{isDropdownMenuOpen && (
|
||||
<DropdownMenu
|
||||
ref={refs.setFloating}
|
||||
style={floatingStyles}
|
||||
width={
|
||||
reference
|
||||
? Math.max(220, reference.getBoundingClientRect().width)
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<StyledRelationsListContainer withDropDownBorder={withDropDownBorder}>
|
||||
{children}
|
||||
</StyledRelationsListContainer>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,74 @@
|
||||
import { ReactElement, useRef, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { ComponentDecorator } from 'packages/twenty-ui';
|
||||
|
||||
import { Tag } from '@/ui/display/tag/components/Tag';
|
||||
import {
|
||||
ExpandableList,
|
||||
ExpandableListProps,
|
||||
} from '@/ui/layout/expandable-list/components/ExpandableList';
|
||||
import { MAIN_COLOR_NAMES } from '@/ui/theme/constants/MainColorNames';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
padding: ${({ theme }) => theme.spacing(1)};
|
||||
width: 300px;
|
||||
`;
|
||||
|
||||
type RenderProps = ExpandableListProps & {
|
||||
children: ReactElement[];
|
||||
};
|
||||
|
||||
const Render = (args: RenderProps) => {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const reference = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<StyledContainer
|
||||
ref={reference}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
<ExpandableList
|
||||
reference={reference.current || undefined}
|
||||
forceDisplayHiddenCount={args.forceDisplayHiddenCount}
|
||||
withDropDownBorder={args.withDropDownBorder}
|
||||
isHovered={isHovered}
|
||||
>
|
||||
{args.children}
|
||||
</ExpandableList>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const meta: Meta<typeof ExpandableList> = {
|
||||
title: 'UI/Layout/ExpandableList/ExpandableList',
|
||||
component: ExpandableList,
|
||||
decorators: [ComponentDecorator],
|
||||
args: {
|
||||
children: [
|
||||
<Tag key={1} text={'Option 1'} color={MAIN_COLOR_NAMES[0]} />,
|
||||
<Tag key={2} text={'Option 2'} color={MAIN_COLOR_NAMES[1]} />,
|
||||
<Tag key={3} text={'Option 3'} color={MAIN_COLOR_NAMES[2]} />,
|
||||
<Tag key={4} text={'Option 4'} color={MAIN_COLOR_NAMES[3]} />,
|
||||
<Tag key={5} text={'Option 5'} color={MAIN_COLOR_NAMES[4]} />,
|
||||
<Tag key={6} text={'Option 6'} color={MAIN_COLOR_NAMES[5]} />,
|
||||
<Tag key={7} text={'Option 7'} color={MAIN_COLOR_NAMES[6]} />,
|
||||
],
|
||||
isHovered: undefined,
|
||||
reference: undefined,
|
||||
forceDisplayHiddenCount: false,
|
||||
withDropDownBorder: false,
|
||||
},
|
||||
argTypes: {
|
||||
children: { control: false },
|
||||
isHovered: { control: false },
|
||||
reference: { control: false },
|
||||
},
|
||||
render: Render,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof ExpandableList>;
|
||||
|
||||
export const Default: Story = {};
|
||||
@ -0,0 +1,41 @@
|
||||
import { getChildrenProperties } from '@/ui/layout/expandable-list/utils/getChildProperties';
|
||||
|
||||
describe('getChildrenProperties', () => {
|
||||
it('should return default value when isFocused is False', () => {
|
||||
const isFocused = false;
|
||||
const availableWidth = 100;
|
||||
expect(getChildrenProperties(isFocused, availableWidth, {})).toEqual({});
|
||||
|
||||
expect(
|
||||
getChildrenProperties(isFocused, availableWidth, { 0: 40, 1: 40 }),
|
||||
).toEqual({});
|
||||
});
|
||||
|
||||
it('should return proper value when isFocused is True', () => {
|
||||
const isFocused = true;
|
||||
const availableWidth = 100;
|
||||
expect(getChildrenProperties(isFocused, availableWidth, {})).toEqual({});
|
||||
|
||||
expect(
|
||||
getChildrenProperties(isFocused, availableWidth, { 0: 40, 1: 40 }),
|
||||
).toEqual({
|
||||
0: { shrink: 0, isVisible: true },
|
||||
1: { shrink: 0, isVisible: true },
|
||||
});
|
||||
expect(
|
||||
getChildrenProperties(isFocused, availableWidth, {
|
||||
0: 40,
|
||||
1: 40,
|
||||
2: 40,
|
||||
3: 40,
|
||||
4: 40,
|
||||
}),
|
||||
).toEqual({
|
||||
0: { shrink: 0, isVisible: true },
|
||||
1: { shrink: 0, isVisible: true },
|
||||
2: { shrink: 1, isVisible: true },
|
||||
3: { shrink: 1, isVisible: false },
|
||||
4: { shrink: 1, isVisible: false },
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,13 @@
|
||||
import { getChipContentWidth } from '@/ui/layout/expandable-list/utils/getChipContentWidth';
|
||||
|
||||
describe('getChipContentWidth', () => {
|
||||
it('should return proper value', () => {
|
||||
expect(getChipContentWidth(0)).toEqual(0);
|
||||
expect(getChipContentWidth(1)).toEqual(0);
|
||||
expect(getChipContentWidth(2)).toEqual(17);
|
||||
expect(getChipContentWidth(20)).toEqual(25);
|
||||
expect(getChipContentWidth(200)).toEqual(33);
|
||||
expect(getChipContentWidth(2000)).toEqual(41);
|
||||
expect(getChipContentWidth(20000)).toEqual(49);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,30 @@
|
||||
import {
|
||||
ChildrenProperty,
|
||||
GAP_WIDTH,
|
||||
} from '@/ui/layout/expandable-list/components/ExpandableList';
|
||||
|
||||
export const getChildrenProperties = (
|
||||
isFocusedMode: boolean,
|
||||
availableWidth: number,
|
||||
childrenWidths: Record<number, number>,
|
||||
) => {
|
||||
if (!isFocusedMode) {
|
||||
return {};
|
||||
}
|
||||
let cumulatedChildrenWidth = 0;
|
||||
const result: Record<number, ChildrenProperty> = {};
|
||||
Object.values(childrenWidths).forEach((width, index) => {
|
||||
// Because there is a 4px gap between children
|
||||
const childWidth = width + GAP_WIDTH;
|
||||
let shrink = 1;
|
||||
let isVisible = true;
|
||||
if (cumulatedChildrenWidth > availableWidth) {
|
||||
isVisible = false;
|
||||
} else if (cumulatedChildrenWidth + childWidth <= availableWidth) {
|
||||
shrink = 0;
|
||||
}
|
||||
result[index] = { shrink, isVisible };
|
||||
cumulatedChildrenWidth += childWidth;
|
||||
});
|
||||
return result;
|
||||
};
|
||||
@ -0,0 +1,6 @@
|
||||
export const getChipContentWidth = (numberOfChildren: number) => {
|
||||
if (numberOfChildren <= 1) {
|
||||
return 0;
|
||||
}
|
||||
return 17 + 8 * Math.trunc(Math.log10(numberOfChildren));
|
||||
};
|
||||
@ -6,6 +6,7 @@ import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { Key } from 'ts-key-enum';
|
||||
|
||||
import { RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID } from '@/ui/layout/right-drawer/constants/RightDrawerClickOutsideListener';
|
||||
import { isRightDrawerAnimationCompletedState } from '@/ui/layout/right-drawer/states/isRightDrawerAnimationCompleted';
|
||||
import { rightDrawerCloseEventState } from '@/ui/layout/right-drawer/states/rightDrawerCloseEventsState';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
|
||||
@ -46,6 +47,9 @@ export const RightDrawer = () => {
|
||||
);
|
||||
|
||||
const isRightDrawerExpanded = useRecoilValue(isRightDrawerExpandedState);
|
||||
const [, setIsRightDrawerAnimationCompleted] = useRecoilState(
|
||||
isRightDrawerAnimationCompletedState,
|
||||
);
|
||||
|
||||
const rightDrawerPage = useRecoilValue(rightDrawerPageState);
|
||||
|
||||
@ -112,6 +116,9 @@ export const RightDrawer = () => {
|
||||
x: '100%',
|
||||
},
|
||||
};
|
||||
const handleAnimationComplete = () => {
|
||||
setIsRightDrawerAnimationCompleted(isRightDrawerOpen);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledContainer
|
||||
@ -121,6 +128,7 @@ export const RightDrawer = () => {
|
||||
transition={{
|
||||
duration: theme.animation.duration.normal,
|
||||
}}
|
||||
onAnimationComplete={handleAnimationComplete}
|
||||
>
|
||||
<StyledRightDrawer ref={rightDrawerRef}>
|
||||
{isRightDrawerOpen && <RightDrawerRouter />}
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
import { createState } from 'twenty-ui';
|
||||
|
||||
export const isRightDrawerAnimationCompletedState = createState<boolean>({
|
||||
key: 'isRightDrawerAnimationCompletedState',
|
||||
defaultValue: false,
|
||||
});
|
||||
@ -1,7 +1,7 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { IconBrandGithub } from 'twenty-ui';
|
||||
|
||||
import { ActionLink } from '@/ui/navigation/link/components/ActionLink.tsx';
|
||||
import { ActionLink } from '@/ui/navigation/link/components/ActionLink';
|
||||
|
||||
import packageJson from '../../../../../../package.json';
|
||||
import { GITHUB_LINK } from '../constants/GithubLink';
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { ComponentDecorator } from 'twenty-ui';
|
||||
|
||||
import { ActionLink } from '@/ui/navigation/link/components/ActionLink.tsx';
|
||||
import { ActionLink } from '@/ui/navigation/link/components/ActionLink';
|
||||
|
||||
const meta: Meta<typeof ActionLink> = {
|
||||
title: 'UI/navigation/link/ActionLink',
|
||||
|
||||
@ -3,21 +3,21 @@ import styled from '@emotion/styled';
|
||||
import { isNonEmptyString, isNumber } from '@sniptt/guards';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { SubTitle } from '@/auth/components/SubTitle.tsx';
|
||||
import { Title } from '@/auth/components/Title.tsx';
|
||||
import { SubscriptionBenefit } from '@/billing/components/SubscriptionBenefit.tsx';
|
||||
import { SubscriptionCard } from '@/billing/components/SubscriptionCard.tsx';
|
||||
import { SubTitle } from '@/auth/components/SubTitle';
|
||||
import { Title } from '@/auth/components/Title';
|
||||
import { SubscriptionBenefit } from '@/billing/components/SubscriptionBenefit';
|
||||
import { SubscriptionCard } from '@/billing/components/SubscriptionCard';
|
||||
import { billingState } from '@/client-config/states/billingState';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { Loader } from '@/ui/feedback/loader/components/Loader.tsx';
|
||||
import { Loader } from '@/ui/feedback/loader/components/Loader';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { MainButton } from '@/ui/input/button/components/MainButton.tsx';
|
||||
import { CardPicker } from '@/ui/input/components/CardPicker.tsx';
|
||||
import { MainButton } from '@/ui/input/button/components/MainButton';
|
||||
import { CardPicker } from '@/ui/input/components/CardPicker';
|
||||
import {
|
||||
ProductPriceEntity,
|
||||
useCheckoutSessionMutation,
|
||||
useGetProductPricesQuery,
|
||||
} from '~/generated/graphql.tsx';
|
||||
} from '~/generated/graphql';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
const StyledChoosePlanContainer = styled.div`
|
||||
|
||||
@ -17,7 +17,7 @@ import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetada
|
||||
import { WorkspaceLogoUploader } from '@/settings/workspace/components/WorkspaceLogoUploader';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { H2Title } from '@/ui/display/typography/components/H2Title';
|
||||
import { Loader } from '@/ui/feedback/loader/components/Loader.tsx';
|
||||
import { Loader } from '@/ui/feedback/loader/components/Loader';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { MainButton } from '@/ui/input/button/components/MainButton';
|
||||
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
|
||||
|
||||
@ -4,12 +4,12 @@ import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { IconCheck } from 'twenty-ui';
|
||||
|
||||
import { SubTitle } from '@/auth/components/SubTitle.tsx';
|
||||
import { Title } from '@/auth/components/Title.tsx';
|
||||
import { SubTitle } from '@/auth/components/SubTitle';
|
||||
import { Title } from '@/auth/components/Title';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { MainButton } from '@/ui/input/button/components/MainButton.tsx';
|
||||
import { MainButton } from '@/ui/input/button/components/MainButton';
|
||||
import { RGBA } from '@/ui/theme/constants/Rgba';
|
||||
import { AnimatedEaseIn } from '@/ui/utilities/animation/components/AnimatedEaseIn.tsx';
|
||||
import { AnimatedEaseIn } from '@/ui/utilities/animation/components/AnimatedEaseIn';
|
||||
|
||||
const StyledCheckContainer = styled.div`
|
||||
align-items: center;
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
"esModuleInterop": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"allowImportingTsExtensions": false,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ThemeType } from './src/modules/ui/theme/constants/ThemeLight';
|
||||
import { ThemeType } from '@/ui/theme/constants/ThemeLight';
|
||||
|
||||
export { ThemeProvider } from '@emotion/react';
|
||||
|
||||
@ -21,7 +21,7 @@ export * from './src/modules/ui/input/button/components/FloatingButtonGroup'
|
||||
export * from './src/modules/ui/input/button/components/FloatingIconButton'
|
||||
export * from './src/modules/ui/input/button/components/FloatingIconButtonGroup'
|
||||
export * from './src/modules/ui/input/button/components/LightButton'
|
||||
export * from './src/modules/ui/navigation/link/components/ActionLink.tsx'
|
||||
export * from './src/modules/ui/navigation/link/components/ActionLink'
|
||||
export * from './src/modules/ui/input/button/components/LightIconButton'
|
||||
export * from './src/modules/ui/input/button/components/MainButton'
|
||||
export * from './src/modules/ui/input/button/components/RoundedIconButton'
|
||||
|
||||
@ -51,6 +51,8 @@ const StyledContainer = styled.div<
|
||||
cursor: ${({ clickable, disabled }) =>
|
||||
clickable ? 'pointer' : disabled ? 'not-allowed' : 'inherit'};
|
||||
display: inline-flex;
|
||||
flex-direction: row-reverse;
|
||||
justify-content: center;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
height: ${({ theme }) => theme.spacing(3)};
|
||||
max-width: ${({ maxWidth }) =>
|
||||
|
||||
Reference in New Issue
Block a user