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:
@ -2,6 +2,7 @@ import { Navigate, Route, Routes } from 'react-router-dom';
|
|||||||
|
|
||||||
import { RequireAuth } from '@/auth/components/RequireAuth';
|
import { RequireAuth } from '@/auth/components/RequireAuth';
|
||||||
import { RequireNotAuth } from '@/auth/components/RequireNotAuth';
|
import { RequireNotAuth } from '@/auth/components/RequireNotAuth';
|
||||||
|
import { useGoToHotkeys } from '@/hotkeys/hooks/useGoToHotkeys';
|
||||||
import { DefaultLayout } from '@/ui/layout/DefaultLayout';
|
import { DefaultLayout } from '@/ui/layout/DefaultLayout';
|
||||||
import { Index } from '~/pages/auth/Index';
|
import { Index } from '~/pages/auth/Index';
|
||||||
import { PasswordLogin } from '~/pages/auth/PasswordLogin';
|
import { PasswordLogin } from '~/pages/auth/PasswordLogin';
|
||||||
@ -12,6 +13,11 @@ import { People } from '~/pages/people/People';
|
|||||||
import { SettingsProfile } from '~/pages/settings/SettingsProfile';
|
import { SettingsProfile } from '~/pages/settings/SettingsProfile';
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
|
useGoToHotkeys('p', '/people');
|
||||||
|
useGoToHotkeys('c', '/companies');
|
||||||
|
useGoToHotkeys('o', '/opportunities');
|
||||||
|
useGoToHotkeys('s', '/settings/profile');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DefaultLayout>
|
<DefaultLayout>
|
||||||
<Routes>
|
<Routes>
|
||||||
|
|||||||
96
front/src/modules/command-menu/components/CommandMenu.tsx
Normal file
96
front/src/modules/command-menu/components/CommandMenu.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -4,7 +4,7 @@ import { Command } from 'cmdk';
|
|||||||
export const StyledDialog = styled(Command.Dialog)`
|
export const StyledDialog = styled(Command.Dialog)`
|
||||||
background: ${(props) => props.theme.primaryBackground};
|
background: ${(props) => props.theme.primaryBackground};
|
||||||
border-radius: ${(props) => props.theme.borderRadius};
|
border-radius: ${(props) => props.theme.borderRadius};
|
||||||
box-shadow: ${(props) => props.theme.modalBoxShadow};
|
box-shadow: ${(props) => props.theme.heavyBoxShadow};
|
||||||
font-family: ${(props) => props.theme.fontFamily};
|
font-family: ${(props) => props.theme.fontFamily};
|
||||||
left: 50%;
|
left: 50%;
|
||||||
max-width: 640px;
|
max-width: 640px;
|
||||||
@ -21,7 +21,6 @@ export const StyledInput = styled(Command.Input)`
|
|||||||
border: none;
|
border: none;
|
||||||
border-bottom: 1px solid ${(props) => props.theme.primaryBorder};
|
border-bottom: 1px solid ${(props) => props.theme.primaryBorder};
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
caret-color: ${(props) => props.theme.blue};
|
|
||||||
color: ${(props) => props.theme.text100};
|
color: ${(props) => props.theme.text100};
|
||||||
font-size: ${(props) => props.theme.fontSizeLarge};
|
font-size: ${(props) => props.theme.fontSizeLarge};
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@ -30,14 +29,15 @@ export const StyledInput = styled(Command.Input)`
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const StyledItem = styled(Command.Item)`
|
export const StyledMenuItem = styled(Command.Item)`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: ${(props) => props.theme.text100};
|
color: ${(props) => props.theme.text80};
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
font-size: ${(props) => props.theme.fontSizeMedium};
|
font-size: ${(props) => props.theme.fontSizeMedium};
|
||||||
gap: ${(props) => props.theme.spacing(3)};
|
gap: ${(props) => props.theme.spacing(3)};
|
||||||
height: 48px;
|
height: 40px;
|
||||||
|
justify-content: space-between;
|
||||||
padding: 0 ${(props) => props.theme.spacing(4)};
|
padding: 0 ${(props) => props.theme.spacing(4)};
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: all 150ms ease;
|
transition: all 150ms ease;
|
||||||
@ -47,23 +47,24 @@ export const StyledItem = styled(Command.Item)`
|
|||||||
background: ${(props) => props.theme.lightBackgroundTransparent};
|
background: ${(props) => props.theme.lightBackgroundTransparent};
|
||||||
}
|
}
|
||||||
&[data-selected='true'] {
|
&[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 {
|
&:after {
|
||||||
background: ${(props) => props.theme.blue};
|
background: ${(props) => props.theme.quaternaryBackground};
|
||||||
content: '';
|
content: '';
|
||||||
height: 100%;
|
height: 100%;
|
||||||
left: 0;
|
left: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 3px;
|
width: 3px;
|
||||||
z-index: ${(props) => props.theme.lastLayerZIndex};
|
z-index: ${(props) => props.theme.lastLayerZIndex};
|
||||||
}
|
} */
|
||||||
}
|
}
|
||||||
&[data-disabled='true'] {
|
&[data-disabled='true'] {
|
||||||
color: ${(props) => props.theme.text30};
|
color: ${(props) => props.theme.text30};
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
svg {
|
svg {
|
||||||
color: ${(props) => props.theme.text80};
|
|
||||||
height: 16px;
|
height: 16px;
|
||||||
width: 16px;
|
width: 16px;
|
||||||
}
|
}
|
||||||
@ -85,7 +86,12 @@ export const StyledGroup = styled(Command.Group)`
|
|||||||
color: ${(props) => props.theme.text30};
|
color: ${(props) => props.theme.text30};
|
||||||
display: flex;
|
display: flex;
|
||||||
font-size: ${(props) => props.theme.fontSizeExtraSmall};
|
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;
|
user-select: none;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@ -101,3 +107,32 @@ export const StyledEmpty = styled(Command.Empty)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const StyledSeparator = styled(Command.Separator)``;
|
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};
|
||||||
|
`;
|
||||||
@ -1,12 +1,13 @@
|
|||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import type { Meta, StoryObj } from '@storybook/react';
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { fireEvent } from '@storybook/testing-library';
|
||||||
|
|
||||||
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
|
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
|
||||||
|
|
||||||
import { CommandMenu } from '../CommandMenu';
|
import { CommandMenu } from '../CommandMenu';
|
||||||
|
|
||||||
const meta: Meta<typeof CommandMenu> = {
|
const meta: Meta<typeof CommandMenu> = {
|
||||||
title: 'Modules/Search/CommandMenu',
|
title: 'Modules/CommandMenu/CommandMenu',
|
||||||
component: CommandMenu,
|
component: CommandMenu,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -16,7 +17,22 @@ type Story = StoryObj<typeof CommandMenu>;
|
|||||||
export const Default: Story = {
|
export const Default: Story = {
|
||||||
render: getRenderWrapperForComponent(
|
render: getRenderWrapperForComponent(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<CommandMenu initiallyOpen={true} />
|
<CommandMenu />
|
||||||
</MemoryRouter>,
|
</MemoryRouter>,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const CmdK: Story = {
|
||||||
|
render: getRenderWrapperForComponent(
|
||||||
|
<MemoryRouter>
|
||||||
|
<CommandMenu />
|
||||||
|
</MemoryRouter>,
|
||||||
|
),
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
fireEvent.keyDown(canvasElement, {
|
||||||
|
key: 'k',
|
||||||
|
code: 'KeyK',
|
||||||
|
metaKey: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
import { atom } from 'recoil';
|
||||||
|
|
||||||
|
export const isCommandMenuOpenedState = atom({
|
||||||
|
key: 'command-menu/isCommandMenuOpenedState',
|
||||||
|
default: false,
|
||||||
|
});
|
||||||
30
front/src/modules/hotkeys/hooks/useDirectHotkeys.ts
Normal file
30
front/src/modules/hotkeys/hooks/useDirectHotkeys.ts
Normal 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);
|
||||||
|
}
|
||||||
11
front/src/modules/hotkeys/hooks/useGoToHotkeys.ts
Normal file
11
front/src/modules/hotkeys/hooks/useGoToHotkeys.ts
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
32
front/src/modules/hotkeys/hooks/useSequenceHotkeys.ts
Normal file
32
front/src/modules/hotkeys/hooks/useSequenceHotkeys.ts
Normal 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],
|
||||||
|
);
|
||||||
|
}
|
||||||
31
front/src/modules/hotkeys/hooks/useUpDownHotkeys.ts
Normal file
31
front/src/modules/hotkeys/hooks/useUpDownHotkeys.ts
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
6
front/src/modules/hotkeys/states/pendingHotkeysState.ts
Normal file
6
front/src/modules/hotkeys/states/pendingHotkeysState.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { atom } from 'recoil';
|
||||||
|
|
||||||
|
export const pendingHotkeyState = atom<string | null>({
|
||||||
|
key: 'command-menu/pendingHotkeyState',
|
||||||
|
default: null,
|
||||||
|
});
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
import scrollIntoView from 'scroll-into-view';
|
import scrollIntoView from 'scroll-into-view';
|
||||||
|
|
||||||
|
import { useUpDownHotkeys } from '@/hotkeys/hooks/useUpDownHotkeys';
|
||||||
import { useRecoilScopedState } from '@/ui/hooks/useRecoilScopedState';
|
import { useRecoilScopedState } from '@/ui/hooks/useRecoilScopedState';
|
||||||
|
|
||||||
import { relationPickerSearchFilterScopedState } from '../states/relationPickerSearchFilterScopedState';
|
import { relationPickerSearchFilterScopedState } from '../states/relationPickerSearchFilterScopedState';
|
||||||
@ -34,39 +34,7 @@ export function useEntitySelectLogic<
|
|||||||
setHoveredIndex(0);
|
setHoveredIndex(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
useHotkeys(
|
useUpDownHotkeys(
|
||||||
'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',
|
|
||||||
() => {
|
() => {
|
||||||
setHoveredIndex((prevSelectedIndex) =>
|
setHoveredIndex((prevSelectedIndex) =>
|
||||||
Math.max(prevSelectedIndex - 1, 0),
|
Math.max(prevSelectedIndex - 1, 0),
|
||||||
@ -88,10 +56,26 @@ export function useEntitySelectLogic<
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
() => {
|
||||||
enableOnContentEditable: true,
|
setHoveredIndex((prevSelectedIndex) =>
|
||||||
enableOnFormTags: true,
|
Math.min(prevSelectedIndex + 1, (entities?.length ?? 0) - 1),
|
||||||
preventDefault: true,
|
);
|
||||||
|
|
||||||
|
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],
|
[setHoveredIndex, entities],
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -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)} />
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -20,6 +20,7 @@ const StyledContainer = styled.div<StyledContainerProps>`
|
|||||||
border: 1px solid ${(props) => props.theme.primaryBorder};
|
border: 1px solid ${(props) => props.theme.primaryBorder};
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
bottom: ${(props) => (props.position.x ? 'auto' : '38px')};
|
bottom: ${(props) => (props.position.x ? 'auto' : '38px')};
|
||||||
|
box-shadow: ${(props) => props.theme.modalBoxShadow};
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import styled from '@emotion/styled';
|
|||||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
import { currentUserState } from '@/auth/states/currentUserState';
|
import { currentUserState } from '@/auth/states/currentUserState';
|
||||||
import { CommandMenu } from '@/search/components/CommandMenu';
|
import { CommandMenu } from '@/command-menu/components/CommandMenu';
|
||||||
import { AppNavbar } from '~/AppNavbar';
|
import { AppNavbar } from '~/AppNavbar';
|
||||||
|
|
||||||
import { NavbarContainer } from './navbar/NavbarContainer';
|
import { NavbarContainer } from './navbar/NavbarContainer';
|
||||||
|
|||||||
@ -31,9 +31,12 @@ const lightThemeSpecific = {
|
|||||||
blueHighTransparency: 'rgba(25, 97, 237, 0.03)',
|
blueHighTransparency: 'rgba(25, 97, 237, 0.03)',
|
||||||
blueLowTransparency: 'rgba(25, 97, 237, 0.32)',
|
blueLowTransparency: 'rgba(25, 97, 237, 0.32)',
|
||||||
boxShadow: '0px 2px 4px 0px #0F0F0F0A',
|
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:
|
lightBoxShadow:
|
||||||
'0px 2px 4px 0px rgba(0, 0, 0, 0.04), 0px 0px 4px 0px rgba(0, 0, 0, 0.08)',
|
'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 = {
|
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
|
modalBoxShadow: '0px 3px 12px rgba(0, 0, 0, 0.09)', // TODO change color for dark theme
|
||||||
lightBoxShadow:
|
lightBoxShadow:
|
||||||
'0px 2px 4px 0px rgba(0, 0, 0, 0.04), 0px 0px 4px 0px rgba(0, 0, 0, 0.08)',
|
'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 };
|
export const lightTheme = { ...commonTheme, ...lightThemeSpecific };
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
import { useDirectHotkeys } from '@/hotkeys/hooks/useDirectHotkeys';
|
||||||
import { IconPlus } from '@/ui/icons/index';
|
import { IconPlus } from '@/ui/icons/index';
|
||||||
|
|
||||||
import NavCollapseButton from '../navbar/NavCollapseButton';
|
import NavCollapseButton from '../navbar/NavCollapseButton';
|
||||||
@ -49,6 +50,8 @@ type OwnProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function TopBar({ title, icon, onAddButtonClick }: OwnProps) {
|
export function TopBar({ title, icon, onAddButtonClick }: OwnProps) {
|
||||||
|
useDirectHotkeys('c', () => onAddButtonClick && onAddButtonClick());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TopBarContainer>
|
<TopBarContainer>
|
||||||
|
|||||||
@ -40,8 +40,6 @@ export class PipelineProgressResolver {
|
|||||||
async findManyPipelineProgress(
|
async findManyPipelineProgress(
|
||||||
@Args() args: FindManyPipelineProgressArgs,
|
@Args() args: FindManyPipelineProgressArgs,
|
||||||
@UserAbility() ability: AppAbility,
|
@UserAbility() ability: AppAbility,
|
||||||
@PrismaSelector({ modelName: 'PipelineProgress' })
|
|
||||||
prismaSelect: PrismaSelect<'PipelineProgress'>,
|
|
||||||
): Promise<Partial<PipelineProgress>[]> {
|
): Promise<Partial<PipelineProgress>[]> {
|
||||||
return this.pipelineProgressService.findMany({
|
return this.pipelineProgressService.findMany({
|
||||||
...args,
|
...args,
|
||||||
|
|||||||
Reference in New Issue
Block a user