Skip to content

Commit

Permalink
feat(GoalCriteria): make criteria form inside popup
Browse files Browse the repository at this point in the history
  • Loading branch information
LamaEats committed Nov 24, 2023
1 parent c09f498 commit a53a164
Show file tree
Hide file tree
Showing 2 changed files with 166 additions and 149 deletions.
42 changes: 25 additions & 17 deletions src/components/GoalActivityFeed.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { nullable } from '@taskany/bricks';
import { forwardRef, useCallback } from 'react';
import { forwardRef, useCallback, useMemo } from 'react';
import dynamic from 'next/dynamic';

import { ModalEvent, dispatchModalEvent } from '../utils/dispatchModal';
Expand Down Expand Up @@ -155,14 +155,36 @@ export const GoalActivityFeed = forwardRef<HTMLDivElement, GoalActivityFeedProps
[goal.goalAchiveCriteria, onGoalCriteriaClick],
);

const criteriaList = useMemo(() => {
if (goal._criteria?.length) {
return goal._criteria.map((criteria) => ({
id: criteria.id,
title: criteria.title,
weight: criteria.weight,
criteriaGoal:
criteria.criteriaGoal != null
? {
id: criteria.criteriaGoal.id,
title: criteria.criteriaGoal.title,
stateColor: criteria.criteriaGoal.state?.hue || 0,
href: routes.goal(criteria.criteriaGoal._shortId),
}
: null,
isDone: criteria.isDone,
}));
}

return [];
}, [goal._criteria]);

return (
<>
<GoalActivity
ref={ref}
feed={goal._activityFeed}
header={
<>
{nullable(goal._criteria.length || goal._isEditable, () => (
{nullable(criteriaList || goal._isEditable, () => (
<GoalCriteria
canEdit={goal._isEditable}
onCreate={handleCreateCriteria}
Expand All @@ -172,21 +194,7 @@ export const GoalActivityFeed = forwardRef<HTMLDivElement, GoalActivityFeedProps
onRemove={handleRemoveCriteria}
onGoalClick={handleGoalClick}
validateGoalCriteriaBindings={handleValidateGoalToCriteriaBinging}
list={goal._criteria.map((criteria) => ({
id: criteria.id,
title: criteria.title,
weight: criteria.weight,
criteriaGoal:
criteria.criteriaGoal != null
? {
id: criteria.criteriaGoal.id,
title: criteria.criteriaGoal.title,
stateColor: criteria.criteriaGoal.state?.hue || 0,
href: routes.goal(criteria.criteriaGoal._shortId),
}
: null,
isDone: criteria.isDone,
}))}
list={criteriaList}
/>
))}
{nullable(lastStateComment, (value) => (
Expand Down
273 changes: 141 additions & 132 deletions src/components/GoalCriteria/GoalCriteria.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import React, { useCallback, useMemo, useRef, useState } from 'react';
import styled, { css } from 'styled-components';
import { Text, nullable, Table, MenuItem, TableRow, TableCell, Dropdown, useClickOutside } from '@taskany/bricks';
import {
Text,
nullable,
Table,
MenuItem,
TableRow,
TableCell,
Dropdown,
useClickOutside,
Popup,
} from '@taskany/bricks';
import {
IconTargetOutline,
IconCircleOutline,
Expand Down Expand Up @@ -100,10 +110,6 @@ const StyledTextHeading = styled(Text)`
border-bottom: 1px solid ${gray4};
`;

const StyledIconTableCell = styled(TableCell)`
padding-top: calc(${gapXs} + 5px); // offset by input vertical center
`;

const StyledBadge = styled(Badge)`
padding: 0;
`;
Expand All @@ -112,22 +118,14 @@ const StyledGoalBadge = styled(GoalBadge)`
padding: 0;
`;

const useGoalSuggestions = (value = '') => {
const [query, setQuery] = useState(() => value);
const StyledCriteriaRow = styled(TableRow)<{ active: boolean }>`
padding: 2.5px 5px;
margin: 0 -5px;
const { data: suggestions } = trpc.goal.suggestions.useQuery(
{
input: query,
limit: 5,
},
{
staleTime: 0,
cacheTime: 0,
},
);

return [suggestions, setQuery] as [typeof suggestions, React.Dispatch<React.SetStateAction<string>>];
};
background-color: ${({ active }) => (active ? gray4 : 'unset')};
border-radius: ${radiusS};
transition: background-color 0.3s ease;
`;

type CriteriaFormData = NonNullable<React.ComponentProps<typeof CriteriaForm>['values']>;
type CriteriaValidityData = React.ComponentProps<typeof CriteriaForm>['validityData'];
Expand Down Expand Up @@ -178,6 +176,7 @@ const CriteriaItem: React.FC<CriteriaItemProps> = ({
const { criteriaGoal, title } = criteria;
const [mode, setMode] = useState<'view' | 'edit'>(() => calculateModeCriteria(criteria));
const formRef = useRef<HTMLDivElement>(null);
const popupRef = useRef<HTMLDivElement>(null);

useClickOutside(formRef, () => {
setMode('view');
Expand Down Expand Up @@ -228,73 +227,74 @@ const CriteriaItem: React.FC<CriteriaItemProps> = ({
}, [criteria.title, onCancel]);

return (
<TableRow gap={5} align="start">
{nullable(
mode === 'edit',
() => (
<>
<StyledIconTableCell width="16px">
<StyledCircleIcon size="s" />
</StyledIconTableCell>
<TableCell width="calc(100% - 16px)">
{renderForm({ onEditCancel: handleCancel, ref: formRef })}
</TableCell>
</>
),
<>
<TableCell width="calc(100% - 5ch)" align="baseline">
{nullable(
criteriaGoal,
(goal) => (
<StyledGoalBadge
title={goal.title}
color={goal.stateColor}
theme={1}
href={goal.href}
onClick={() => onClick(criteria)}
<>
<StyledCriteriaRow align="start" active={mode === 'edit'}>
<TableCell width="calc(100% - 4ch)" align="baseline" ref={popupRef}>
{nullable(
criteriaGoal,
(goal) => (
<StyledGoalBadge
title={goal.title}
color={goal.stateColor}
theme={1}
href={goal.href}
onClick={() => onClick(criteria)}
/>
),
<StyledBadge
icon={
<GoalCriteriaCheckBox
checked={criteria.isDone}
canEdit={canEdit}
onClick={() => onUpdateState({ ...criteria, isDone: !criteria.isDone })}
/>
),
<StyledBadge
icon={
<GoalCriteriaCheckBox
checked={criteria.isDone}
canEdit={canEdit}
onClick={() => onUpdateState({ ...criteria, isDone: !criteria.isDone })}
/>
}
text={title}
/>,
}
text={title}
/>,
)}
</TableCell>
<TableCell width="3ch" justify="end" align="start">
{nullable(criteria.weight > 0, () => (
<Text size="s" color={gray9}>
{criteria.weight}
</Text>
))}
</TableCell>
<TableCell min align="baseline">
<Dropdown
onChange={handleChange}
renderTrigger={({ onClick }) => <IconMoreVerticalOutline size="xs" onClick={onClick} />}
placement="right"
items={availableActions}
renderItem={(props) => (
<MenuItem
key={props.index}
onClick={props.onClick}
icon={props.item.icon}
ghost
color={props.item.color}
>
{props.item.label}
</MenuItem>
)}
</TableCell>
<TableCell width="3ch" justify="end" align="start">
{nullable(criteria.weight > 0, () => (
<Text size="s" color={gray9}>
{criteria.weight}
</Text>
))}
</TableCell>
<TableCell min align="baseline">
<Dropdown
onChange={handleChange}
renderTrigger={({ onClick }) => <IconMoreVerticalOutline size="xs" onClick={onClick} />}
placement="right"
items={availableActions}
renderItem={(props) => (
<MenuItem
key={props.index}
onClick={props.onClick}
icon={props.item.icon}
ghost
color={props.item.color}
>
{props.item.label}
</MenuItem>
)}
/>
</TableCell>
</>,
)}
</TableRow>
/>
</TableCell>
</StyledCriteriaRow>
{nullable(mode === 'edit', () => (
<Popup
arrow={false}
placement="bottom-start"
visible={mode === 'edit'}
reference={popupRef}
interactive
minWidth={350}
maxWidth={350}
offset={[20, 5]}
>
{renderForm({ onEditCancel: handleCancel, ref: formRef })}
</Popup>
))}
</>
);
};

Expand Down Expand Up @@ -328,8 +328,19 @@ export const GoalCriteria: React.FC<GoalCriteriaProps> = ({
onConvertToGoal,
validateGoalCriteriaBindings,
}) => {
const [suggestions = [], setQuery] = useGoalSuggestions();
const [addingCriteria, setAddingCriteria] = useState(false);
const [query, setQuery] = useState('');

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

const sortedCriteriaItems = useMemo(() => {
const sorted = list.reduce<Record<'done' | 'undone', CriteriaItemValue[]>>(
Expand Down Expand Up @@ -428,52 +439,50 @@ export const GoalCriteria: React.FC<GoalCriteriaProps> = ({
onClick={onGoalClick}
canEdit={canEdit}
renderForm={(props) => (
<StyledBox>
<CriteriaForm
ref={props.ref}
withModeSwitch
defaultMode={criteria.criteriaGoal != null ? 'goal' : 'simple'}
values={
!addingCriteria
? {
id: criteria.id,
mode: criteria.criteriaGoal != null ? 'goal' : 'simple',
title: criteria.title,
selected: criteria.criteriaGoal,
weight: criteria.weight > 0 ? String(criteria.weight) : '',
}
: undefined
}
validityData={{
title: dataForValidate.title.filter((title) => title !== criteria.title),
sumOfCriteria: dataForValidate.sumOfCriteria - criteria.weight,
}}
items={suggestions?.map((goal) => ({
id: goal.id,
title: goal.title,
stateColor: goal.state?.hue,
}))}
onSubmit={handleFormSubmit(props.onEditCancel)}
onInputChange={(val = '') => setQuery(val)}
onCancel={props.onEditCancel}
renderItem={(props) => (
<MenuItem
ghost
focused={props.active || props.hovered}
onClick={props.onItemClick}
onMouseMove={props.onMouseMove}
onMouseLeave={props.onMouseLeave}
>
<GoalBadge
title={props.item.title}
color={props.item.stateColor}
theme={1}
/>
</MenuItem>
)}
validateBindingsFor={validateGoalCriteriaBindings}
/>
</StyledBox>
<CriteriaForm
ref={props.ref}
withModeSwitch
defaultMode={criteria.criteriaGoal != null ? 'goal' : 'simple'}
values={
!addingCriteria
? {
id: criteria.id,
mode: criteria.criteriaGoal != null ? 'goal' : 'simple',
title: criteria.title,
selected: criteria.criteriaGoal,
weight: criteria.weight > 0 ? String(criteria.weight) : '',
}
: undefined
}
validityData={{
title: dataForValidate.title.filter((title) => title !== criteria.title),
sumOfCriteria: dataForValidate.sumOfCriteria - criteria.weight,
}}
items={suggestions?.map((goal) => ({
id: goal.id,
title: goal.title,
stateColor: goal.state?.hue,
}))}
onSubmit={handleFormSubmit(props.onEditCancel)}
onInputChange={(val = '') => setQuery(val)}
onCancel={props.onEditCancel}
renderItem={(props) => (
<MenuItem
ghost
focused={props.active || props.hovered}
onClick={props.onItemClick}
onMouseMove={props.onMouseMove}
onMouseLeave={props.onMouseLeave}
>
<GoalBadge
title={props.item.title}
color={props.item.stateColor}
theme={1}
/>
</MenuItem>
)}
validateBindingsFor={validateGoalCriteriaBindings}
/>
)}
/>
))}
Expand Down

0 comments on commit a53a164

Please sign in to comment.