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 <charles@twenty.com>
This commit is contained in:
Félix Malfait
2023-06-25 22:25:31 -07:00
committed by GitHub
parent 9bd8f6df01
commit 827d6390e4
19 changed files with 387 additions and 414 deletions

View File

@ -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 (
<DefaultLayout>
<Routes>

View File

@ -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 = (
<StyledGroup heading="Create">
<CommandMenuItem
label="Create People"
onClick={createPeople}
icon={<IconUser />}
shortcuts={
!!useMatch({
path: useResolvedPath('/people').pathname,
end: true,
})
? ['C']
: []
}
/>
<CommandMenuItem
label="Create Company"
onClick={createCompany}
icon={<IconBuildingSkyscraper />}
shortcuts={
!!useMatch({
path: useResolvedPath('/companies').pathname,
end: true,
})
? ['C']
: []
}
/>
</StyledGroup>
);*/
return (
<StyledDialog
open={open}
onOpenChange={setOpen}
label="Global Command Menu"
>
<StyledInput placeholder="Search" />
<StyledList>
<StyledEmpty>No results found.</StyledEmpty>
<StyledGroup heading="Go to">
<CommandMenuItem to="/people" label="People" shortcuts={['G', 'P']} />
<CommandMenuItem
to="/companies"
label="Companies"
shortcuts={['G', 'C']}
/>
<CommandMenuItem
to="/opportunities"
label="Opportunities"
shortcuts={['G', 'O']}
/>
<CommandMenuItem
to="/settings/profile"
label="Settings"
shortcuts={['G', 'S']}
/>
</StyledGroup>
</StyledList>
</StyledDialog>
);
}

View File

@ -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<string>;
};
export function CommandMenuItem({
label,
to,
onClick,
icon,
shortcuts,
}: OwnProps) {
const setOpen = useSetRecoilState(isCommandMenuOpenedState);
const navigate = useNavigate();
if (to) {
icon = <IconArrowUpRight />;
}
const onItemClick = () => {
setOpen(false);
if (onClick) {
onClick();
return;
}
if (to) {
navigate(to);
return;
}
};
return (
<StyledMenuItem onSelect={onItemClick}>
<StyledIconAndLabelContainer>
<StyledIconContainer>{icon}</StyledIconContainer>
{label}
</StyledIconAndLabelContainer>
<StyledShortcutsContainer>
{shortcuts &&
shortcuts.map((shortcut, index) => {
const prefix = index > 0 ? 'then' : '';
return (
<React.Fragment key={index}>
{prefix}
<StyledShortCut>{shortcut}</StyledShortCut>
</React.Fragment>
);
})}
</StyledShortcutsContainer>
</StyledMenuItem>
);
}

View File

@ -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};
`;

View File

@ -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<typeof CommandMenu> = {
title: 'Modules/Search/CommandMenu',
title: 'Modules/CommandMenu/CommandMenu',
component: CommandMenu,
};
@ -16,7 +17,22 @@ type Story = StoryObj<typeof CommandMenu>;
export const Default: Story = {
render: getRenderWrapperForComponent(
<MemoryRouter>
<CommandMenu initiallyOpen={true} />
<CommandMenu />
</MemoryRouter>,
),
};
export const CmdK: Story = {
render: getRenderWrapperForComponent(
<MemoryRouter>
<CommandMenu />
</MemoryRouter>,
),
play: async ({ canvasElement }) => {
fireEvent.keyDown(canvasElement, {
key: 'k',
code: 'KeyK',
metaKey: true,
});
},
};

View File

@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const isCommandMenuOpenedState = atom({
key: 'command-menu/isCommandMenuOpenedState',
default: false,
});

View File

@ -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);
}

View File

@ -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);
});
}

View File

@ -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],
);
}

View File

@ -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,
);
}

View File

@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const pendingHotkeyState = atom<string | null>({
key: 'command-menu/pendingHotkeyState',
default: null,
});

View File

@ -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],
);

View File

@ -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 (
<StyledDialog
open={open}
onOpenChange={setOpen}
label="Global Command Menu"
>
<StyledInput />
<StyledList>
<StyledEmpty>No results found.</StyledEmpty>
<StyledGroup heading="Go to">
<StyledItem
onSelect={() => {
setOpen(false);
navigate('/people');
}}
>
People
</StyledItem>
<StyledItem
onSelect={() => {
setOpen(false);
navigate('/companies');
}}
>
Companies
</StyledItem>
<StyledItem
onSelect={() => {
setOpen(false);
navigate('/settings/profile');
}}
>
Settings
</StyledItem>
</StyledGroup>
</StyledList>
</StyledDialog>
);
};

View File

@ -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<StyledEditModeResultItemProps>`
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<RelationType, ChipComponentPropsType> = {
relation?: any;
searchPlaceholder: string;
searchConfig: SearchConfigType;
onChange: (relation: RelationType) => void;
onChangeSearchInput?: (searchInput: string) => void;
editModeHorizontalAlign?: 'left' | 'right';
ChipComponent: ComponentType<ChipComponentPropsType>;
chipComponentPropsMapper: (
relation: RelationType,
) => ChipComponentPropsType & JSX.IntrinsicAttributes;
// TODO: refactor, newRelationName is too hard coded.
onCreate?: (newRelationName: string) => void;
};
// TODO: split this component
export function EditableRelation<RelationType, ChipComponentPropsType>({
relation,
searchPlaceholder,
searchConfig,
onChange,
onChangeSearchInput,
editModeHorizontalAlign,
ChipComponent,
chipComponentPropsMapper,
onCreate,
}: EditableRelationProps<RelationType, ChipComponentPropsType>) {
const [isEditMode, setIsEditMode] = useState(false);
const [, setIsSomeInputInEditMode] = useRecoilState(
isSomeInputInEditModeState,
);
// TODO: Tie this to a react context
const [filterSearchResults, setSearchInput, setFilterSearch, searchInput] =
useSearch<RelationType>();
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 (
<>
<EditableCell
editModeHorizontalAlign={editModeHorizontalAlign}
isEditMode={isEditMode}
onOutsideClick={() => setIsEditMode(false)}
onInsideClick={() => {
if (!isEditMode) {
setIsEditMode(true);
}
}}
editModeContent={
<StyledEditModeContainer>
<StyledEditModeSelectedContainer>
{relation ? (
<ChipComponent {...chipComponentPropsMapper(relation)} />
) : (
<></>
)}
</StyledEditModeSelectedContainer>
<StyledEditModeSearchContainer>
<StyledEditModeSearchInput
autoFocus
placeholder={searchPlaceholder}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
setFilterSearch(searchConfig);
setSearchInput(event.target.value);
}}
/>
</StyledEditModeSearchContainer>
{createButtonIsVisible && (
<StyledEditModeCreateButtonContainer>
<HoverableMenuItem>
<EditableRelationCreateButton
onClick={handleCreateNewRelationButtonClick}
>
<StyledCreateButtonIcon>
<IconPlus />
</StyledCreateButtonIcon>
<StyledCreateButtonText>Create new</StyledCreateButtonText>
</EditableRelationCreateButton>
</HoverableMenuItem>
</StyledEditModeCreateButtonContainer>
)}
<StyledEditModeResults>
{filterSearchResults.results &&
filterSearchResults.results.map((result, index) => (
<StyledEditModeResultItem
key={index}
isSelected={index === selectedIndex}
onClick={() => {
onChange(result.value);
closeEditMode();
}}
>
<HoverableMenuItem>
<ChipComponent
{...chipComponentPropsMapper(result.value)}
/>
</HoverableMenuItem>
</StyledEditModeResultItem>
))}
</StyledEditModeResults>
</StyledEditModeContainer>
}
nonEditModeContent={
<>
{relation ? (
<ChipComponent {...chipComponentPropsMapper(relation)} />
) : (
<></>
)}
</>
}
/>
</>
);
}

View File

@ -20,6 +20,7 @@ const StyledContainer = styled.div<StyledContainerProps>`
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;

View File

@ -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';

View File

@ -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 };

View File

@ -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 (
<>
<TopBarContainer>

View File

@ -40,8 +40,6 @@ export class PipelineProgressResolver {
async findManyPipelineProgress(
@Args() args: FindManyPipelineProgressArgs,
@UserAbility() ability: AppAbility,
@PrismaSelector({ modelName: 'PipelineProgress' })
prismaSelect: PrismaSelect<'PipelineProgress'>,
): Promise<Partial<PipelineProgress>[]> {
return this.pipelineProgressService.findMany({
...args,