Show Entity task/note tabs (#1282)

* - show task tab
- tab bar

* - add notes tab

* - fixed unused style

* - add button
- fixed company edit note test

* - fixed merge & dropdown

* - added Tests
- refactored directory structure activities
- moved Task/Note Pages to corresponding modules
- fixed TabList

* lint
This commit is contained in:
brendanlaschke
2023-08-25 22:44:13 +02:00
committed by GitHub
parent f8e3dd3f6b
commit 7e264565ef
34 changed files with 957 additions and 188 deletions

View File

@ -80,7 +80,7 @@ export function DropdownButton({
dropdownHotkeyScope,
dropdownButtonCustomHotkeyScope,
]);
console.log(dropdownComponents, isDropdownButtonOpen);
return (
<StyledContainer ref={containerRef}>
{hotkey && (

View File

@ -14,6 +14,7 @@ type OwnProps = {
icon: ReactNode;
onAddButtonClick?: () => void;
onFavoriteButtonClick?: () => void;
extraButtons?: ReactNode[];
};
const StyledContainer = styled.div`
@ -30,6 +31,7 @@ export function WithTopBarContainer({
icon,
onAddButtonClick,
onFavoriteButtonClick,
extraButtons,
}: OwnProps) {
return (
<StyledContainer>
@ -41,6 +43,7 @@ export function WithTopBarContainer({
icon={icon}
onAddButtonClick={onAddButtonClick}
onFavoriteButtonClick={onFavoriteButtonClick}
extraButtons={extraButtons}
/>
<RightDrawerContainer topMargin={PAGE_BAR_MIN_HEIGHT + 16 + 16}>
{children}

View File

@ -4,8 +4,10 @@ import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { IconButton } from '@/ui/button/components/IconButton';
import { DropdownRecoilScopeContext } from '@/ui/dropdown/states/recoil-scope-contexts/DropdownRecoilScopeContext';
import { IconChevronLeft, IconHeart, IconPlus } from '@/ui/icon/index';
import NavCollapseButton from '@/ui/navbar/components/NavCollapseButton';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { navbarIconSize } from '../../../navbar/constants';
@ -70,6 +72,7 @@ type OwnProps = {
icon: ReactNode;
onAddButtonClick?: () => void;
onFavoriteButtonClick?: () => void;
extraButtons?: ReactNode[];
};
export function PageBar({
@ -79,6 +82,7 @@ export function PageBar({
icon,
onAddButtonClick,
onFavoriteButtonClick,
extraButtons,
}: OwnProps) {
const navigate = useNavigate();
const navigateBack = useCallback(() => navigate(-1), [navigate]);
@ -113,28 +117,31 @@ export function PageBar({
</StyledTitleContainer>
</StyledTopBarIconTitleContainer>
</StyledLeftContainer>
<StyledActionButtonsContainer>
{onFavoriteButtonClick && (
<IconButton
icon={<IconHeart size={16} />}
size="large"
data-testid="add-button"
textColor={isFavorite ? 'danger' : 'secondary'}
onClick={onFavoriteButtonClick}
variant="border"
/>
)}
{onAddButtonClick && (
<IconButton
icon={<IconPlus size={16} />}
size="large"
data-testid="add-button"
textColor="secondary"
onClick={onAddButtonClick}
variant="border"
/>
)}
</StyledActionButtonsContainer>
<RecoilScope SpecificContext={DropdownRecoilScopeContext}>
<StyledActionButtonsContainer>
{onFavoriteButtonClick && (
<IconButton
icon={<IconHeart size={16} />}
size="large"
data-testid="add-button"
textColor={isFavorite ? 'danger' : 'secondary'}
onClick={onFavoriteButtonClick}
variant="border"
/>
)}
{onAddButtonClick && (
<IconButton
icon={<IconPlus size={16} />}
size="large"
data-testid="add-button"
textColor="secondary"
onClick={onAddButtonClick}
variant="border"
/>
)}
{extraButtons}
</StyledActionButtonsContainer>
</RecoilScope>
</StyledTopBarContainer>
</>
);

View File

@ -0,0 +1,77 @@
import styled from '@emotion/styled';
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
import { ActivityTargetableEntity } from '@/activities/types/ActivityTargetableEntity';
import { IconButton } from '@/ui/button/components/IconButton';
import { DropdownButton } from '@/ui/dropdown/components/DropdownButton';
import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem';
import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu';
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { useDropdownButton } from '@/ui/dropdown/hooks/useDropdownButton';
import { IconCheckbox, IconNotes, IconPlus } from '@/ui/icon/index';
import { RelationPickerHotkeyScope } from '@/ui/input/relation-picker/types/RelationPickerHotkeyScope';
import { ActivityType } from '~/generated/graphql';
const StyledContainer = styled.div`
z-index: 1;
`;
export function ShowPageAddButton({
entity,
}: {
entity: ActivityTargetableEntity;
}) {
const { closeDropdownButton, toggleDropdownButton } = useDropdownButton({
key: 'add-show-page',
});
const openCreateActivity = useOpenCreateActivityDrawer();
function handleSelect(type: ActivityType) {
console.log(type, entity);
openCreateActivity(type, [entity]);
closeDropdownButton();
}
return (
<StyledContainer>
<DropdownButton
dropdownKey="add-show-page"
buttonComponents={
<IconButton
icon={<IconPlus size={16} />}
size="large"
data-testid="add-showpage-button"
textColor={'secondary'}
variant="border"
onClick={toggleDropdownButton}
/>
}
dropdownComponents={
<StyledDropdownMenu>
<StyledDropdownMenuItemsContainer
onClick={(e) => e.stopPropagation()}
>
<DropdownMenuItem
onClick={() => handleSelect(ActivityType.Note)}
accent="regular"
>
<IconNotes size={16} />
Note
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleSelect(ActivityType.Note)}
accent="regular"
>
<IconCheckbox size={16} />
Task
</DropdownMenuItem>
</StyledDropdownMenuItemsContainer>
</StyledDropdownMenu>
}
dropdownHotkeyScope={{
scope: RelationPickerHotkeyScope.RelationPicker,
}}
/>
</StyledContainer>
);
}

View File

@ -1,9 +1,24 @@
import { ReactElement } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { Notes } from '@/activities/notes/components/Notes';
import { EntityTasks } from '@/activities/tasks/components/EntityTasks';
import { Timeline } from '@/activities/timeline/components/Timeline';
import { ActivityTargetableEntity } from '@/activities/types/ActivityTargetableEntity';
import {
IconCheckbox,
IconMail,
IconNotes,
IconTimelineEvent,
} from '@/ui/icon';
import { TabList } from '@/ui/tab/components/TabList';
import { activeTabIdScopedState } from '@/ui/tab/states/activeTabIdScopedState';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
export const StyledShowPageRightContainer = styled.div`
import { ShowPageRecoilScopeContext } from '../../states/ShowPageRecoilScopeContext';
const StyledShowPageRightContainer = styled.div`
display: flex;
flex: 1 0 0;
flex-direction: column;
@ -12,14 +27,75 @@ export const StyledShowPageRightContainer = styled.div`
width: calc(100% + 4px);
`;
export type ShowPageRightContainerProps = {
children: ReactElement;
const StyledTabListContainer = styled.div`
align-items: end;
border-bottom: ${({ theme }) => `1px solid ${theme.border.color.light}`};
box-sizing: border-box;
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
height: 40px;
padding-left: ${({ theme }) => theme.spacing(1)};
padding-right: ${({ theme }) => theme.spacing(1)};
`;
type OwnProps = {
entity: ActivityTargetableEntity;
timeline?: boolean;
tasks?: boolean;
notes?: boolean;
emails?: boolean;
};
export function ShowPageRightContainer({
children,
}: ShowPageRightContainerProps) {
entity,
timeline,
tasks,
notes,
emails,
}: OwnProps) {
const theme = useTheme();
const TASK_TABS = [
{
id: 'timeline',
title: 'Timeline',
icon: <IconTimelineEvent size={theme.icon.size.md} />,
hide: !timeline,
},
{
id: 'tasks',
title: 'Tasks',
icon: <IconCheckbox size={theme.icon.size.md} />,
hide: !tasks,
},
{
id: 'notes',
title: 'Notes',
icon: <IconNotes size={theme.icon.size.md} />,
hide: !notes,
},
{
id: 'emails',
title: 'Emails',
icon: <IconMail size={theme.icon.size.md} />,
hide: !emails,
disabled: true,
},
];
const [activeTabId] = useRecoilScopedState(
activeTabIdScopedState,
ShowPageRecoilScopeContext,
);
return (
<StyledShowPageRightContainer>{children} </StyledShowPageRightContainer>
<StyledShowPageRightContainer>
<StyledTabListContainer>
<TabList context={ShowPageRecoilScopeContext} tabs={TASK_TABS} />
</StyledTabListContainer>
{activeTabId === 'timeline' && <Timeline entity={entity} />}
{activeTabId === 'tasks' && <EntityTasks entity={entity} />}
{activeTabId === 'notes' && <Notes entity={entity} />}
</StyledShowPageRightContainer>
);
}

View File

@ -0,0 +1,3 @@
import { createContext } from 'react';
export const ShowPageRecoilScopeContext = createContext<string | null>(null);

View File

@ -2,26 +2,33 @@ import * as React from 'react';
import styled from '@emotion/styled';
type OwnProps = {
id: string;
title: string;
icon?: React.ReactNode;
active?: boolean;
className?: string;
onClick?: () => void;
disabled?: boolean;
};
const StyledTab = styled.div<{ active?: boolean }>`
const StyledTab = styled.div<{ active?: boolean; disabled?: boolean }>`
align-items: center;
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
border-color: ${({ theme, active }) =>
active ? theme.border.color.inverted : 'transparent'};
color: ${({ theme, active }) =>
active ? theme.font.color.primary : theme.font.color.secondary};
color: ${({ theme, active, disabled }) =>
active
? theme.font.color.primary
: disabled
? theme.font.color.light
: theme.font.color.secondary};
cursor: pointer;
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
justify-content: center;
padding: ${({ theme }) => theme.spacing(2) + ' ' + theme.spacing(2)};
pointer-events: ${({ disabled }) => (disabled ? 'none' : '')};
`;
const StyledHover = styled.span`
@ -38,14 +45,22 @@ const StyledHover = styled.span`
`;
export function Tab({
id,
title,
icon,
active = false,
onClick,
className,
disabled,
}: OwnProps) {
return (
<StyledTab onClick={onClick} active={active} className={className}>
<StyledTab
onClick={onClick}
active={active}
className={className}
disabled={disabled}
data-testid={'tab-' + id}
>
<StyledHover>
{icon}
{title}

View File

@ -10,6 +10,8 @@ type SingleTabProps = {
title: string;
icon?: React.ReactNode;
id: string;
hide?: boolean;
disabled?: boolean;
};
type OwnProps = {
@ -31,17 +33,21 @@ export function TabList({ tabs, context }: OwnProps) {
return (
<>
{tabs.map((tab) => (
<Tab
key={tab.id}
title={tab.title}
icon={tab.icon}
active={tab.id === activeTabId}
onClick={() => {
setActiveTabId(tab.id);
}}
/>
))}
{tabs
.filter((tab) => !tab.hide)
.map((tab) => (
<Tab
id={tab.id}
key={tab.id}
title={tab.title}
icon={tab.icon}
active={tab.id === activeTabId}
onClick={() => {
setActiveTabId(tab.id);
}}
disabled={tab.disabled}
/>
))}
</>
);
}

View File

@ -26,6 +26,7 @@ export const Catalog: Story = {
args: { title: 'Tab title', icon: <IconCheckbox /> },
argTypes: {
active: { control: false },
disabled: { control: false },
onClick: { control: false },
},
parameters: {
@ -43,6 +44,11 @@ export const Catalog: Story = {
values: ['true', 'false'],
props: (active: string) => ({ active: active === 'true' }),
},
{
name: 'Disabled',
values: ['true', 'false'],
props: (disabled: string) => ({ disabled: disabled === 'true' }),
},
],
},
},

View File

@ -0,0 +1,69 @@
import { expect } from '@storybook/jest';
import type { Meta, StoryObj } from '@storybook/react';
import { within } from '@storybook/testing-library';
import { IconCheckbox } from '@tabler/icons-react';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { TabList } from '../TabList';
const tabs = [
{
id: '1',
title: 'Tab1',
icon: <IconCheckbox size={16} />,
hide: true,
},
{
id: '2',
title: 'Tab2',
icon: <IconCheckbox size={16} />,
hide: false,
},
{
id: '3',
title: 'Tab3',
icon: <IconCheckbox size={16} />,
hide: false,
disabled: true,
},
{
id: '4',
title: 'Tab4',
icon: <IconCheckbox size={16} />,
hide: false,
disabled: false,
},
];
const meta: Meta<typeof TabList> = {
title: 'UI/Tab/TabList',
component: TabList,
args: {
tabs: tabs,
},
decorators: [
(Story) => (
<RecoilScope>
<Story />
</RecoilScope>
),
ComponentDecorator,
],
};
export default meta;
type Story = StoryObj<typeof TabList>;
export const TabListDisplay: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const submitButton = canvas.queryByText('Tab1');
expect(submitButton).toBeNull();
expect(await canvas.findByText('Tab2')).toBeInTheDocument();
expect(await canvas.findByText('Tab3')).toBeInTheDocument();
expect(await canvas.findByText('Tab4')).toBeInTheDocument();
},
};