Skip to content

Commit

Permalink
feat: add GoalParentCombobox to Project Create Modal
Browse files Browse the repository at this point in the history
  • Loading branch information
asabotovich committed Jun 4, 2024
1 parent 7610a4a commit 9191ae2
Show file tree
Hide file tree
Showing 20 changed files with 590 additions and 498 deletions.
13 changes: 7 additions & 6 deletions src/components/Dropdown/Dropdown.i18n/en.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
{
"Project": "",
"Owner": "",
"Priority": "",
"State": "",
"Estimate": "",
"Not chosen": ""
"Parent projects": "Parent projects",
"Project": "Project",
"Owner": "Owner",
"Priority": "Priority",
"State": "State",
"Estimate": "Estimate",
"Not chosen": "Not chosen"
}
1 change: 1 addition & 0 deletions src/components/Dropdown/Dropdown.i18n/ru.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"Parent projects": "Родители",
"Project": "Проект",
"Owner": "Ответственный",
"Priority": "Приоритет",
Expand Down
19 changes: 4 additions & 15 deletions src/components/GoalCreateForm/GoalCreateForm.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useContext, useMemo, useState } from 'react';
import { IconUpSmallSolid, IconDownSmallSolid } from '@taskany/icons';
import { Button, Text } from '@taskany/bricks/harmony';
import { KeyCode, useKeyboard } from '@taskany/bricks';
import { useRouter as useNextRouter } from 'next/router';

import { useRouter } from '../../hooks/router';
import { usePageContext } from '../../hooks/usePageContext';
Expand All @@ -23,6 +22,7 @@ import {
goalForm,
} from '../../utils/domObjects';
import { FormAction } from '../FormActions/FormActions';
import { ProjectContext } from '../ProjectContext/ProjectContext';
import { Dropdown, DropdownPanel, DropdownTrigger } from '../Dropdown/Dropdown';

import { tr } from './GoalCreateForm.i18n';
Expand All @@ -36,10 +36,6 @@ interface GoalCreateFormProps {

const GoalCreateForm: React.FC<GoalCreateFormProps> = ({ title, onGoalCreate, personal }) => {
const router = useRouter();
const {
asPath,
query: { id },
} = useNextRouter();
const { user } = usePageContext();
const [goalCreateFormActionCache, setGoalCreateFormActionCache] = useLocalStorage('goalCreateFormAction');
const [busy, setBusy] = useState(false);
Expand All @@ -50,14 +46,7 @@ const GoalCreateForm: React.FC<GoalCreateFormProps> = ({ title, onGoalCreate, pe
const defaultPriority = useMemo(() => priorities?.filter((priority) => priority.default)[0], [priorities]);
const [isOpen, setIsOpen] = useState(false);

const { data: project } = trpc.project.getById.useQuery(
{
id: id as string,
},
{
enabled: Boolean(asPath.includes('/projects/') && id),
},
);
const { project: parent } = useContext(ProjectContext);

const createOptions = [
{
Expand Down Expand Up @@ -151,7 +140,7 @@ const GoalCreateForm: React.FC<GoalCreateFormProps> = ({ title, onGoalCreate, pe
busy={busy}
validitySchema={goalCommonSchema}
owner={{ id: user?.activityId, user } as ActivityByIdReturnType}
parent={project ?? undefined}
parent={parent ?? undefined}
personal={personal}
priority={defaultPriority ?? undefined}
onSubmit={createGoal}
Expand Down
6 changes: 6 additions & 0 deletions src/components/GoalForm/GoalForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
stateCombobox,
usersCombobox,
} from '../../utils/domObjects';
import { ModalEvent, dispatchModalEvent } from '../../utils/dispatchModal';
import { TagsList } from '../TagsList/TagsList';
import { GoalParentDropdown } from '../GoalParentDropdown/GoalParentDropdown';
import { UserDropdown } from '../UserDropdown/UserDropdown';
Expand Down Expand Up @@ -161,6 +162,10 @@ export const GoalForm: React.FC<GoalFormProps> = ({
setGoalType(goalTypeMap.default);
}, [setValue, goalType, parent]);

const onNewProjectClick = useCallback(() => {
dispatchModalEvent(ModalEvent.GoalCreateModal)();
}, []);

return (
<ModalContent {...attrs}>
<form onSubmit={handleSubmit(onSubmit)} className={s.Form}>
Expand Down Expand Up @@ -231,6 +236,7 @@ export const GoalForm: React.FC<GoalFormProps> = ({
error={errorsResolver(field.name)}
disabled={busy}
className={s.GoalFormParentDropdown}
onNewProjectClick={onNewProjectClick}
{...field}
{...projectsCombobox.attr}
/>
Expand Down
98 changes: 56 additions & 42 deletions src/components/GoalParentDropdown/GoalParentDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { IconAddOutline } from '@taskany/icons';
import { trpc } from '../../utils/trpcClient';
import { Dropdown, DropdownTrigger, DropdownPanel, DropdownGuardedProps } from '../Dropdown/Dropdown';
import { ModalEvent, dispatchModalEvent } from '../../utils/dispatchModal';
import { ModalContext } from '../ModalOnEvent';

import s from './GoalParentDropdown.module.css';
import { tr } from './GoalParentDropdown.i18n';
Expand All @@ -25,8 +26,10 @@ type GoalParentDropdownProps = {
placeholder?: string;
disabled?: boolean;
readOnly?: boolean;
filter?: string[];
placement?: ComponentProps<typeof DropdownPanel>['placement'];
onClose?: () => void;
onNewProjectClick?: () => void;
} & DropdownGuardedProps<GoalParentValue>;

export const GoalParentDropdown = ({
Expand All @@ -37,58 +40,63 @@ export const GoalParentDropdown = ({
placement,
onChange,
onClose,
filter,
onNewProjectClick,
...props
}: GoalParentDropdownProps) => {
const { values, filterIds } = useMemo(() => {
const res: GoalParentValue[] = [];
const values = res.concat(value || []);

const filterIds = Array.from(
values.reduce((acum, { id }) => {
acum.add(id);

return acum;
}, new Set<string>(filter)),
);

return {
values,
filterIds,
};
}, [value, filter]);

const [inputState, setInputState] = useState(query);

const { data: userProjects = [] } = trpc.v2.project.userProjects.useQuery(undefined, {
keepPreviousData: true,
});
const enableSuggestion = inputState.length >= 2;

useEffect(() => {
setInputState(query);
}, [query]);
const { data: userProjects = [] } = trpc.v2.project.userProjects.useQuery(
{
take: 10,
filter: filterIds,
},
{
keepPreviousData: true,
},
);

const { data } = trpc.project.suggestions.useQuery(
const { data: suggestionsProjects = [] } = trpc.project.suggestions.useQuery(
{
query: inputState,
filter: filterIds,
},
{
enabled: inputState.length >= 2,
enabled: enableSuggestion,
keepPreviousData: true,
cacheTime: 0,
staleTime: 0,
},
);

const suggestions = useMemo<GoalParentValue[]>(
() => (data && data?.length > 0 ? data : userProjects.slice(0, 10)),
[data, userProjects],
);

const values = useMemo(() => {
const res: GoalParentValue[] = [];
return res.concat(value || []);
}, [value]);

const valuesMap = useMemo(() => {
return values.reduce<Record<string, boolean>>((acc, cur) => {
acc[cur.id] = true;
return acc;
}, {});
}, [values]);

const items = useMemo(() => {
if (mode === 'single') {
return suggestions;
}

return suggestions.filter((suggestion) => !valuesMap[suggestion.id]);
}, [mode, suggestions, valuesMap]);
useEffect(() => {
setInputState(query);
}, [query]);

const handleCreateProject = useCallback(() => {
dispatchModalEvent(ModalEvent.GoalCreateModal)();
onNewProjectClick?.();
dispatchModalEvent(ModalEvent.ProjectCreateModal)();
}, []);
}, [onNewProjectClick]);

const handleClose = useCallback(() => {
onClose?.();
Expand All @@ -114,7 +122,7 @@ export const GoalParentDropdown = ({
width={320}
value={values}
title={tr('Suggestions')}
items={items}
items={enableSuggestion ? suggestionsProjects : userProjects}
placement={placement}
mode={mode}
selectable
Expand All @@ -133,13 +141,19 @@ export const GoalParentDropdown = ({
</div>
)}
>
<Button
text={tr('Create project')}
view="ghost"
iconLeft={<IconAddOutline size="s" />}
onClick={handleCreateProject}
className={s.CreateProjectButton}
/>
<ModalContext.Consumer>
{(ctx) =>
nullable(!ctx[ModalEvent.ProjectCreateModal], () => (
<Button
text={tr('Create project')}
view="ghost"
iconLeft={<IconAddOutline size="s" />}
onClick={handleCreateProject}
className={s.CreateProjectButton}
/>
))
}
</ModalContext.Consumer>
</DropdownPanel>
</Dropdown>
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/PageNavigation/PageNavigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export const PageNavigation: FC<AppNavigationProps> = ({ logo }) => {
const nextRouter = useRouter();
const activeRoute = nextRouter.asPath.split('?')[0];

const { data: projects = [] } = trpc.v2.project.userProjects.useQuery();
const { data: projects = [] } = trpc.v2.project.userProjects.useQuery({});

const { data: presets = [] } = trpc.filter.getUserFilters.useQuery(undefined, {
keepPreviousData: true,
Expand Down
7 changes: 7 additions & 0 deletions src/components/ProjectContext/ProjectContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { createContext } from 'react';

import { ProjectByIdReturnType } from '../../../trpc/inferredTypes';

export const ProjectContext = createContext<{ project: ProjectByIdReturnType }>({
project: null,
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@
"Flow or state title": "Flow or state title",
"issue key one": "#{key}-42",
"issue key two": "#{key}-911",
"Perfect": "Perfect!"
"Perfect": "Perfect!",
"Enter project": "Enter project"
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@
"Flow or state title": "Модель или статус",
"issue key one": "#{key}-42",
"issue key two": "#{key}-911",
"Perfect": "Супер!"
"Perfect": "Супер!",
"Enter project": "Проект"
}
4 changes: 4 additions & 0 deletions src/components/ProjectCreateForm/ProjectCreateForm.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@
.FormActions {
align-items: center;
}

.ProjectFormParentDropdown {
min-width: 190px;
}
35 changes: 27 additions & 8 deletions src/components/ProjectCreateForm/ProjectCreateForm.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { FormEvent, useCallback, useEffect, useRef, useState } from 'react';
import React, { FormEvent, useCallback, useContext, useRef, useState } from 'react';
import { useForm, Controller } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import dynamic from 'next/dynamic';
Expand All @@ -23,6 +23,7 @@ import { FlowDropdown } from '../FlowDropdown/FlowDropdown';
import { trpc } from '../../utils/trpcClient';
import { ProjectCreate, projectCreateSchema } from '../../schema/project';
import { ModalEvent, dispatchModalEvent } from '../../utils/dispatchModal';
import { GoalParentDropdown } from '../GoalParentDropdown/GoalParentDropdown';
import { HelpButton } from '../HelpButton/HelpButton';
import {
projectCancelButton,
Expand All @@ -33,6 +34,7 @@ import {
} from '../../utils/domObjects';
import RotatableTip from '../RotatableTip/RotatableTip';
import { FormAction, FormActions } from '../FormActions/FormActions';
import { ProjectContext } from '../ProjectContext/ProjectContext';

import { tr } from './ProjectCreateForm.i18n';
import s from './ProjectCreateForm.module.css';
Expand All @@ -45,6 +47,9 @@ const ProjectCreateForm: React.FC = () => {
const [busy, setBusy] = useState(false);
const [dirtyKey, setDirtyKey] = useState(false);

const { project: parent } = useContext(ProjectContext);
const { data: flowRecomendations = [] } = trpc.flow.recommedations.useQuery();

const {
register,
handleSubmit,
Expand All @@ -57,14 +62,17 @@ const ProjectCreateForm: React.FC = () => {
mode: 'onChange',
reValidateMode: 'onChange',
shouldFocusError: false,
defaultValues: {
parent: parent ? [{ id: parent.id, title: parent.title }] : undefined,
flow: flowRecomendations[0],
},
});

const errorsResolver = errorsProvider(errors, isSubmitted);
const titleWatcher = watch('title');
const keyWatcher = watch('id');

const isKeyEnoughLength = Boolean(keyWatcher?.length >= 3);
const flowRecomendations = trpc.flow.recommedations.useQuery();
const existingProject = trpc.project.getById.useQuery(
{
id: keyWatcher,
Expand All @@ -76,12 +84,6 @@ const ProjectCreateForm: React.FC = () => {

const isKeyUnique = Boolean(!existingProject?.data);

useEffect(() => {
if (flowRecomendations.data) {
setValue('flow', flowRecomendations.data[0]);
}
}, [setValue, flowRecomendations]);

const onCreateProject = useCallback(
(form: ProjectCreate) => {
setBusy(true);
Expand Down Expand Up @@ -208,6 +210,23 @@ const ProjectCreateForm: React.FC = () => {
</div>

<FormActions className={s.FormActions} align="left">
<FormAction className={s.FormAction}>
<Controller
name="parent"
control={control}
render={({ field }) => (
<GoalParentDropdown
mode="multiple"
label="Parent projects"
placeholder={tr('Enter project')}
error={errorsResolver(field.name)}
disabled={busy}
className={s.ProjectFormParentDropdown}
{...field}
/>
)}
/>
</FormAction>
<FormAction className={s.FormAction}>
<Controller
name="flow"
Expand Down
Loading

0 comments on commit 9191ae2

Please sign in to comment.