Add tasks page (#1015)
* Refactor top bar component * Add task page with tabs * Add tasks * Add logic for task status * Fix isoweek definition * Enable click on task * Deduplicate component * Lint --------- Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
This commit is contained in:
@ -6,6 +6,7 @@ import SortAndFilterBar from '@/ui/filter-n-sort/components/SortAndFilterBar';
|
||||
import { SortDropdownButton } from '@/ui/filter-n-sort/components/SortDropdownButton';
|
||||
import { FiltersHotkeyScope } from '@/ui/filter-n-sort/types/FiltersHotkeyScope';
|
||||
import { SelectedSortType, SortType } from '@/ui/filter-n-sort/types/interface';
|
||||
import { TopBar } from '@/ui/top-bar/TopBar';
|
||||
|
||||
type OwnProps<SortField> = {
|
||||
viewName: string;
|
||||
@ -15,24 +16,6 @@ type OwnProps<SortField> = {
|
||||
context: Context<string | null>;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const StyledBoardHeader = styled.div`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
height: 40px;
|
||||
justify-content: space-between;
|
||||
padding-left: ${({ theme }) => theme.spacing(3)};
|
||||
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledIcon = styled.div`
|
||||
display: flex;
|
||||
margin-left: ${({ theme }) => theme.spacing(1)};
|
||||
@ -43,16 +26,6 @@ const StyledIcon = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledViewSection = styled.div`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const StyledFilters = styled.div`
|
||||
display: flex;
|
||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||
gap: 2px;
|
||||
`;
|
||||
|
||||
export function BoardHeader<SortField>({
|
||||
viewName,
|
||||
viewIcon,
|
||||
@ -83,35 +56,37 @@ export function BoardHeader<SortField>({
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledBoardHeader>
|
||||
<StyledViewSection>
|
||||
<TopBar
|
||||
leftComponent={
|
||||
<>
|
||||
<StyledIcon>{viewIcon}</StyledIcon>
|
||||
{viewName}
|
||||
</StyledViewSection>
|
||||
<StyledFilters>
|
||||
<FilterDropdownButton
|
||||
context={context}
|
||||
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
|
||||
/>
|
||||
<SortDropdownButton<SortField>
|
||||
isSortSelected={sorts.length > 0}
|
||||
availableSorts={availableSorts || []}
|
||||
onSortSelect={sortSelect}
|
||||
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
|
||||
/>
|
||||
</StyledFilters>
|
||||
</StyledBoardHeader>
|
||||
<SortAndFilterBar
|
||||
context={context}
|
||||
sorts={sorts}
|
||||
onRemoveSort={sortUnselect}
|
||||
onCancelClick={() => {
|
||||
innerSetSorts([]);
|
||||
onSortsUpdate && onSortsUpdate([]);
|
||||
}}
|
||||
/>
|
||||
</StyledContainer>
|
||||
</>
|
||||
}
|
||||
rightComponents={[
|
||||
<FilterDropdownButton
|
||||
context={context}
|
||||
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
|
||||
/>,
|
||||
<SortDropdownButton<SortField>
|
||||
isSortSelected={sorts.length > 0}
|
||||
availableSorts={availableSorts || []}
|
||||
onSortSelect={sortSelect}
|
||||
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
|
||||
/>,
|
||||
]}
|
||||
bottomComponent={
|
||||
<SortAndFilterBar
|
||||
context={context}
|
||||
sorts={sorts}
|
||||
onRemoveSort={sortUnselect}
|
||||
onCancelClick={() => {
|
||||
innerSetSorts([]);
|
||||
onSortsUpdate && onSortsUpdate([]);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@ export { IconUser } from '@tabler/icons-react';
|
||||
export { IconList } from '@tabler/icons-react';
|
||||
export { IconInbox } from '@tabler/icons-react';
|
||||
export { IconSearch } from '@tabler/icons-react';
|
||||
export { IconArchive } from '@tabler/icons-react';
|
||||
export { IconSettings } from '@tabler/icons-react';
|
||||
export { IconLogout } from '@tabler/icons-react';
|
||||
export { IconColorSwatch } from '@tabler/icons-react';
|
||||
|
||||
@ -3,6 +3,7 @@ import styled from '@emotion/styled';
|
||||
|
||||
type OwnProps = {
|
||||
title: string;
|
||||
icon?: React.ReactNode;
|
||||
active?: boolean;
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
@ -28,10 +29,16 @@ const StyledTab = styled.div<{ active?: boolean }>`
|
||||
}
|
||||
`;
|
||||
|
||||
export function Tab({ title, active = false, onClick, className }: OwnProps) {
|
||||
export function Tab({
|
||||
title,
|
||||
icon,
|
||||
active = false,
|
||||
onClick,
|
||||
className,
|
||||
}: OwnProps) {
|
||||
return (
|
||||
<StyledTab onClick={onClick} active={active} className={className}>
|
||||
{title}
|
||||
{icon} {title}
|
||||
</StyledTab>
|
||||
);
|
||||
}
|
||||
|
||||
47
front/src/modules/ui/tab/components/TabList.tsx
Normal file
47
front/src/modules/ui/tab/components/TabList.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||
|
||||
import { activeTabIdScopedState } from '../states/activeTabIdScopedState';
|
||||
|
||||
import { Tab } from './Tab';
|
||||
|
||||
type SingleTabProps = {
|
||||
title: string;
|
||||
icon?: React.ReactNode;
|
||||
id: string;
|
||||
};
|
||||
|
||||
type OwnProps = {
|
||||
tabs: SingleTabProps[];
|
||||
context: React.Context<string | null>;
|
||||
};
|
||||
|
||||
export function TabList({ tabs, context }: OwnProps) {
|
||||
const initialActiveTabId = tabs[0].id;
|
||||
|
||||
const [activeTabId, setActiveTabId] = useRecoilScopedState(
|
||||
activeTabIdScopedState,
|
||||
context,
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
setActiveTabId(initialActiveTabId);
|
||||
}, [initialActiveTabId, setActiveTabId]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{tabs.map((tab) => (
|
||||
<Tab
|
||||
key={tab.id}
|
||||
title={tab.title}
|
||||
icon={tab.icon}
|
||||
active={tab.id === activeTabId}
|
||||
onClick={() => {
|
||||
setActiveTabId(tab.id);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { IconCheckbox } from '@tabler/icons-react';
|
||||
|
||||
import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator';
|
||||
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||
@ -22,7 +23,7 @@ export const Default: Story = {
|
||||
};
|
||||
|
||||
export const Catalog: Story = {
|
||||
args: { title: 'Tab title' },
|
||||
args: { title: 'Tab title', icon: <IconCheckbox /> },
|
||||
argTypes: {
|
||||
active: { control: false },
|
||||
onClick: { control: false },
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
export const activeTabIdScopedState = atomFamily<string | null, string>({
|
||||
key: 'activeTabIdScopedState',
|
||||
default: null,
|
||||
});
|
||||
@ -6,6 +6,7 @@ import SortAndFilterBar from '@/ui/filter-n-sort/components/SortAndFilterBar';
|
||||
import { SortDropdownButton } from '@/ui/filter-n-sort/components/SortDropdownButton';
|
||||
import { FiltersHotkeyScope } from '@/ui/filter-n-sort/types/FiltersHotkeyScope';
|
||||
import { SelectedSortType, SortType } from '@/ui/filter-n-sort/types/interface';
|
||||
import { TopBar } from '@/ui/top-bar/TopBar';
|
||||
|
||||
import { TableContext } from '../../states/TableContext';
|
||||
|
||||
@ -16,23 +17,6 @@ type OwnProps<SortField> = {
|
||||
onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const StyledTableHeader = styled.div`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
height: 40px;
|
||||
justify-content: space-between;
|
||||
padding-left: ${({ theme }) => theme.spacing(3)};
|
||||
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledIcon = styled.div`
|
||||
display: flex;
|
||||
margin-left: ${({ theme }) => theme.spacing(1)};
|
||||
@ -43,16 +27,6 @@ const StyledIcon = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledViewSection = styled.div`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const StyledFilters = styled.div`
|
||||
display: flex;
|
||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||
gap: 2px;
|
||||
`;
|
||||
|
||||
export function TableHeader<SortField>({
|
||||
viewName,
|
||||
viewIcon,
|
||||
@ -82,35 +56,37 @@ export function TableHeader<SortField>({
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledTableHeader>
|
||||
<StyledViewSection>
|
||||
<TopBar
|
||||
leftComponent={
|
||||
<>
|
||||
<StyledIcon>{viewIcon}</StyledIcon>
|
||||
{viewName}
|
||||
</StyledViewSection>
|
||||
<StyledFilters>
|
||||
<FilterDropdownButton
|
||||
context={TableContext}
|
||||
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
|
||||
/>
|
||||
<SortDropdownButton<SortField>
|
||||
isSortSelected={sorts.length > 0}
|
||||
availableSorts={availableSorts || []}
|
||||
onSortSelect={sortSelect}
|
||||
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
|
||||
/>
|
||||
</StyledFilters>
|
||||
</StyledTableHeader>
|
||||
<SortAndFilterBar
|
||||
context={TableContext}
|
||||
sorts={sorts}
|
||||
onRemoveSort={sortUnselect}
|
||||
onCancelClick={() => {
|
||||
innerSetSorts([]);
|
||||
onSortsUpdate && onSortsUpdate([]);
|
||||
}}
|
||||
/>
|
||||
</StyledContainer>
|
||||
</>
|
||||
}
|
||||
rightComponents={[
|
||||
<FilterDropdownButton
|
||||
context={TableContext}
|
||||
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
|
||||
/>,
|
||||
<SortDropdownButton<SortField>
|
||||
isSortSelected={sorts.length > 0}
|
||||
availableSorts={availableSorts || []}
|
||||
onSortSelect={sortSelect}
|
||||
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
|
||||
/>,
|
||||
]}
|
||||
bottomComponent={
|
||||
<SortAndFilterBar
|
||||
context={TableContext}
|
||||
sorts={sorts}
|
||||
onRemoveSort={sortUnselect}
|
||||
onCancelClick={() => {
|
||||
innerSetSorts([]);
|
||||
onSortsUpdate && onSortsUpdate([]);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
51
front/src/modules/ui/top-bar/TopBar.tsx
Normal file
51
front/src/modules/ui/top-bar/TopBar.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import { ReactNode } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
type OwnProps = {
|
||||
leftComponent?: ReactNode;
|
||||
rightComponents?: ReactNode[];
|
||||
bottomComponent?: ReactNode;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const StyledTableHeader = styled.div`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
height: 40px;
|
||||
justify-content: space-between;
|
||||
padding-left: ${({ theme }) => theme.spacing(3)};
|
||||
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledLeftSection = styled.div`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const StyledRightSection = styled.div`
|
||||
display: flex;
|
||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||
gap: 2px;
|
||||
`;
|
||||
|
||||
export function TopBar({
|
||||
leftComponent,
|
||||
rightComponents,
|
||||
bottomComponent,
|
||||
}: OwnProps) {
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledTableHeader>
|
||||
<StyledLeftSection>{leftComponent}</StyledLeftSection>
|
||||
<StyledRightSection>{rightComponents}</StyledRightSection>
|
||||
</StyledTableHeader>
|
||||
{bottomComponent}
|
||||
</StyledContainer>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user