From 827d6390e4e8b8a551b787159567986e56929030 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Sun, 25 Jun 2023 22:25:31 -0700 Subject: [PATCH] Refactoring shortcuts and commandbar (#412) * Begin refactoring shortcuts and commandbar * Continue refacto hotkeys * Remove debug logs * Add new story * Simplify hotkeys * Simplify hotkeys --------- Co-authored-by: Charles Bochet --- front/src/App.tsx | 6 + .../command-menu/components/CommandMenu.tsx | 96 ++++++ .../components/CommandMenuItem.tsx | 73 +++++ .../components/CommandMenuStyles.tsx | 55 +++- .../__stories__/CommandMenu.stories.tsx | 20 +- .../states/isCommandMenuOpened.ts | 6 + .../modules/hotkeys/hooks/useDirectHotkeys.ts | 30 ++ .../modules/hotkeys/hooks/useGoToHotkeys.ts | 11 + .../hotkeys/hooks/useSequenceHotkeys.ts | 32 ++ .../modules/hotkeys/hooks/useUpDownHotkeys.ts | 31 ++ .../hotkeys/states/pendingHotkeysState.ts | 6 + .../hooks/useEntitySelectLogic.ts | 60 ++-- .../modules/search/components/CommandMenu.tsx | 72 ----- .../editable-cell/types/EditableRelation.tsx | 288 ------------------ .../table/action-bar/EntityTableActionBar.tsx | 1 + front/src/modules/ui/layout/DefaultLayout.tsx | 2 +- front/src/modules/ui/layout/styles/themes.ts | 7 +- .../src/modules/ui/layout/top-bar/TopBar.tsx | 3 + .../resolvers/pipeline-progress.resolver.ts | 2 - 19 files changed, 387 insertions(+), 414 deletions(-) create mode 100644 front/src/modules/command-menu/components/CommandMenu.tsx create mode 100644 front/src/modules/command-menu/components/CommandMenuItem.tsx rename front/src/modules/{search => command-menu}/components/CommandMenuStyles.tsx (58%) rename front/src/modules/{search => command-menu}/components/__stories__/CommandMenu.stories.tsx (54%) create mode 100644 front/src/modules/command-menu/states/isCommandMenuOpened.ts create mode 100644 front/src/modules/hotkeys/hooks/useDirectHotkeys.ts create mode 100644 front/src/modules/hotkeys/hooks/useGoToHotkeys.ts create mode 100644 front/src/modules/hotkeys/hooks/useSequenceHotkeys.ts create mode 100644 front/src/modules/hotkeys/hooks/useUpDownHotkeys.ts create mode 100644 front/src/modules/hotkeys/states/pendingHotkeysState.ts delete mode 100644 front/src/modules/search/components/CommandMenu.tsx delete mode 100644 front/src/modules/ui/components/editable-cell/types/EditableRelation.tsx diff --git a/front/src/App.tsx b/front/src/App.tsx index 118d0d146..e2e62236d 100644 --- a/front/src/App.tsx +++ b/front/src/App.tsx @@ -2,6 +2,7 @@ import { Navigate, Route, Routes } from 'react-router-dom'; import { RequireAuth } from '@/auth/components/RequireAuth'; import { RequireNotAuth } from '@/auth/components/RequireNotAuth'; +import { useGoToHotkeys } from '@/hotkeys/hooks/useGoToHotkeys'; import { DefaultLayout } from '@/ui/layout/DefaultLayout'; import { Index } from '~/pages/auth/Index'; import { PasswordLogin } from '~/pages/auth/PasswordLogin'; @@ -12,6 +13,11 @@ import { People } from '~/pages/people/People'; import { SettingsProfile } from '~/pages/settings/SettingsProfile'; export function App() { + useGoToHotkeys('p', '/people'); + useGoToHotkeys('c', '/companies'); + useGoToHotkeys('o', '/opportunities'); + useGoToHotkeys('s', '/settings/profile'); + return ( diff --git a/front/src/modules/command-menu/components/CommandMenu.tsx b/front/src/modules/command-menu/components/CommandMenu.tsx new file mode 100644 index 000000000..3da800dc6 --- /dev/null +++ b/front/src/modules/command-menu/components/CommandMenu.tsx @@ -0,0 +1,96 @@ +import React from 'react'; +import { useRecoilState } from 'recoil'; + +import { useDirectHotkeys } from '@/hotkeys/hooks/useDirectHotkeys'; + +import { isCommandMenuOpenedState } from '../states/isCommandMenuOpened'; + +import { CommandMenuItem } from './CommandMenuItem'; +import { + StyledDialog, + StyledEmpty, + StyledGroup, + StyledInput, + StyledList, + // StyledSeparator, +} from './CommandMenuStyles'; + +export function CommandMenu() { + const [open, setOpen] = useRecoilState(isCommandMenuOpenedState); + + useDirectHotkeys( + 'ctrl+k,meta+k', + () => { + setOpen((prevOpen) => !prevOpen); + }, + [setOpen], + ); + + /* + TODO: Allow performing actions on page through CommandBar + + import { useMatch, useResolvedPath } from 'react-router-dom'; + import { IconBuildingSkyscraper, IconUser } from '@/ui/icons'; + + const createSection = ( + + } + shortcuts={ + !!useMatch({ + path: useResolvedPath('/people').pathname, + end: true, + }) + ? ['C'] + : [] + } + /> + } + shortcuts={ + !!useMatch({ + path: useResolvedPath('/companies').pathname, + end: true, + }) + ? ['C'] + : [] + } + /> + + );*/ + + return ( + + + + No results found. + + + + + + + + + ); +} diff --git a/front/src/modules/command-menu/components/CommandMenuItem.tsx b/front/src/modules/command-menu/components/CommandMenuItem.tsx new file mode 100644 index 000000000..8f21eeab7 --- /dev/null +++ b/front/src/modules/command-menu/components/CommandMenuItem.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import { ReactNode } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useSetRecoilState } from 'recoil'; + +import { IconArrowUpRight } from '@/ui/icons'; + +import { isCommandMenuOpenedState } from '../states/isCommandMenuOpened'; + +import { + StyledIconAndLabelContainer, + StyledIconContainer, + StyledMenuItem, + StyledShortCut, + StyledShortcutsContainer, +} from './CommandMenuStyles'; + +export type OwnProps = { + label: string; + to?: string; + onClick?: () => void; + icon?: ReactNode; + shortcuts?: Array; +}; + +export function CommandMenuItem({ + label, + to, + onClick, + icon, + shortcuts, +}: OwnProps) { + const setOpen = useSetRecoilState(isCommandMenuOpenedState); + const navigate = useNavigate(); + + if (to) { + icon = ; + } + + const onItemClick = () => { + setOpen(false); + + if (onClick) { + onClick(); + return; + } + if (to) { + navigate(to); + return; + } + }; + + return ( + + + {icon} + {label} + + + {shortcuts && + shortcuts.map((shortcut, index) => { + const prefix = index > 0 ? 'then' : ''; + return ( + + {prefix} + {shortcut} + + ); + })} + + + ); +} diff --git a/front/src/modules/search/components/CommandMenuStyles.tsx b/front/src/modules/command-menu/components/CommandMenuStyles.tsx similarity index 58% rename from front/src/modules/search/components/CommandMenuStyles.tsx rename to front/src/modules/command-menu/components/CommandMenuStyles.tsx index 28eb2fe4e..88909f42c 100644 --- a/front/src/modules/search/components/CommandMenuStyles.tsx +++ b/front/src/modules/command-menu/components/CommandMenuStyles.tsx @@ -4,7 +4,7 @@ import { Command } from 'cmdk'; export const StyledDialog = styled(Command.Dialog)` background: ${(props) => props.theme.primaryBackground}; border-radius: ${(props) => props.theme.borderRadius}; - box-shadow: ${(props) => props.theme.modalBoxShadow}; + box-shadow: ${(props) => props.theme.heavyBoxShadow}; font-family: ${(props) => props.theme.fontFamily}; left: 50%; max-width: 640px; @@ -21,7 +21,6 @@ export const StyledInput = styled(Command.Input)` border: none; border-bottom: 1px solid ${(props) => props.theme.primaryBorder}; border-radius: 0; - caret-color: ${(props) => props.theme.blue}; color: ${(props) => props.theme.text100}; font-size: ${(props) => props.theme.fontSizeLarge}; margin: 0; @@ -30,14 +29,15 @@ export const StyledInput = styled(Command.Input)` width: 100%; `; -export const StyledItem = styled(Command.Item)` +export const StyledMenuItem = styled(Command.Item)` align-items: center; - color: ${(props) => props.theme.text100}; + color: ${(props) => props.theme.text80}; cursor: pointer; display: flex; font-size: ${(props) => props.theme.fontSizeMedium}; gap: ${(props) => props.theme.spacing(3)}; - height: 48px; + height: 40px; + justify-content: space-between; padding: 0 ${(props) => props.theme.spacing(4)}; position: relative; transition: all 150ms ease; @@ -47,23 +47,24 @@ export const StyledItem = styled(Command.Item)` background: ${(props) => props.theme.lightBackgroundTransparent}; } &[data-selected='true'] { - background: ${(props) => props.theme.secondaryBackground}; + background: ${(props) => props.theme.tertiaryBackground}; + /* Could be nice to add a caret like this for better accessibility in the future + But it needs to be consistend with other picker dropdown (e.g. company) &:after { - background: ${(props) => props.theme.blue}; + background: ${(props) => props.theme.quaternaryBackground}; content: ''; height: 100%; left: 0; position: absolute; width: 3px; z-index: ${(props) => props.theme.lastLayerZIndex}; - } + } */ } &[data-disabled='true'] { color: ${(props) => props.theme.text30}; cursor: not-allowed; } svg { - color: ${(props) => props.theme.text80}; height: 16px; width: 16px; } @@ -85,7 +86,12 @@ export const StyledGroup = styled(Command.Group)` color: ${(props) => props.theme.text30}; display: flex; font-size: ${(props) => props.theme.fontSizeExtraSmall}; - padding: ${(props) => props.theme.spacing(2)}; + font-weight: ${(props) => props.theme.fontWeightBold}; + padding-bottom: ${(props) => props.theme.spacing(2)}; + padding-left: ${(props) => props.theme.spacing(4)}; + padding-right: ${(props) => props.theme.spacing(4)}; + padding-top: ${(props) => props.theme.spacing(2)}; + text-transform: uppercase; user-select: none; } `; @@ -101,3 +107,32 @@ export const StyledEmpty = styled(Command.Empty)` `; export const StyledSeparator = styled(Command.Separator)``; + +export const StyledIconAndLabelContainer = styled.div` + align-items: center; + display: flex; + gap: ${(props) => props.theme.spacing(2)}; +`; +export const StyledIconContainer = styled.div` + align-items: center; + background: ${(props) => props.theme.lightBackgroundTransparent}; + border-radius: 4px; + color: ${(props) => props.theme.text60}; + display: flex; + padding: ${(props) => props.theme.spacing(1)}; +`; +export const StyledShortCut = styled.div` + background-color: ${(props) => props.theme.lightBackgroundTransparent}; + border-radius: 4px; + color: ${(props) => props.theme.text30}; + margin-left: ${(props) => props.theme.spacing(1)}; + margin-right: ${(props) => props.theme.spacing(1)}; + padding: ${(props) => props.theme.spacing(1)}; +`; + +export const StyledShortcutsContainer = styled.div` + align-items: center; + color: ${(props) => props.theme.text30}; + display: flex; + font-size: ${(props) => props.theme.fontSizeSmall}; +`; diff --git a/front/src/modules/search/components/__stories__/CommandMenu.stories.tsx b/front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx similarity index 54% rename from front/src/modules/search/components/__stories__/CommandMenu.stories.tsx rename to front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx index 7464ae623..d656e0450 100644 --- a/front/src/modules/search/components/__stories__/CommandMenu.stories.tsx +++ b/front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx @@ -1,12 +1,13 @@ import { MemoryRouter } from 'react-router-dom'; import type { Meta, StoryObj } from '@storybook/react'; +import { fireEvent } from '@storybook/testing-library'; import { getRenderWrapperForComponent } from '~/testing/renderWrappers'; import { CommandMenu } from '../CommandMenu'; const meta: Meta = { - title: 'Modules/Search/CommandMenu', + title: 'Modules/CommandMenu/CommandMenu', component: CommandMenu, }; @@ -16,7 +17,22 @@ type Story = StoryObj; export const Default: Story = { render: getRenderWrapperForComponent( - + , ), }; + +export const CmdK: Story = { + render: getRenderWrapperForComponent( + + + , + ), + play: async ({ canvasElement }) => { + fireEvent.keyDown(canvasElement, { + key: 'k', + code: 'KeyK', + metaKey: true, + }); + }, +}; diff --git a/front/src/modules/command-menu/states/isCommandMenuOpened.ts b/front/src/modules/command-menu/states/isCommandMenuOpened.ts new file mode 100644 index 000000000..0077d54df --- /dev/null +++ b/front/src/modules/command-menu/states/isCommandMenuOpened.ts @@ -0,0 +1,6 @@ +import { atom } from 'recoil'; + +export const isCommandMenuOpenedState = atom({ + key: 'command-menu/isCommandMenuOpenedState', + default: false, +}); diff --git a/front/src/modules/hotkeys/hooks/useDirectHotkeys.ts b/front/src/modules/hotkeys/hooks/useDirectHotkeys.ts new file mode 100644 index 000000000..c1223f266 --- /dev/null +++ b/front/src/modules/hotkeys/hooks/useDirectHotkeys.ts @@ -0,0 +1,30 @@ +import { useHotkeys } from 'react-hotkeys-hook'; +import { + Hotkey, + HotkeyCallback, + OptionsOrDependencyArray, +} from 'react-hotkeys-hook/dist/types'; +import { useRecoilState } from 'recoil'; + +import { pendingHotkeyState } from '../states/pendingHotkeysState'; + +export function useDirectHotkeys( + keys: string, + callback: HotkeyCallback, + dependencies?: OptionsOrDependencyArray, +) { + const [pendingHotkey, setPendingHotkey] = useRecoilState(pendingHotkeyState); + + const callbackIfDirectKey = function ( + keyboardEvent: KeyboardEvent, + hotkeysEvent: Hotkey, + ) { + if (!pendingHotkey) { + callback(keyboardEvent, hotkeysEvent); + return; + } + setPendingHotkey(null); + }; + + useHotkeys(keys, callbackIfDirectKey, dependencies); +} diff --git a/front/src/modules/hotkeys/hooks/useGoToHotkeys.ts b/front/src/modules/hotkeys/hooks/useGoToHotkeys.ts new file mode 100644 index 000000000..df67341dc --- /dev/null +++ b/front/src/modules/hotkeys/hooks/useGoToHotkeys.ts @@ -0,0 +1,11 @@ +import { useNavigate } from 'react-router-dom'; + +import { useSequenceHotkeys } from './useSequenceHotkeys'; + +export function useGoToHotkeys(key: string, location: string) { + const navigate = useNavigate(); + + useSequenceHotkeys('g', key, () => { + navigate(location); + }); +} diff --git a/front/src/modules/hotkeys/hooks/useSequenceHotkeys.ts b/front/src/modules/hotkeys/hooks/useSequenceHotkeys.ts new file mode 100644 index 000000000..3ec2544bf --- /dev/null +++ b/front/src/modules/hotkeys/hooks/useSequenceHotkeys.ts @@ -0,0 +1,32 @@ +import { useHotkeys } from 'react-hotkeys-hook'; +import { useRecoilState } from 'recoil'; + +import { pendingHotkeyState } from '../states/pendingHotkeysState'; + +export function useSequenceHotkeys( + firstKey: string, + secondKey: string, + callback: () => void, +) { + const [pendingHotkey, setPendingHotkey] = useRecoilState(pendingHotkeyState); + + useHotkeys( + firstKey, + () => { + setPendingHotkey(firstKey); + }, + [pendingHotkey], + ); + + useHotkeys( + secondKey, + () => { + if (pendingHotkey !== firstKey) { + return; + } + setPendingHotkey(null); + callback(); + }, + [pendingHotkey, setPendingHotkey], + ); +} diff --git a/front/src/modules/hotkeys/hooks/useUpDownHotkeys.ts b/front/src/modules/hotkeys/hooks/useUpDownHotkeys.ts new file mode 100644 index 000000000..f5149e37d --- /dev/null +++ b/front/src/modules/hotkeys/hooks/useUpDownHotkeys.ts @@ -0,0 +1,31 @@ +import { useHotkeys } from 'react-hotkeys-hook'; +import { HotkeyCallback } from 'react-hotkeys-hook/dist/types'; +import { OptionsOrDependencyArray } from 'react-hotkeys-hook/dist/types'; + +export function useUpDownHotkeys( + upArrowCallBack: HotkeyCallback, + downArrownCallback: HotkeyCallback, + dependencies?: OptionsOrDependencyArray, +) { + useHotkeys( + 'up', + upArrowCallBack, + { + enableOnContentEditable: true, + enableOnFormTags: true, + preventDefault: true, + }, + dependencies, + ); + + useHotkeys( + 'down', + downArrownCallback, + { + enableOnContentEditable: true, + enableOnFormTags: true, + preventDefault: true, + }, + dependencies, + ); +} diff --git a/front/src/modules/hotkeys/states/pendingHotkeysState.ts b/front/src/modules/hotkeys/states/pendingHotkeysState.ts new file mode 100644 index 000000000..685e1753c --- /dev/null +++ b/front/src/modules/hotkeys/states/pendingHotkeysState.ts @@ -0,0 +1,6 @@ +import { atom } from 'recoil'; + +export const pendingHotkeyState = atom({ + key: 'command-menu/pendingHotkeyState', + default: null, +}); diff --git a/front/src/modules/relation-picker/hooks/useEntitySelectLogic.ts b/front/src/modules/relation-picker/hooks/useEntitySelectLogic.ts index 9cdf982f8..f48bb4efa 100644 --- a/front/src/modules/relation-picker/hooks/useEntitySelectLogic.ts +++ b/front/src/modules/relation-picker/hooks/useEntitySelectLogic.ts @@ -1,8 +1,8 @@ import { useState } from 'react'; -import { useHotkeys } from 'react-hotkeys-hook'; import { debounce } from 'lodash'; import scrollIntoView from 'scroll-into-view'; +import { useUpDownHotkeys } from '@/hotkeys/hooks/useUpDownHotkeys'; import { useRecoilScopedState } from '@/ui/hooks/useRecoilScopedState'; import { relationPickerSearchFilterScopedState } from '../states/relationPickerSearchFilterScopedState'; @@ -34,39 +34,7 @@ export function useEntitySelectLogic< setHoveredIndex(0); } - useHotkeys( - 'down', - () => { - setHoveredIndex((prevSelectedIndex) => - Math.min(prevSelectedIndex + 1, (entities?.length ?? 0) - 1), - ); - - const currentHoveredRef = containerRef.current?.children[ - hoveredIndex - ] as HTMLElement; - - if (currentHoveredRef) { - scrollIntoView(currentHoveredRef, { - align: { - top: 0.275, - }, - isScrollable: (target) => { - return target === containerRef.current; - }, - time: 0, - }); - } - }, - { - enableOnContentEditable: true, - enableOnFormTags: true, - preventDefault: true, - }, - [setHoveredIndex, entities], - ); - - useHotkeys( - 'up', + useUpDownHotkeys( () => { setHoveredIndex((prevSelectedIndex) => Math.max(prevSelectedIndex - 1, 0), @@ -88,10 +56,26 @@ export function useEntitySelectLogic< }); } }, - { - enableOnContentEditable: true, - enableOnFormTags: true, - preventDefault: true, + () => { + setHoveredIndex((prevSelectedIndex) => + Math.min(prevSelectedIndex + 1, (entities?.length ?? 0) - 1), + ); + + const currentHoveredRef = containerRef.current?.children[ + hoveredIndex + ] as HTMLElement; + + if (currentHoveredRef) { + scrollIntoView(currentHoveredRef, { + align: { + top: 0.275, + }, + isScrollable: (target) => { + return target === containerRef.current; + }, + time: 0, + }); + } }, [setHoveredIndex, entities], ); diff --git a/front/src/modules/search/components/CommandMenu.tsx b/front/src/modules/search/components/CommandMenu.tsx deleted file mode 100644 index 4c320e125..000000000 --- a/front/src/modules/search/components/CommandMenu.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import React from 'react'; -import { useHotkeys } from 'react-hotkeys-hook'; -import { useNavigate } from 'react-router-dom'; - -import { - StyledDialog, - StyledEmpty, - StyledGroup, - StyledInput, - StyledItem, - StyledList, - // StyledSeparator, -} from './CommandMenuStyles'; - -export const CommandMenu = ({ initiallyOpen = false }) => { - const [open, setOpen] = React.useState(initiallyOpen); - - useHotkeys( - 'ctrl+k,meta+k', - () => { - setOpen((prevOpen) => !prevOpen); - }, - { - preventDefault: true, - enableOnContentEditable: true, - enableOnFormTags: true, - }, - [setOpen], - ); - - const navigate = useNavigate(); - - return ( - - - - No results found. - - - { - setOpen(false); - navigate('/people'); - }} - > - People - - { - setOpen(false); - navigate('/companies'); - }} - > - Companies - - { - setOpen(false); - navigate('/settings/profile'); - }} - > - Settings - - - - - ); -}; diff --git a/front/src/modules/ui/components/editable-cell/types/EditableRelation.tsx b/front/src/modules/ui/components/editable-cell/types/EditableRelation.tsx deleted file mode 100644 index c65ae92ed..000000000 --- a/front/src/modules/ui/components/editable-cell/types/EditableRelation.tsx +++ /dev/null @@ -1,288 +0,0 @@ -import { ChangeEvent, ComponentType, useEffect, useState } from 'react'; -import { useHotkeys } from 'react-hotkeys-hook'; -import styled from '@emotion/styled'; -import { useRecoilState } from 'recoil'; - -import { SearchConfigType } from '@/search/interfaces/interface'; -import { useSearch } from '@/search/services/search'; -import { IconPlus } from '@/ui/icons/index'; -import { textInputStyle } from '@/ui/layout/styles/themes'; -import { isSomeInputInEditModeState } from '@/ui/tables/states/isSomeInputInEditModeState'; -import { isDefined } from '@/utils/type-guards/isDefined'; -import { isNonEmptyString } from '@/utils/type-guards/isNonEmptyString'; - -import { EditableCell } from '../EditableCell'; -import { HoverableMenuItem } from '../HoverableMenuItem'; - -import { EditableRelationCreateButton } from './EditableRelationCreateButton'; - -const StyledEditModeContainer = styled.div` - width: 200px; -`; - -const StyledEditModeSelectedContainer = styled.div` - align-items: center; - display: flex; - height: 31px; - padding-left: ${(props) => props.theme.spacing(2)}; - padding-right: ${(props) => props.theme.spacing(1)}; -`; - -const StyledEditModeSearchContainer = styled.div` - align-items: center; - border-top: 1px solid ${(props) => props.theme.primaryBorder}; - display: flex; - height: 32px; - padding-left: ${(props) => props.theme.spacing(1)}; - padding-right: ${(props) => props.theme.spacing(1)}; -`; - -const StyledEditModeCreateButtonContainer = styled.div` - align-items: center; - border-top: 1px solid ${(props) => props.theme.primaryBorder}; - color: ${(props) => props.theme.text60}; - display: flex; - height: 36px; - padding: ${(props) => props.theme.spacing(1)}; -`; - -const StyledEditModeSearchInput = styled.input` - width: 100%; - - ${textInputStyle} -`; - -const StyledEditModeResults = styled.div` - border-top: 1px solid ${(props) => props.theme.primaryBorder}; - padding-left: ${(props) => props.theme.spacing(1)}; - padding-right: ${(props) => props.theme.spacing(1)}; -`; - -type StyledEditModeResultItemProps = { - isSelected: boolean; -}; - -const StyledEditModeResultItem = styled.div` - align-items: center; - cursor: pointer; - display: flex; - height: 32px; - user-select: none; - ${(props) => - props.isSelected && - ` - background-color: ${props.theme.tertiaryBackground}; -`} -`; - -const StyledCreateButtonIcon = styled.div` - align-self: center; - color: ${(props) => props.theme.text100}; - padding-top: 4px; -`; - -const StyledCreateButtonText = styled.div` - color: ${(props) => props.theme.text60}; -`; - -export type EditableRelationProps = { - relation?: any; - searchPlaceholder: string; - searchConfig: SearchConfigType; - onChange: (relation: RelationType) => void; - onChangeSearchInput?: (searchInput: string) => void; - editModeHorizontalAlign?: 'left' | 'right'; - ChipComponent: ComponentType; - chipComponentPropsMapper: ( - relation: RelationType, - ) => ChipComponentPropsType & JSX.IntrinsicAttributes; - // TODO: refactor, newRelationName is too hard coded. - onCreate?: (newRelationName: string) => void; -}; - -// TODO: split this component -export function EditableRelation({ - relation, - searchPlaceholder, - searchConfig, - onChange, - onChangeSearchInput, - editModeHorizontalAlign, - ChipComponent, - chipComponentPropsMapper, - onCreate, -}: EditableRelationProps) { - const [isEditMode, setIsEditMode] = useState(false); - const [, setIsSomeInputInEditMode] = useRecoilState( - isSomeInputInEditModeState, - ); - - // TODO: Tie this to a react context - const [filterSearchResults, setSearchInput, setFilterSearch, searchInput] = - useSearch(); - - useEffect(() => { - if (isDefined(onChangeSearchInput)) { - onChangeSearchInput(searchInput); - } - }, [onChangeSearchInput, searchInput]); - - const canCreate = isDefined(onCreate); - - const createButtonIsVisible = - canCreate && isEditMode && isNonEmptyString(searchInput); - - function handleCreateNewRelationButtonClick() { - onCreate?.(searchInput); - closeEditMode(); - } - - function closeEditMode() { - setIsEditMode(false); - setIsSomeInputInEditMode(false); - } - - const [selectedIndex, setSelectedIndex] = useState(0); - useHotkeys( - 'down', - () => { - setSelectedIndex((prevSelectedIndex) => - Math.min( - prevSelectedIndex + 1, - (filterSearchResults.results?.length ?? 0) - 1, - ), - ); - }, - { - enableOnContentEditable: true, - enableOnFormTags: true, - preventDefault: true, - }, - [setSelectedIndex, filterSearchResults.results], - ); - - useHotkeys( - 'up', - () => { - setSelectedIndex((prevSelectedIndex) => - Math.max(prevSelectedIndex - 1, 0), - ); - }, - { - enableOnContentEditable: true, - enableOnFormTags: true, - preventDefault: true, - }, - [setSelectedIndex], - ); - - useHotkeys( - 'enter', - () => { - if (isEditMode) { - if ( - filterSearchResults.results && - selectedIndex < filterSearchResults.results.length - ) { - const selectedResult = filterSearchResults.results[selectedIndex]; - onChange(selectedResult.value); - closeEditMode(); - } else if (canCreate && isNonEmptyString(searchInput)) { - onCreate(searchInput); - closeEditMode(); - } - } - }, - { - enableOnContentEditable: true, - enableOnFormTags: true, - }, - [ - filterSearchResults.results, - selectedIndex, - onChange, - closeEditMode, - canCreate, - searchInput, - onCreate, - ], - ); - - return ( - <> - setIsEditMode(false)} - onInsideClick={() => { - if (!isEditMode) { - setIsEditMode(true); - } - }} - editModeContent={ - - - {relation ? ( - - ) : ( - <> - )} - - - ) => { - setFilterSearch(searchConfig); - setSearchInput(event.target.value); - }} - /> - - {createButtonIsVisible && ( - - - - - - - Create new - - - - )} - - {filterSearchResults.results && - filterSearchResults.results.map((result, index) => ( - { - onChange(result.value); - closeEditMode(); - }} - > - - - - - ))} - - - } - nonEditModeContent={ - <> - {relation ? ( - - ) : ( - <> - )} - - } - /> - - ); -} diff --git a/front/src/modules/ui/components/table/action-bar/EntityTableActionBar.tsx b/front/src/modules/ui/components/table/action-bar/EntityTableActionBar.tsx index 86dcb3864..42cab9add 100644 --- a/front/src/modules/ui/components/table/action-bar/EntityTableActionBar.tsx +++ b/front/src/modules/ui/components/table/action-bar/EntityTableActionBar.tsx @@ -20,6 +20,7 @@ const StyledContainer = styled.div` border: 1px solid ${(props) => props.theme.primaryBorder}; border-radius: 8px; bottom: ${(props) => (props.position.x ? 'auto' : '38px')}; + box-shadow: ${(props) => props.theme.modalBoxShadow}; display: flex; height: 48px; diff --git a/front/src/modules/ui/layout/DefaultLayout.tsx b/front/src/modules/ui/layout/DefaultLayout.tsx index 37abdd518..20b4b7078 100644 --- a/front/src/modules/ui/layout/DefaultLayout.tsx +++ b/front/src/modules/ui/layout/DefaultLayout.tsx @@ -2,7 +2,7 @@ import styled from '@emotion/styled'; import { useRecoilState, useRecoilValue } from 'recoil'; import { currentUserState } from '@/auth/states/currentUserState'; -import { CommandMenu } from '@/search/components/CommandMenu'; +import { CommandMenu } from '@/command-menu/components/CommandMenu'; import { AppNavbar } from '~/AppNavbar'; import { NavbarContainer } from './navbar/NavbarContainer'; diff --git a/front/src/modules/ui/layout/styles/themes.ts b/front/src/modules/ui/layout/styles/themes.ts index 71c6d9948..fb2b1caf6 100644 --- a/front/src/modules/ui/layout/styles/themes.ts +++ b/front/src/modules/ui/layout/styles/themes.ts @@ -31,9 +31,12 @@ const lightThemeSpecific = { blueHighTransparency: 'rgba(25, 97, 237, 0.03)', blueLowTransparency: 'rgba(25, 97, 237, 0.32)', boxShadow: '0px 2px 4px 0px #0F0F0F0A', - modalBoxShadow: '0px 3px 12px rgba(0, 0, 0, 0.09)', + modalBoxShadow: + '2px 4px 16px 0px rgba(0, 0, 0, 0.12), 0px 2px 4px 0px rgba(0, 0, 0, 0.04)', lightBoxShadow: '0px 2px 4px 0px rgba(0, 0, 0, 0.04), 0px 0px 4px 0px rgba(0, 0, 0, 0.08)', + heavyBoxShadow: + '0px 16px 40px 0px rgba(0, 0, 0, 0.24), 0px 0px 12px 0px rgba(0, 0, 0, 0.24)', }; const darkThemeSpecific: typeof lightThemeSpecific = { @@ -52,6 +55,8 @@ const darkThemeSpecific: typeof lightThemeSpecific = { modalBoxShadow: '0px 3px 12px rgba(0, 0, 0, 0.09)', // TODO change color for dark theme lightBoxShadow: '0px 2px 4px 0px rgba(0, 0, 0, 0.04), 0px 0px 4px 0px rgba(0, 0, 0, 0.08)', + heavyBoxShadow: + 'box-shadow: 0px 16px 40px 0px rgba(0, 0, 0, 0.24), 0px 0px 12px 0px rgba(0, 0, 0, 0.24)', }; export const lightTheme = { ...commonTheme, ...lightThemeSpecific }; diff --git a/front/src/modules/ui/layout/top-bar/TopBar.tsx b/front/src/modules/ui/layout/top-bar/TopBar.tsx index 0c19de089..f5c434098 100644 --- a/front/src/modules/ui/layout/top-bar/TopBar.tsx +++ b/front/src/modules/ui/layout/top-bar/TopBar.tsx @@ -1,6 +1,7 @@ import { ReactNode } from 'react'; import styled from '@emotion/styled'; +import { useDirectHotkeys } from '@/hotkeys/hooks/useDirectHotkeys'; import { IconPlus } from '@/ui/icons/index'; import NavCollapseButton from '../navbar/NavCollapseButton'; @@ -49,6 +50,8 @@ type OwnProps = { }; export function TopBar({ title, icon, onAddButtonClick }: OwnProps) { + useDirectHotkeys('c', () => onAddButtonClick && onAddButtonClick()); + return ( <> diff --git a/server/src/core/pipeline/resolvers/pipeline-progress.resolver.ts b/server/src/core/pipeline/resolvers/pipeline-progress.resolver.ts index cdde43cba..fc01b4b4d 100644 --- a/server/src/core/pipeline/resolvers/pipeline-progress.resolver.ts +++ b/server/src/core/pipeline/resolvers/pipeline-progress.resolver.ts @@ -40,8 +40,6 @@ export class PipelineProgressResolver { async findManyPipelineProgress( @Args() args: FindManyPipelineProgressArgs, @UserAbility() ability: AppAbility, - @PrismaSelector({ modelName: 'PipelineProgress' }) - prismaSelect: PrismaSelect<'PipelineProgress'>, ): Promise[]> { return this.pipelineProgressService.findMany({ ...args,