321 command menu context chips compact version (#10072)
Closes https://github.com/twentyhq/core-team-issues/issues/321 - Create component - Create stories - Fix bug due to `WorkflowDiagramCanvasEditableEffect`
This commit is contained in:
@ -33,20 +33,23 @@ const StyledChip = styled.button<{
|
||||
`;
|
||||
|
||||
const StyledIconsContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
export type CommandMenuContextChipProps = {
|
||||
Icons: React.ReactNode[];
|
||||
text?: string;
|
||||
onClick?: () => void;
|
||||
testId?: string;
|
||||
};
|
||||
|
||||
export const CommandMenuContextChip = ({
|
||||
Icons,
|
||||
text,
|
||||
onClick,
|
||||
testId,
|
||||
}: {
|
||||
Icons: React.ReactNode[];
|
||||
text?: string;
|
||||
onClick?: () => void;
|
||||
testId?: string;
|
||||
}) => {
|
||||
}: CommandMenuContextChipProps) => {
|
||||
return (
|
||||
<StyledChip
|
||||
withText={isNonEmptyString(text)}
|
||||
|
||||
@ -0,0 +1,51 @@
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import {
|
||||
CommandMenuContextChip,
|
||||
CommandMenuContextChipProps,
|
||||
} from './CommandMenuContextChip';
|
||||
|
||||
export const CommandMenuContextChipGroups = ({
|
||||
contextChips,
|
||||
}: {
|
||||
contextChips: CommandMenuContextChipProps[];
|
||||
}) => {
|
||||
if (contextChips.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (contextChips.length < 3) {
|
||||
return (
|
||||
<>
|
||||
{contextChips.map((chip) => (
|
||||
<CommandMenuContextChip
|
||||
key={chip.text}
|
||||
Icons={chip.Icons}
|
||||
text={chip.text}
|
||||
onClick={chip.onClick}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const firstChips = contextChips.slice(0, -1);
|
||||
const lastChip = contextChips.at(-1);
|
||||
|
||||
return (
|
||||
<>
|
||||
{firstChips.length > 0 && (
|
||||
<CommandMenuContextChip
|
||||
Icons={firstChips.map((chip) => chip.Icons?.[0])}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isDefined(lastChip) && (
|
||||
<CommandMenuContextChip
|
||||
Icons={lastChip.Icons}
|
||||
text={lastChip.text}
|
||||
onClick={lastChip.onClick}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,53 @@
|
||||
import { CommandMenuContextChipGroups } from '@/command-menu/components/CommandMenuContextChipGroups';
|
||||
import { CommandMenuContextRecordChipAvatars } from '@/command-menu/components/CommandMenuContextRecordChipAvatars';
|
||||
import { getSelectedRecordsContextText } from '@/command-menu/utils/getRecordContextText';
|
||||
import { useFindManyRecordsSelectedInContextStore } from '@/context-store/hooks/useFindManyRecordsSelectedInContextStore';
|
||||
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
|
||||
import { CommandMenuContextChipProps } from './CommandMenuContextChip';
|
||||
|
||||
export const CommandMenuContextChipGroupsWithRecordSelection = ({
|
||||
contextChips,
|
||||
objectMetadataItemId,
|
||||
}: {
|
||||
contextChips: CommandMenuContextChipProps[];
|
||||
objectMetadataItemId: string;
|
||||
}) => {
|
||||
const { objectMetadataItem } = useObjectMetadataItemById({
|
||||
objectId: objectMetadataItemId,
|
||||
});
|
||||
|
||||
const { records, loading, totalCount } =
|
||||
useFindManyRecordsSelectedInContextStore({
|
||||
limit: 3,
|
||||
});
|
||||
|
||||
if (loading || !totalCount) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const Avatars = records.map((record) => (
|
||||
<CommandMenuContextRecordChipAvatars
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
key={record.id}
|
||||
record={record}
|
||||
/>
|
||||
));
|
||||
|
||||
const selectedRecordsContextText = getSelectedRecordsContextText(
|
||||
objectMetadataItem,
|
||||
records,
|
||||
totalCount,
|
||||
);
|
||||
|
||||
return (
|
||||
<CommandMenuContextChipGroups
|
||||
contextChips={[
|
||||
{
|
||||
text: selectedRecordsContextText,
|
||||
Icons: Avatars,
|
||||
},
|
||||
...contextChips,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -1,10 +1,11 @@
|
||||
import { CommandMenuContextChip } from '@/command-menu/components/CommandMenuContextChip';
|
||||
import { CommandMenuContextRecordChip } from '@/command-menu/components/CommandMenuContextRecordChip';
|
||||
import { CommandMenuContextChipGroups } from '@/command-menu/components/CommandMenuContextChipGroups';
|
||||
import { CommandMenuContextChipGroupsWithRecordSelection } from '@/command-menu/components/CommandMenuContextChipGroupsWithRecordSelection';
|
||||
import { COMMAND_MENU_SEARCH_BAR_HEIGHT } from '@/command-menu/constants/CommandMenuSearchBarHeight';
|
||||
import { COMMAND_MENU_SEARCH_BAR_PADDING } from '@/command-menu/constants/CommandMenuSearchBarPadding';
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
|
||||
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
|
||||
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageTitle';
|
||||
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
|
||||
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
|
||||
@ -13,6 +14,7 @@ import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useMemo } from 'react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import {
|
||||
@ -98,7 +100,9 @@ export const CommandMenuTopBar = () => {
|
||||
|
||||
const commandMenuPage = useRecoilValue(commandMenuPageState);
|
||||
|
||||
const { title, Icon } = useRecoilValue(commandMenuPageInfoState);
|
||||
const commandMenuNavigationStack = useRecoilValue(
|
||||
commandMenuNavigationStackState,
|
||||
);
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
@ -106,31 +110,40 @@ export const CommandMenuTopBar = () => {
|
||||
FeatureFlagKey.IsCommandMenuV2Enabled,
|
||||
);
|
||||
|
||||
const contextChips = useMemo(() => {
|
||||
return commandMenuNavigationStack
|
||||
.filter((page) => page.page !== CommandMenuPages.Root)
|
||||
.map((page) => {
|
||||
return {
|
||||
Icons: [<page.pageIcon size={theme.icon.size.sm} />],
|
||||
text: page.pageTitle,
|
||||
};
|
||||
});
|
||||
}, [commandMenuNavigationStack, theme.icon.size.sm]);
|
||||
|
||||
return (
|
||||
<StyledInputContainer>
|
||||
<StyledContentContainer>
|
||||
{isCommandMenuV2Enabled && (
|
||||
<CommandMenuContextChip
|
||||
Icons={[<IconChevronLeft size={theme.icon.size.sm} />]}
|
||||
onClick={() => {
|
||||
goBackFromCommandMenu();
|
||||
}}
|
||||
testId="command-menu-go-back-button"
|
||||
/>
|
||||
)}
|
||||
{commandMenuPage !== CommandMenuPages.SearchRecords &&
|
||||
isDefined(contextStoreCurrentObjectMetadataId) && (
|
||||
<CommandMenuContextRecordChip
|
||||
objectMetadataItemId={contextStoreCurrentObjectMetadataId}
|
||||
<>
|
||||
<CommandMenuContextChip
|
||||
Icons={[<IconChevronLeft size={theme.icon.size.sm} />]}
|
||||
onClick={() => {
|
||||
goBackFromCommandMenu();
|
||||
}}
|
||||
testId="command-menu-go-back-button"
|
||||
/>
|
||||
)}
|
||||
{isDefined(Icon) && (
|
||||
<CommandMenuContextChip
|
||||
Icons={[<Icon size={theme.icon.size.sm} />]}
|
||||
text={title}
|
||||
/>
|
||||
{isDefined(contextStoreCurrentObjectMetadataId) &&
|
||||
commandMenuPage !== CommandMenuPages.SearchRecords ? (
|
||||
<CommandMenuContextChipGroupsWithRecordSelection
|
||||
contextChips={contextChips}
|
||||
objectMetadataItemId={contextStoreCurrentObjectMetadataId}
|
||||
/>
|
||||
) : (
|
||||
<CommandMenuContextChipGroups contextChips={contextChips} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{(commandMenuPage === CommandMenuPages.Root ||
|
||||
commandMenuPage === CommandMenuPages.SearchRecords) && (
|
||||
<StyledInput
|
||||
|
||||
@ -22,6 +22,7 @@ import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
|
||||
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
|
||||
import { HttpResponse, graphql } from 'msw';
|
||||
import { IconDotsVertical } from 'twenty-ui';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
||||
import { JestContextStoreSetter } from '~/testing/jest/JestContextStoreSetter';
|
||||
@ -89,6 +90,8 @@ const meta: Meta<typeof CommandMenu> = {
|
||||
setCommandMenuNavigationStack([
|
||||
{
|
||||
page: CommandMenuPages.Root,
|
||||
pageTitle: 'Command Menu',
|
||||
pageIcon: IconDotsVertical,
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
@ -0,0 +1,97 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import styled from '@emotion/styled';
|
||||
import {
|
||||
ComponentDecorator,
|
||||
IconBuildingSkyscraper,
|
||||
IconSearch,
|
||||
IconSettingsAutomation,
|
||||
IconUser,
|
||||
} from 'twenty-ui';
|
||||
import { CommandMenuContextChipGroups } from '../CommandMenuContextChipGroups';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const meta: Meta<typeof CommandMenuContextChipGroups> = {
|
||||
title: 'Modules/CommandMenu/CommandMenuContextChipGroups',
|
||||
component: CommandMenuContextChipGroups,
|
||||
decorators: [
|
||||
(Story) => <StyledContainer>{Story()}</StyledContainer>,
|
||||
ComponentDecorator,
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof CommandMenuContextChipGroups>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
contextChips: [
|
||||
{
|
||||
Icons: [<IconBuildingSkyscraper size={16} />],
|
||||
text: 'Company',
|
||||
},
|
||||
{
|
||||
Icons: [<IconUser size={16} />],
|
||||
text: 'Person',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const SingleChip: Story = {
|
||||
args: {
|
||||
contextChips: [
|
||||
{
|
||||
Icons: [<IconUser size={16} />],
|
||||
text: 'Person',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const ThreeChipsWithIcons: Story = {
|
||||
args: {
|
||||
contextChips: [
|
||||
{
|
||||
Icons: [<IconBuildingSkyscraper size={16} />],
|
||||
text: 'Company',
|
||||
},
|
||||
{
|
||||
Icons: [<IconUser size={16} />],
|
||||
text: 'Person',
|
||||
},
|
||||
{
|
||||
Icons: [<IconSettingsAutomation size={16} />],
|
||||
text: 'Settings',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const FourChipsWithIcons: Story = {
|
||||
args: {
|
||||
contextChips: [
|
||||
{
|
||||
Icons: [<IconBuildingSkyscraper size={16} />],
|
||||
text: 'Company',
|
||||
},
|
||||
{
|
||||
Icons: [<IconUser size={16} />],
|
||||
text: 'Person',
|
||||
},
|
||||
{
|
||||
Icons: [<IconSettingsAutomation size={16} />],
|
||||
text: 'Settings',
|
||||
},
|
||||
{
|
||||
Icons: [<IconSearch size={16} />],
|
||||
text: 'Search',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@ -9,7 +9,7 @@ import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState
|
||||
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageTitle';
|
||||
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
|
||||
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||
import { IconSearch } from 'twenty-ui';
|
||||
import { IconList, IconSearch } from 'twenty-ui';
|
||||
|
||||
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<RecoilRoot>
|
||||
@ -53,9 +53,7 @@ describe('useCommandMenu', () => {
|
||||
const { result } = renderHooks();
|
||||
|
||||
act(() => {
|
||||
result.current.commandMenu.navigateCommandMenu({
|
||||
page: CommandMenuPages.Root,
|
||||
});
|
||||
result.current.commandMenu.openRootCommandMenu();
|
||||
});
|
||||
|
||||
expect(result.current.isCommandMenuOpened).toBe(true);
|
||||
@ -119,7 +117,8 @@ describe('useCommandMenu', () => {
|
||||
act(() => {
|
||||
result.current.commandMenu.navigateCommandMenu({
|
||||
page: CommandMenuPages.ViewRecord,
|
||||
pageTitle: 'View Record',
|
||||
pageTitle: 'Company',
|
||||
pageIcon: IconList,
|
||||
});
|
||||
});
|
||||
|
||||
@ -131,13 +130,14 @@ describe('useCommandMenu', () => {
|
||||
},
|
||||
{
|
||||
page: CommandMenuPages.ViewRecord,
|
||||
pageTitle: 'View Record',
|
||||
pageTitle: 'Company',
|
||||
pageIcon: IconList,
|
||||
},
|
||||
]);
|
||||
expect(result.current.commandMenuPage).toBe(CommandMenuPages.ViewRecord);
|
||||
expect(result.current.commandMenuPageInfo).toEqual({
|
||||
title: 'View Record',
|
||||
Icon: undefined,
|
||||
title: 'Company',
|
||||
Icon: IconList,
|
||||
});
|
||||
});
|
||||
|
||||
@ -155,7 +155,8 @@ describe('useCommandMenu', () => {
|
||||
act(() => {
|
||||
result.current.commandMenu.navigateCommandMenu({
|
||||
page: CommandMenuPages.ViewRecord,
|
||||
pageTitle: 'View Record',
|
||||
pageTitle: 'Company',
|
||||
pageIcon: IconList,
|
||||
});
|
||||
});
|
||||
|
||||
@ -167,7 +168,8 @@ describe('useCommandMenu', () => {
|
||||
},
|
||||
{
|
||||
page: CommandMenuPages.ViewRecord,
|
||||
pageTitle: 'View Record',
|
||||
pageTitle: 'Company',
|
||||
pageIcon: IconList,
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
@ -23,8 +23,9 @@ import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType
|
||||
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
|
||||
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
|
||||
import { emitRightDrawerCloseEvent } from '@/ui/layout/right-drawer/utils/emitRightDrawerCloseEvent';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { IconSearch } from 'twenty-ui';
|
||||
import { IconDotsVertical, IconList, IconSearch } from 'twenty-ui';
|
||||
import { isCommandMenuOpenedState } from '../states/isCommandMenuOpenedState';
|
||||
|
||||
export const useCommandMenu = () => {
|
||||
@ -123,6 +124,14 @@ export const useCommandMenu = () => {
|
||||
[openCommandMenu],
|
||||
);
|
||||
|
||||
const openRootCommandMenu = useCallback(() => {
|
||||
navigateCommandMenu({
|
||||
page: CommandMenuPages.Root,
|
||||
pageTitle: 'Command Menu',
|
||||
pageIcon: IconDotsVertical,
|
||||
});
|
||||
}, [navigateCommandMenu]);
|
||||
|
||||
const toggleCommandMenu = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
async () => {
|
||||
@ -135,12 +144,10 @@ export const useCommandMenu = () => {
|
||||
if (isCommandMenuOpened) {
|
||||
closeCommandMenu();
|
||||
} else {
|
||||
navigateCommandMenu({
|
||||
page: CommandMenuPages.Root,
|
||||
});
|
||||
openRootCommandMenu();
|
||||
}
|
||||
},
|
||||
[closeCommandMenu, navigateCommandMenu],
|
||||
[closeCommandMenu, openRootCommandMenu],
|
||||
);
|
||||
|
||||
const goBackFromCommandMenu = useRecoilCallback(
|
||||
@ -204,6 +211,8 @@ export const useCommandMenu = () => {
|
||||
set(viewableRecordIdState, recordId);
|
||||
navigateCommandMenu({
|
||||
page: CommandMenuPages.ViewRecord,
|
||||
pageTitle: objectNameSingular,
|
||||
pageIcon: IconList,
|
||||
});
|
||||
};
|
||||
},
|
||||
@ -267,6 +276,7 @@ export const useCommandMenu = () => {
|
||||
);
|
||||
|
||||
return {
|
||||
openRootCommandMenu,
|
||||
closeCommandMenu,
|
||||
navigateCommandMenu,
|
||||
navigateCommandMenuHistory,
|
||||
|
||||
@ -3,8 +3,8 @@ import { IconComponent, createState } from 'twenty-ui';
|
||||
|
||||
export type CommandMenuNavigationStackItem = {
|
||||
page: CommandMenuPages;
|
||||
pageTitle?: string;
|
||||
pageIcon?: IconComponent;
|
||||
pageTitle: string;
|
||||
pageIcon: IconComponent;
|
||||
};
|
||||
|
||||
export const commandMenuNavigationStackState = createState<
|
||||
|
||||
Reference in New Issue
Block a user