Skip to content

Commit

Permalink
feat: kanban mode
Browse files Browse the repository at this point in the history
  • Loading branch information
asabotovich committed Aug 5, 2024
1 parent 8a2490b commit ea8cd07
Show file tree
Hide file tree
Showing 24 changed files with 588 additions and 137 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Setup Docker
run: docker-compose -f docker-compose.ci.yml up -d
run: docker compose -f docker-compose.ci.yml up -d
- name: Run specs
uses: cypress-io/github-action@v5
env:
Expand Down
128 changes: 124 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"@sentry/nextjs": "7.99.0",
"@tanstack/react-query": "4.29.5",
"@tanstack/react-query-devtools": "4.29.6",
"@taskany/bricks": "5.41.1",
"@taskany/bricks": "5.43.0",
"@taskany/colors": "1.13.0",
"@taskany/icons": "2.0.7",
"@tippyjs/react": "4.2.6",
Expand Down
56 changes: 38 additions & 18 deletions src/components/DashboardPage/DashboardPage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo } from 'react';
import React, { ComponentProps, useCallback, useEffect, useMemo } from 'react';
import { nullable } from '@taskany/bricks';
import { ListView, TreeViewElement } from '@taskany/bricks/harmony';

Expand All @@ -9,6 +9,7 @@ import { useUrlFilterParams } from '../../hooks/useUrlFilterParams';
import { useFiltersPreset } from '../../hooks/useFiltersPreset';
import { GoalByIdReturnType } from '../../../trpc/inferredTypes';
import { trpc } from '../../utils/trpcClient';
import { buildKanban } from '../../utils/kanban';
import { getPageTitle } from '../../utils/getPageTitle';
import { Page } from '../Page/Page';
import { useGoalPreview } from '../GoalPreview/GoalPreviewProvider';
Expand All @@ -20,6 +21,7 @@ import { routes } from '../../hooks/router';
import { GoalTableList } from '../GoalTableList/GoalTableList';
import { PresetModals } from '../PresetModals';
import { FiltersPanel } from '../FiltersPanel/FiltersPanel';
import { Kanban } from '../Kanban/Kanban';

import { tr } from './DashboardPage.i18n';

Expand All @@ -28,7 +30,7 @@ export const DashboardPage = ({ user, ssrTime, defaultPresetFallback }: External

const { preset } = useFiltersPreset({ defaultPresetFallback });

const { currentPreset, queryState } = useUrlFilterParams({
const { currentPreset, queryState, view } = useUrlFilterParams({
preset,
});

Expand All @@ -44,16 +46,23 @@ export const DashboardPage = ({ user, ssrTime, defaultPresetFallback }: External

const pages = useMemo(() => data?.pages || [], [data?.pages]);

const [groupsOnScreen, goalsCount, totalGoalsCount] = useMemo(() => {
const [groupsOnScreen, canbansByProject, goalsCount, totalGoalsCount] = useMemo(() => {
const groups = pages?.[0]?.groups;

const gr = pages.reduce<typeof groups>((acc, cur) => {
acc.push(...cur.groups);
return acc;
}, []);

const canbans = gr.reduce<Record<string, ComponentProps<typeof Kanban>['value']>>((acum, project) => {
acum[project.id] = buildKanban(project.goals ?? []);

return acum;
}, {});

return [
gr,
canbans,
gr.reduce((acc, group) => acc + group._count.goals, 0),
pages.reduce((acc, { totalGoalsCount = 0 }) => acc + Number(totalGoalsCount), 0),
];
Expand Down Expand Up @@ -101,28 +110,39 @@ export const DashboardPage = ({ user, ssrTime, defaultPresetFallback }: External
counter={goalsCount}
filterPreset={preset}
loading={isLoading}
enableLayoutToggle
/>
}
>
<ListView onKeyboardClick={handleItemEnter}>
{groupsOnScreen?.map(({ goals, ...project }) => (
<ProjectListItemCollapsable
key={project.id}
interactive={false}
visible
project={project}
href={routes.project(project.id)}
goals={nullable(goals, (g) => (
{groupsOnScreen?.map(({ goals, ...project }) => {
const kanban = canbansByProject[project.id];

const children = nullable(
view === 'kanban',
() => <Kanban value={kanban} filterPreset={preset} />,
nullable(goals, (g) => (
<TreeViewElement>
<GoalTableList goals={g} />
</TreeViewElement>
))}
>
{nullable(!goals?.length, () => (
<InlineCreateGoalControl project={project} />
))}
</ProjectListItemCollapsable>
))}
)),
);

return (
<ProjectListItemCollapsable
key={project.id}
interactive={false}
visible
project={project}
href={routes.project(project.id, view ? `view=${view}` : undefined)}
goals={children}
>
{nullable(!goals?.length, () => (
<InlineCreateGoalControl project={project} />
))}
</ProjectListItemCollapsable>
);
})}
</ListView>

{nullable(hasNextPage, () => (
Expand Down
5 changes: 5 additions & 0 deletions src/components/EstimateDropdown/EstimateDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ const toEstimateState = (value: Estimate): EstimateState => {
};
};

export const getEstimateDropdownValueFromDate = (date: Date, type?: DateType | null) => ({
date: getDateString(date),
type: type ?? 'Strict',
});

export const EstimateDropdown = ({ value, onChange, onClose, placement, ...props }: EstimateDropdownProps) => {
const locale = useLocale();
const [estimate, setEstimate] = useState<EstimateState | undefined>(value ? toEstimateState(value) : undefined);
Expand Down
48 changes: 23 additions & 25 deletions src/components/FiltersBar/FiltersBar.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { ComponentProps, FC, HTMLAttributes, ReactNode } from 'react';
import { ComponentProps, FC, HTMLAttributes, ReactNode, useCallback } from 'react';
import cn from 'classnames';
import { nullable } from '@taskany/bricks';
import { Text, Switch, SwitchControl, Button } from '@taskany/bricks/harmony';
import { IconAddOutline, IconAdjustHorizontalSolid, IconAlignTopSolid, IconListUnorderedOutline } from '@taskany/icons';

import { PageView } from '../../hooks/useUrlFilterParams';
import { Dropdown, DropdownPanel, DropdownTrigger } from '../Dropdown/Dropdown';

import s from './FiltersBar.module.css';
Expand All @@ -27,33 +28,30 @@ export const FilterBarCounter: FC<FilterBarCounterProps> = ({ counter, total })
</Text>
);

export const layoutType = {
kanban: 'kanban',
table: 'table',
} as const;

export type LayoutType = keyof typeof layoutType;

interface FiltersBarLayoutSwitchProps {
value: LayoutType;
value?: PageView;
onChange?: (value: PageView) => void;
}

export const FiltersBarLayoutSwitch: FC<FiltersBarLayoutSwitchProps> = ({ value }) => (
<Switch value={value}>
<SwitchControl
disabled
className={s.FiltersBarButton}
iconLeft={<IconListUnorderedOutline size="s" />}
value={layoutType.table}
/>
<SwitchControl
disabled
className={s.FiltersBarButton}
iconLeft={<IconAlignTopSolid size="s" />}
value={layoutType.kanban}
/>
</Switch>
);
export const FiltersBarLayoutSwitch: FC<FiltersBarLayoutSwitchProps> = ({ value = 'list', onChange }) => {
const onChangeCallback = useCallback(
(_: React.SyntheticEvent<HTMLButtonElement>, active: string) => {
onChange?.(active as PageView);
},
[onChange],
);

return (
<Switch value={value} onChange={onChangeCallback}>
<SwitchControl
className={s.FiltersBarButton}
iconLeft={<IconListUnorderedOutline size="s" />}
value="list"
/>
<SwitchControl className={s.FiltersBarButton} iconLeft={<IconAlignTopSolid size="s" />} value="kanban" />
</Switch>
);
};

export const FiltersBar: FC<HTMLAttributes<HTMLDivElement>> = ({ children, className, ...props }) => (
<div className={cn(s.FiltersBar, className)} {...props}>
Expand Down
Loading

0 comments on commit ea8cd07

Please sign in to comment.