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:
@ -80,7 +80,7 @@ export function DropdownButton({
|
||||
dropdownHotkeyScope,
|
||||
dropdownButtonCustomHotkeyScope,
|
||||
]);
|
||||
|
||||
console.log(dropdownComponents, isDropdownButtonOpen);
|
||||
return (
|
||||
<StyledContainer ref={containerRef}>
|
||||
{hotkey && (
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export const ShowPageRecoilScopeContext = createContext<string | null>(null);
|
||||
@ -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}
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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' }),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@ -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();
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user