Skip to content

Commit

Permalink
feat: goal deps redesign
Browse files Browse the repository at this point in the history
  • Loading branch information
DenisVorop committed Nov 9, 2023
1 parent 912e11c commit 7c62dd4
Show file tree
Hide file tree
Showing 20 changed files with 420 additions and 326 deletions.
12 changes: 10 additions & 2 deletions src/components/Badge.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import styled from 'styled-components';
import { Text, nullable } from '@taskany/bricks';
import { gapS, gapXs, gray8, gray9 } from '@taskany/colors';
import { gapM, gapS, gapXs, gray8, gray9 } from '@taskany/colors';

interface BadgeProps {
icon: React.ReactNode;
Expand All @@ -23,9 +23,16 @@ const StyledBadgeIconContainer = styled.span`
}
`;

/**
* First row has height equal to the height of the first text line.
* If the content (like icons) is larger than the first text line, then it expands the first row.
* Suitable for text sizes 'xs', 's', 'm', 'l'.
*/
const StyledBadge = styled.span`
position: relative;
display: flex;
display: grid;
grid-template-columns: min-content auto min-content;
grid-template-rows: minmax(max-content, calc(${gapM} + ${gapXs})) minmax(0, 1fr);
align-items: center;
padding: ${gapXs} 0;
Expand All @@ -47,6 +54,7 @@ const StyledText = styled(Text).attrs({
ellipsis: true,
})`
padding: 0 ${gapXs} 0 ${gapS};
grid-row: span 2;
`;

export const Badge: React.FC<BadgeProps> = ({ icon, text, action, className }) => {
Expand Down
4 changes: 2 additions & 2 deletions src/components/FilterBase/FilterBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import { tr } from './FilterBase.i18n';
interface FilterBaseProps<T> extends Omit<React.ComponentProps<typeof AutoComplete<T>>, 'onChange'> {
inputProps?: React.ComponentProps<typeof AutoCompleteInput>;
viewMode: 'split' | 'union';
onChange: (items: string[]) => void;
onChange?: (items: string[]) => void;
}

export function FilterBase<T>({ viewMode, onChange, keyGetter, children, ...props }: FilterBaseProps<T>) {
const handleChange = useCallback(
(items: T[]) => {
onChange(items.map(keyGetter));
onChange?.(items.map(keyGetter));
},
[onChange, keyGetter],
);
Expand Down
31 changes: 1 addition & 30 deletions src/components/GoalActivityFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import { usePageContext } from '../hooks/usePageContext';

import { GoalDeleteModal } from './GoalDeleteModal/GoalDeleteModal';
import { CommentView } from './CommentView/CommentView';
import { GoalDependencyAddForm } from './GoalDependencyForm/GoalDependencyForm';
import { GoalDependencyListByKind } from './GoalDependencyList/GoalDependencyList';
import { GoalCriteria } from './GoalCriteria/GoalCriteria';
import { GoalActivity } from './GoalActivity';
import { AddCriteriaForm } from './CriteriaForm/CriteriaForm';
Expand All @@ -25,13 +23,12 @@ interface GoalActivityFeedProps {
shortId?: string;

onGoalCriteriaClick?: ComponentProps<typeof GoalCriteria>['onClick'];
onGoalDependencyClick?: ComponentProps<typeof GoalDependencyListByKind>['onClick'];
onGoalDeleteConfirm?: () => void;
onInvalidate?: () => void;
}

export const GoalActivityFeed = forwardRef<HTMLDivElement, GoalActivityFeedProps>(
({ goal, shortId, onGoalCriteriaClick, onGoalDependencyClick, onGoalDeleteConfirm, onInvalidate }, ref) => {
({ goal, shortId, onGoalCriteriaClick, onGoalDeleteConfirm, onInvalidate }, ref) => {
const { user } = usePageContext();
const {
onGoalCommentUpdate,
Expand All @@ -44,8 +41,6 @@ export const GoalActivityFeed = forwardRef<HTMLDivElement, GoalActivityFeedProps
onGoalCommentCreate,
onGoalCommentReactionToggle,
onGoalCommentDelete,
onGoalDependencyAdd,
onGoalDependencyRemove,
lastStateComment,
highlightCommentId,
} = useGoalResource(
Expand Down Expand Up @@ -103,30 +98,6 @@ export const GoalActivityFeed = forwardRef<HTMLDivElement, GoalActivityFeedProps
}
/>
))}

{goal._relations.map((deps) =>
nullable(deps.goals.length || goal._isEditable, () => (
<GoalDependencyListByKind
goalId={goal.id}
key={deps.kind}
kind={deps.kind}
items={deps.goals}
canEdit={goal._isEditable}
onRemove={onGoalDependencyRemove}
onClick={onGoalDependencyClick}
>
{nullable(goal._isEditable, () => (
<GoalDependencyAddForm
onSubmit={onGoalDependencyAdd}
kind={deps.kind}
goalId={goal.id}
isEmpty={deps.goals.length === 0}
/>
))}
</GoalDependencyListByKind>
)),
)}

{nullable(lastStateComment, (value) => (
<CommentView
pin
Expand Down
37 changes: 37 additions & 0 deletions src/components/GoalBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from 'react';
import { Link, nullable } from '@taskany/bricks';

import { Badge } from './Badge';
import { NextLink } from './NextLink';
import { StateDot } from './StateDot';

interface GoalBadgeProps {
title: string;
href?: string;
state?: {
title?: string;
hue?: number;
} | null;
children?: React.ReactNode;
className?: string;
onClick?: () => void;
}

export const GoalBadge: React.FC<GoalBadgeProps> = ({ href, title, state, children, className, onClick }) => {
return (
<Badge
className={className}
icon={<StateDot title={state?.title} hue={state?.hue} />}
text={nullable(
href,
() => (
<Link as={NextLink} href={href} inline onClick={onClick}>
{title}
</Link>
),
title,
)}
action={children}
/>
);
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"blocks": "This goal blocked by",
"dependsOn": "This goal depends on",
"relatesTo": "This goal relates to",
"Delete": ""
"relatedTo": "This goal relates to",
"Kind": ""
}
6 changes: 6 additions & 0 deletions src/components/GoalDependency/GoalDependency.i18n/ru.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"blocks": "Блокирует",
"dependsOn": "Зависит от",
"relatedTo": "Связана с",
"Kind": "Вид зависимости"
}
146 changes: 146 additions & 0 deletions src/components/GoalDependency/GoalDependency.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { FC, useCallback, useMemo, useState } from 'react';
import styled from 'styled-components';
import { AutoCompleteRadioGroup, CheckboxInput, nullable } from '@taskany/bricks';
import { Goal } from '@prisma/client';

import { trpc } from '../../utils/trpcClient';
import { ToggleGoalDependency, dependencyKind } from '../../schema/goal';
import { FilterBase } from '../FilterBase/FilterBase';
import { FilterAutoCompleteInput } from '../FilterAutoCompleteInput/FilterAutoCompleteInput';
import { CustomCell, GoalListItemCompact } from '../GoalListItemCompact';
import { Title } from '../Table';
import { UserGroup } from '../UserGroup';

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

const StyledCheckboxInput = styled(CheckboxInput)`
margin: 0;
`;

const StyledGoalListItemCompact = styled(GoalListItemCompact)`
cursor: pointer;
`;

interface GoalDependencyProps {
id: string;
items: { kind: dependencyKind; goals: Goal[] }[];
onSubmit: (values: ToggleGoalDependency) => void;
}

const getId = (item: { id: string }) => item.id;

export const GoalDependency: FC<GoalDependencyProps> = ({ id, items = [], onSubmit }) => {
const [goalQuery, setGoalQuery] = useState('');
const [kind, setKind] = useState(dependencyKind.dependsOn);

const selectedGoals = useMemo(() => {
return items.flatMap(({ goals }) => goals);
}, [items]);

const { data: goals = [] } = trpc.goal.suggestions.useQuery(
{
input: goalQuery,
limit: 5,
},
{
staleTime: 0,
cacheTime: 0,
},
);

const radios = useMemo(() => {
return [
{
title: tr('dependsOn'),
value: dependencyKind.dependsOn,
},
{
title: tr('blocks'),
value: dependencyKind.blocks,
},
{
title: tr('relatedTo'),
value: dependencyKind.relatedTo,
},
];
}, []);

const handleClick = useCallback(
(values: { id: string; onClick: () => void }) => () => {
const data = {
id,
kind,
relation: { id: values.id },
};

values.onClick();
onSubmit(data);
},
[id, kind, onSubmit],
);

return (
<FilterBase
mode="multiple"
viewMode="split"
items={goals}
keyGetter={getId}
value={selectedGoals}
renderItem={({ item, checked, onItemClick }) => {
return nullable(!checked, () => (
<StyledGoalListItemCompact
icon
rawIcon={<StyledCheckboxInput checked={checked} value={getId(item)} />}
item={item}
onClick={handleClick({
id: getId(item),
onClick: onItemClick,
})}
columns={[
{
name: 'title',
renderColumn: (values) => (
<CustomCell col={6}>
<Title size="s" weight="bold">
{values.title}
</Title>
</CustomCell>
),
},
{
name: 'state',
columnProps: {
min: true,
forIcon: true,
},
},
{
name: 'projectId',
columnProps: {
col: 1,
},
},
{
name: 'issuers',
renderColumn: (values) => (
<CustomCell align="start" width={45}>
<UserGroup users={values.issuers} size={18} />
</CustomCell>
),
},
]}
/>
));
}}
>
<FilterAutoCompleteInput onChange={setGoalQuery} />
<AutoCompleteRadioGroup
title={tr('Kind')}
items={radios}
name="Kind"
onChange={({ value }) => setKind(value as dependencyKind)}
value={kind}
/>
</FilterBase>
);
};
56 changes: 56 additions & 0 deletions src/components/GoalDependencyList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React, { useCallback } from 'react';
import { Goal } from '@prisma/client';
import { IconXCircleSolid } from '@taskany/icons';
import styled from 'styled-components';

import { ToggleGoalDependency, dependencyKind } from '../schema/goal';
import { GoalDependencyItem } from '../../trpc/inferredTypes';
import { routes } from '../hooks/router';

import { GoalBadge } from './GoalBadge';

const StyledGoalBadge = styled(GoalBadge)`
margin-left: 5px; // 24 / 2 - 7 center of UserPic and center of PlusIcon
`;

interface GoalDependency extends Goal, GoalDependencyItem {}
interface GoalDependencyListByKindProps {
id: string;
kind: dependencyKind;
goals: GoalDependency[];
onClick?: (item: GoalDependency) => void;
onRemove?: (values: ToggleGoalDependency) => void;
}

export const GoalDependencyListByKind = ({
id,
kind,
goals = [],
onClick,
onRemove,
}: GoalDependencyListByKindProps) => {
const onClickHandler = useCallback(
(goal: GoalDependency) => (e?: React.MouseEvent) => {
if (onClick) {
e?.preventDefault();
onClick(goal);
}
},
[onClick],
);

return goals.map((item) => (
<StyledGoalBadge
key={item.id}
title={item.title}
state={item?.state}
href={routes.goal(item._shortId)}
onClick={onClickHandler(item)}
>
<IconXCircleSolid
size="xs"
onClick={onRemove ? () => onRemove({ id, kind, relation: { id: item.id } }) : undefined}
/>
</StyledGoalBadge>
));
};

This file was deleted.

Loading

0 comments on commit 7c62dd4

Please sign in to comment.