From b5b46f923af1c9c53270a51a4c378a917d13745f Mon Sep 17 00:00:00 2001 From: Rishi Raj Jain Date: Thu, 21 Sep 2023 23:48:44 +0530 Subject: [PATCH] fix: Command bar is broken (#1617) * Update CommandMenu.tsx * remove broken states * convert to array * convert filter conditions * empty condition * finally * update the logic * add test * lint * move file --- .../command-menu/components/CommandMenu.tsx | 143 +++++----- .../__stories__/CommandMenu.stories.tsx | 241 +++++++++++++++-- .../__stories__/WrapperCommandMenu.tsx | 247 ++++++++++++++++++ 3 files changed, 538 insertions(+), 93 deletions(-) create mode 100644 front/src/modules/command-menu/components/__stories__/WrapperCommandMenu.tsx diff --git a/front/src/modules/command-menu/components/CommandMenu.tsx b/front/src/modules/command-menu/components/CommandMenu.tsx index 66ffca3a0..80e6def7e 100644 --- a/front/src/modules/command-menu/components/CommandMenu.tsx +++ b/front/src/modules/command-menu/components/CommandMenu.tsx @@ -17,7 +17,7 @@ import { getLogoUrlFromDomainName } from '~/utils'; import { useCommandMenu } from '../hooks/useCommandMenu'; import { commandMenuCommandsState } from '../states/commandMenuCommandsState'; import { isCommandMenuOpenedState } from '../states/isCommandMenuOpenedState'; -import { CommandType } from '../types/Command'; +import { Command, CommandType } from '../types/Command'; import { CommandMenuItem } from './CommandMenuItem'; import { @@ -65,6 +65,7 @@ export const CommandMenu = () => { limit: 3, }, }); + const companies = companyData?.searchResults ?? []; const { data: activityData } = useSearchActivityQuery({ @@ -78,18 +79,38 @@ export const CommandMenu = () => { limit: 3, }, }); + const activities = activityData?.searchResults ?? []; - const matchingNavigateCommand = commandMenuCommands.find( + const checkInShortcuts = (cmd: Command, search: string) => { + if (cmd.shortcuts && cmd.shortcuts.length > 0) { + return cmd.shortcuts + .join('') + .toLowerCase() + .includes(search.toLowerCase()); + } + return false; + }; + + const checkInLabels = (cmd: Command, search: string) => { + if (cmd.label) { + return cmd.label.toLowerCase().includes(search.toLowerCase()); + } + return false; + }; + + const matchingNavigateCommand = commandMenuCommands.filter( (cmd) => - cmd.shortcuts?.join('') === search?.toUpperCase() && - cmd.type === CommandType.Navigate, + (search.length > 0 + ? checkInShortcuts(cmd, search) || checkInLabels(cmd, search) + : true) && cmd.type === CommandType.Navigate, ); - const matchingCreateCommand = commandMenuCommands.find( + const matchingCreateCommand = commandMenuCommands.filter( (cmd) => - cmd.shortcuts?.join('') === search?.toUpperCase() && - cmd.type === CommandType.Create, + (search.length > 0 + ? checkInShortcuts(cmd, search) || checkInLabels(cmd, search) + : true) && cmd.type === CommandType.Create, ); return ( @@ -100,122 +121,96 @@ export const CommandMenu = () => { closeCommandMenu(); } }} - label="Global Command Menu" shouldFilter={false} + label="Global Command Menu" > - No results found. - {!matchingCreateCommand && ( + {matchingCreateCommand.length < 1 && + matchingNavigateCommand.length < 1 && + people.length < 1 && + companies.length < 1 && + activities.length < 1 && No results found.} + {matchingCreateCommand.length > 0 && ( - {commandMenuCommands - .filter((cmd) => cmd.type === CommandType.Create) - .map((cmd) => ( - - ))} + {matchingCreateCommand.map((cmd) => ( + + ))} )} - {matchingCreateCommand && ( - - - - )} - {matchingNavigateCommand && ( + {matchingNavigateCommand.length > 0 && ( - + {matchingNavigateCommand.map((cmd) => ( + + ))} )} - {!!people.length && ( + {people.length > 0 && ( {people.map((person) => ( ( )} /> ))} )} - {!!companies.length && ( + {companies.length > 0 && ( {companies.map((company) => ( ( )} /> ))} )} - {!!activities.length && ( + {activities.length > 0 && ( {activities.map((activity) => ( openActivityRightDrawer(activity.id)} - label={activity.title ?? ''} - key={activity.id} Icon={IconNotes} + key={activity.id} + label={activity.title ?? ''} + onClick={() => openActivityRightDrawer(activity.id)} /> ))} )} - {!matchingNavigateCommand && ( - - {commandMenuCommands - .filter( - (cmd) => - (cmd.shortcuts?.join('').includes(search?.toUpperCase()) || - cmd.label?.toUpperCase().includes(search?.toUpperCase())) && - cmd.type === CommandType.Navigate, - ) - .map((cmd) => ( - - ))} - - )} ); diff --git a/front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx b/front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx index a91c47ffd..501bbd782 100644 --- a/front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx +++ b/front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx @@ -1,15 +1,160 @@ import { MemoryRouter } from 'react-router-dom'; +import { expect } from '@storybook/jest'; import type { Meta, StoryObj } from '@storybook/react'; import { fireEvent, userEvent, within } from '@storybook/testing-library'; import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; import { sleep } from '~/testing/sleep'; -import { CommandMenu } from '../CommandMenu'; +import { WrapperCommandMenu } from './WrapperCommandMenu'; -const meta: Meta = { - title: 'Modules/CommandMenu/CommandMenu', - component: CommandMenu, +enum CommandType { + Navigate = 'Navigate', + Create = 'Create', +} + +const meta: Meta = { + title: 'Modules/CommandMenu/WrapperCommandMenu', + component: () => ( + + ), decorators: [ (Story) => ( @@ -21,36 +166,94 @@ const meta: Meta = { }; export default meta; -type Story = StoryObj; +type Story = StoryObj; -export const Default: Story = {}; - -export const CmdK: Story = { +export const DefaultWithoutSearch: Story = { play: async ({ canvasElement }) => { fireEvent.keyDown(canvasElement, { key: 'k', code: 'KeyK', metaKey: true, }); - await sleep(50); - const canvas = within(document.body); - const searchInput = await canvas.findByPlaceholderText('Search'); + await sleep(10); + await userEvent.type(searchInput, ''); + expect(await canvas.findByText('Create Task')).toBeInTheDocument(); + expect(await canvas.findByText('Go to People')).toBeInTheDocument(); + expect(await canvas.findByText('Go to Companies')).toBeInTheDocument(); + expect(await canvas.findByText('Go to Opportunities')).toBeInTheDocument(); + expect(await canvas.findByText('Go to Settings')).toBeInTheDocument(); + expect(await canvas.findByText('Go to Tasks')).toBeInTheDocument(); + }, +}; - await userEvent.type(searchInput, '{arrowdown}'); - await userEvent.type(searchInput, '{arrowup}'); - await userEvent.type(searchInput, '{arrowdown}'); - await userEvent.type(searchInput, '{arrowdown}'); - await userEvent.type(searchInput, '{enter}'); - - await sleep(50); - +export const MatchingPersonCompanyActivityCreateNavigate: Story = { + play: async ({ canvasElement }) => { fireEvent.keyDown(canvasElement, { key: 'k', code: 'KeyK', metaKey: true, }); + await sleep(50); + const canvas = within(document.body); + const searchInput = await canvas.findByPlaceholderText('Search'); + await sleep(10); + await userEvent.type(searchInput, 'a'); + expect(await canvas.findByText('Isabella Scott')).toBeInTheDocument(); + expect(await canvas.findByText('Airbnb')).toBeInTheDocument(); + expect(await canvas.findByText('Buyout Proposal')).toBeInTheDocument(); + expect(await canvas.findByText('Create Task')).toBeInTheDocument(); + expect(await canvas.findByText('Go to Tasks')).toBeInTheDocument(); + }, +}; + +export const OnlyMatchingCreateAndNavigate: Story = { + play: async ({ canvasElement }) => { + fireEvent.keyDown(canvasElement, { + key: 'k', + code: 'KeyK', + metaKey: true, + }); + await sleep(50); + const canvas = within(document.body); + const searchInput = await canvas.findByPlaceholderText('Search'); + await sleep(10); + await userEvent.type(searchInput, 'tas'); + expect(await canvas.findByText('Create Task')).toBeInTheDocument(); + expect(await canvas.findByText('Go to Tasks')).toBeInTheDocument(); + }, +}; + +export const AtleastMatchingOnePerson: Story = { + play: async ({ canvasElement }) => { + fireEvent.keyDown(canvasElement, { + key: 'k', + code: 'KeyK', + metaKey: true, + }); + await sleep(50); + const canvas = within(document.body); + const searchInput = await canvas.findByPlaceholderText('Search'); + await sleep(10); + await userEvent.type(searchInput, 'sy'); + expect(await canvas.findByText('Sylvie Palmer')).toBeInTheDocument(); + }, +}; + +export const NotMatchingAnything: Story = { + play: async ({ canvasElement }) => { + fireEvent.keyDown(canvasElement, { + key: 'k', + code: 'KeyK', + metaKey: true, + }); + await sleep(50); + const canvas = within(document.body); + const searchInput = await canvas.findByPlaceholderText('Search'); + await sleep(10); + await userEvent.type(searchInput, 'asdasdasd'); + expect(await canvas.findByText('No results found.')).toBeInTheDocument(); }, }; diff --git a/front/src/modules/command-menu/components/__stories__/WrapperCommandMenu.tsx b/front/src/modules/command-menu/components/__stories__/WrapperCommandMenu.tsx new file mode 100644 index 000000000..ccdea421c --- /dev/null +++ b/front/src/modules/command-menu/components/__stories__/WrapperCommandMenu.tsx @@ -0,0 +1,247 @@ +import { useState } from 'react'; +import { useRecoilValue } from 'recoil'; + +import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer'; +import { IconNotes } from '@/ui/icon'; +import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; +import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope'; +import { Avatar } from '@/users/components/Avatar'; +import { getLogoUrlFromDomainName } from '~/utils'; + +import { useCommandMenu } from '../../hooks/useCommandMenu'; +import { isCommandMenuOpenedState } from '../../states/isCommandMenuOpenedState'; +import { Command, CommandType } from '../../types/Command'; +import { CommandMenuItem } from '../CommandMenuItem'; +import { + StyledDialog, + StyledEmpty, + StyledGroup, + StyledInput, + StyledList, +} from '../CommandMenuStyles'; + +export const WrapperCommandMenu = ({ + values, + people, + companies, + activities, +}: { + values: Command[]; + people: Array<{ + __typename?: 'Person'; + id: string; + phone?: string | null; + email?: string | null; + city?: string | null; + firstName?: string | null; + lastName?: string | null; + displayName: string; + avatarUrl?: string | null; + createdAt: string; + }>; + companies: Array<{ + __typename?: 'Company'; + address: string; + createdAt: string; + domainName: string; + employees?: number | null; + linkedinUrl?: string | null; + xUrl?: string | null; + annualRecurringRevenue?: number | null; + idealCustomerProfile: boolean; + id: string; + name: string; + _activityCount: number; + accountOwner?: { + __typename?: 'User'; + id: string; + email: string; + displayName: string; + avatarUrl?: string | null; + } | null; + }>; + activities: Array<{ + __typename?: 'Activity'; + id: string; + title?: string | null; + body?: string | null; + }>; +}) => { + const { openCommandMenu, closeCommandMenu } = useCommandMenu(); + const openActivityRightDrawer = useOpenActivityRightDrawer(); + const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState); + const [search, setSearch] = useState(''); + const commandMenuCommands = values; + + const peopleData = people.filter((i) => + search.length > 0 + ? (i.firstName + ? i.firstName?.toLowerCase().includes(search.toLowerCase()) + : false) || + (i.lastName + ? i.lastName?.toLowerCase().includes(search.toLowerCase()) + : false) + : false, + ); + + const companyData = companies.filter((i) => + search.length > 0 + ? i.name + ? i.name?.toLowerCase().includes(search.toLowerCase()) + : false + : false, + ); + + const activityData = activities.filter((i) => + search.length > 0 + ? (i.title + ? i.title?.toLowerCase().includes(search.toLowerCase()) + : false) || + (i.body ? i.body?.toLowerCase().includes(search.toLowerCase()) : false) + : false, + ); + + useScopedHotkeys( + 'ctrl+k,meta+k', + () => { + openCommandMenu(); + }, + AppHotkeyScope.CommandMenu, + [openCommandMenu], + ); + + const checkInShortcuts = (cmd: Command, search: string) => { + if (cmd.shortcuts && cmd.shortcuts.length > 0) { + return cmd.shortcuts + .join('') + .toLowerCase() + .includes(search.toLowerCase()); + } + return false; + }; + + const checkInLabels = (cmd: Command, search: string) => { + if (cmd.label) { + return cmd.label.toLowerCase().includes(search.toLowerCase()); + } + return false; + }; + + const matchingNavigateCommand = commandMenuCommands.filter( + (cmd) => + (search.length > 0 + ? checkInShortcuts(cmd, search) || checkInLabels(cmd, search) + : true) && cmd.type === CommandType.Navigate, + ); + + const matchingCreateCommand = commandMenuCommands.filter( + (cmd) => + (search.length > 0 + ? checkInShortcuts(cmd, search) || checkInLabels(cmd, search) + : true) && cmd.type === CommandType.Create, + ); + + return ( + { + if (!opened) { + closeCommandMenu(); + } + }} + shouldFilter={false} + label="Global Command Menu" + > + + + {matchingCreateCommand.length < 1 && + matchingNavigateCommand.length < 1 && + peopleData.length < 1 && + companyData.length < 1 && + activityData.length < 1 && ( + No results found. + )} + {matchingCreateCommand.length > 0 && ( + + {matchingCreateCommand.map((cmd) => ( + + ))} + + )} + {matchingNavigateCommand.length > 0 && ( + + {matchingNavigateCommand.map((cmd) => ( + + ))} + + )} + {peopleData.length > 0 && ( + + {peopleData.map((person) => ( + ( + + )} + /> + ))} + + )} + {companyData.length > 0 && ( + + {companyData.map((company) => ( + ( + + )} + /> + ))} + + )} + {activityData.length > 0 && ( + + {activityData.map((activity) => ( + openActivityRightDrawer(activity.id)} + /> + ))} + + )} + + + ); +};