Skip to content

Commit

Permalink
[Improvement] Create task and assign another team member (#3353)
Browse files Browse the repository at this point in the history
* implement the assignee dropdown

* add the task assignees dropdowm

* Update settings.json

---------

Co-authored-by: Ruslan Konviser <[email protected]>
  • Loading branch information
CREDO23 and evereq authored Nov 20, 2024
1 parent 9ab591b commit 681714b
Show file tree
Hide file tree
Showing 7 changed files with 146 additions and 48 deletions.
3 changes: 1 addition & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@
"source.fixAll": "explicit",
"source.organizeImports": "never",
"source.sortMembers": "never",
"organizeImports": "never",
// "source.removeUnusedImports": "always"
"organizeImports": "never"
},
"vsicons.presets.angular": true,
"deepscan.enable": true,
Expand Down
12 changes: 7 additions & 5 deletions apps/web/app/hooks/features/useTaskInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export function useTaskInput({
const taskDescription = useRef<null | string>(null);
const taskLabels = useRef<[] | ITaskLabelsItemList[]>([]);
const taskProject = useRef<null | string>(null);
const taskAssignees = useRef<{id:string}[]>([]);

const tasks = customTasks || teamTasks;

Expand Down Expand Up @@ -134,7 +135,6 @@ export function useTaskInput({
const handleTaskCreation = ({
autoActiveTask = true,
autoAssignTaskAuth = true,
assignToUsers = []
}: {
autoActiveTask?: boolean;
autoAssignTaskAuth?: boolean;
Expand All @@ -152,19 +152,20 @@ export function useTaskInput({
const statusId = taskStatusList.find(
(item) => item.name === taskStatus.current
)?.id;

return createTask(
{
taskName: query.trim(),
title: query.trim(),
issueType: taskIssue || 'Bug',
taskStatusId: statusId || (openId as string),
status: taskStatus.current || undefined,
priority: taskPriority.current || undefined,
size: taskSize.current || undefined,
tags: taskLabels.current || [],
description: taskDescription.current,
projectId : taskProject.current
description: taskDescription.current ?? '',
projectId : taskProject.current,
members : [...(autoAssignTaskAuth && user?.employee.id ? [{id : user?.employee.id}] : [] ), ...taskAssignees.current]
},
!autoAssignTaskAuth ? assignToUsers : undefined
).then((res) => {
setQuery('');
localStorage.setItem('lastTaskIssue', taskIssue || 'Bug');
Expand Down Expand Up @@ -237,6 +238,7 @@ export function useTaskInput({
user,
userRef,
taskProject,
taskAssignees
};
}

Expand Down
20 changes: 8 additions & 12 deletions apps/web/app/hooks/features/useTeamTasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,17 +289,18 @@ export function useTeamTasks() {
const createTask = useCallback(
(
{
taskName,
title,
issueType,
taskStatusId,
status = taskStatus[0]?.name,
priority,
size,
tags,
description,
projectId
projectId,
members
}: {
taskName: string;
title: string;
issueType?: string;
status?: string;
taskStatusId: string;
Expand All @@ -308,12 +309,13 @@ export function useTeamTasks() {
tags?: ITaskLabelsItemList[];
description?: string | null;
projectId?: string | null;
members?: { id: string }[]
},
members?: { id: string }[]
) => {

return createQueryCall(
{
title: taskName,
title,
issueType,
status,
priority,
Expand All @@ -322,17 +324,11 @@ export function useTeamTasks() {
// Set Project Id to cookie
// TODO: Make it dynamic when we add Dropdown in Navbar

// ...(activeTeam?.projects && activeTeam?.projects.length > 0
// ? {
// projectId: activeTeam.projects[0].id
// }
// : {}),
projectId,
...(description ? { description: `<p>${description}</p>` } : {}),
...(members ? { members } : {}),
members,
taskStatusId: taskStatusId
},
$user.current
).then((res) => {
deepCheckAndUpdateTasks(res?.data?.items || [], true);
return res;
Expand Down
4 changes: 1 addition & 3 deletions apps/web/app/services/client/api/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
getOrganizationIdCookie,
getTenantIdCookie
} from '@app/helpers';
import { IUser } from '@app/interfaces';
import { TTasksTimesheetStatisticsParams } from '@app/services/server/requests';
import qs from 'qs';

Expand Down Expand Up @@ -108,7 +107,7 @@ export async function updateTaskAPI(taskId: string, body: Partial<ITeamTask>) {
return put<PaginationResponse<ITeamTask>>(`/tasks/${taskId}`, body);
}

export async function createTeamTaskAPI(body: Partial<ICreateTask> & { title: string }, user: IUser | undefined) {
export async function createTeamTaskAPI(body: Partial<ICreateTask> & { title: string }) {
if (GAUZY_API_BASE_SERVER_URL.value) {
const organizationId = getOrganizationIdCookie();
const teamId = getActiveTeamIdCookie();
Expand All @@ -118,7 +117,6 @@ export async function createTeamTaskAPI(body: Partial<ICreateTask> & { title: st

const datas: ICreateTask = {
description: '',
members: user?.employee?.id ? [{ id: user.employee.id }] : [],
teams: [
{
id: teamId
Expand Down
4 changes: 2 additions & 2 deletions apps/web/components/pages/kanban/menu-kanban-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export default function MenuKanbanCard({ item: task, member }: { item: ITeamTask
await createTask({
...task,
taskStatusId: task.taskStatusId ?? taskStatus[0].id,
taskName: `Copy ${task.title}`,
title: `Copy ${task.title}`,
issueType: task.issueType ?? 'Bug'
});
} catch (error) {
Expand Down Expand Up @@ -201,7 +201,7 @@ interface ITeamMemberSelectProps {
*
* @return {JSX.Element} The multi select component
*/
function TeamMembersSelect(props: ITeamMemberSelectProps): JSX.Element {
export function TeamMembersSelect(props: ITeamMemberSelectProps): JSX.Element {
const { teamMembers, task } = props;
const t = useTranslations();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -591,7 +591,7 @@ export function SearchTaskInput(props: ISearchTaskInputProps) {
setCreateTaskLoading(true);
if (taskName.trim().length < 5) return;
await createTask({
taskName: taskName.trim(),
title: taskName.trim(),
status: taskStatus[0].name,
taskStatusId: taskStatus[0].id,
issueType: 'Bug' // TODO: Let the user choose the issue type
Expand Down
149 changes: 126 additions & 23 deletions apps/web/lib/features/task/task-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@ import {
ITaskSize,
ITaskStatus,
ITeamTask,
Nullable
Nullable,
OT_Member
} from '@app/interfaces';
import { activeTeamTaskId, timerStatusState } from '@app/stores';
import { clsxm } from '@app/utils';
import { Popover, Transition } from '@headlessui/react';
import { PlusIcon } from '@heroicons/react/20/solid';
import { Combobox, Popover, Transition } from '@headlessui/react';
import { CheckIcon, ChevronUpDownIcon, PlusIcon } from '@heroicons/react/20/solid';
import {
Button,
Card,
Expand All @@ -37,6 +38,7 @@ import {
} from 'lib/components';
import { CheckCircleTickIcon as TickCircleIcon } from 'assets/svg';
import {
Fragment,
MutableRefObject,
PropsWithChildren,
useCallback,
Expand Down Expand Up @@ -86,7 +88,6 @@ type Props = {
onTaskCreated?: (task: ITeamTask | undefined) => void;
cardWithoutShadow?: boolean;
assignTaskPopup?: boolean;

forParentChildRelationship?: boolean;
} & PropsWithChildren;

Expand All @@ -107,7 +108,7 @@ export function TaskInput(props: Props) {
const {
viewType = 'input-trigger',
showTaskNumber = false,
showCombobox = true
showCombobox = true,
} = props;

const datas = useTaskInput({
Expand Down Expand Up @@ -449,22 +450,18 @@ export function TaskInput(props: Props) {
);

const taskCard = (
<TaskCard
datas={datas}
onItemClick={
props.task !== undefined || props.onTaskClick
? onTaskClick
: setAuthActiveTask
}
inputField={viewType === 'one-view' ? inputField : undefined}
fullWidth={props.fullWidthCombobox}
fullHeight={props.fullHeightCombobox}
handleTaskCreation={handleTaskCreation}
cardWithoutShadow={props.cardWithoutShadow}
assignTaskPopup={props.assignTaskPopup}
updatedTaskList={updatedTaskList}
forParentChildRelationship={props.forParentChildRelationship}
/>
<TaskCard
datas={datas}
onItemClick={props.task !== undefined || props.onTaskClick ? onTaskClick : setAuthActiveTask}
inputField={viewType === 'one-view' ? inputField : undefined}
fullWidth={props.fullWidthCombobox}
fullHeight={props.fullHeightCombobox}
handleTaskCreation={handleTaskCreation}
cardWithoutShadow={props.cardWithoutShadow}
assignTaskPopup={props.assignTaskPopup}
updatedTaskList={updatedTaskList}
forParentChildRelationship={props.forParentChildRelationship}
/>
);

return viewType === 'one-view' ? (
Expand Down Expand Up @@ -518,7 +515,7 @@ function TaskCard({
cardWithoutShadow,
forParentChildRelationship,
updatedTaskList,
assignTaskPopup
assignTaskPopup,
}: {
datas: Partial<RTuseTaskInput>;
onItemClick?: (task: ITeamTask) => void;
Expand All @@ -535,14 +532,16 @@ function TaskCard({
const t = useTranslations();
const activeTaskEl = useRef<HTMLLIElement | null>(null);
const { taskLabels: taskLabelsData } = useTaskLabels();
const { activeTeam } = useOrganizationTeams();

const {
taskStatus,
taskPriority,
taskSize,
taskLabels,
taskDescription,
taskProject
taskProject,
taskAssignees
} = datas;
const { nextOffset, data } = useInfinityScrolling(updatedTaskList ?? [], 5);

Expand Down Expand Up @@ -643,6 +642,9 @@ function TaskCard({
task={datas.inputTask}
/>

{taskAssignees !== undefined && <AssigneesSelect assignees={taskAssignees} teamMembers={activeTeam?.members ?? []}/>}


<ProjectDropDown
styles={{
container: 'rounded-xl w-[10.625rem] !max-w-[10.625rem]',
Expand Down Expand Up @@ -797,3 +799,104 @@ function TaskCard({
</>
);
}


/**
* ----------------------------------------------
* ----------- ASSIGNEES MULTI SELECT -----------
* ----------------------------------------------
*/

interface ITeamMemberSelectProps {
teamMembers: OT_Member[];
assignees : MutableRefObject<{
id: string;
}[]>
}
/**
* A multi select component for assignees
*
* @param {object} props - The props object
* @param {string[]} props.teamMembers - Members of the current team
* @param {ITeamMemberSelectProps["assignees"]} props.assignees - Assigned members
*
* @return {JSX.Element} The multi select component
*/
function AssigneesSelect(props: ITeamMemberSelectProps): JSX.Element {
const { teamMembers , assignees} = props;
const t = useTranslations();
const {user} = useAuthenticateUser()
const authMember = useMemo(() => teamMembers.find(member => member.employee.user?.id == user?.id), [teamMembers, user?.id])

return (
<div className=" w-40 rounded-xl bg-[#F2F2F2] py-2 px-3">
<Combobox multiple={true}>
<div className="relative my-auto">
<div className="relative w-full cursor-default overflow-hidden rounded-lg text-left focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 sm:text-sm">
<Combobox.Input readOnly className="w-0 h-0" />
<Combobox.Button className="absolute hover:transition-all inset-y-0 right-0 flex justify-between w-full items-center">
<span>{t('common.ASSIGNEE')}</span>
<ChevronUpDownIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
</Combobox.Button>
</div>
<Transition
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Combobox.Options className="absolute mt-1 max-h-60 h-auto w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-none sm:text-sm">
{authMember && (
<Combobox.Option
className={({ active }) =>
`relative cursor-default select-none py-2 pl-10 pr-4 ${
active ? 'bg-primary/5' : 'text-gray-900'
}`
}
value={authMember}
>
<span className={`absolute inset-y-0 left-0 flex items-center pl-3 `}>
<CheckIcon className="h-5 w-5" aria-hidden="true" />
</span>
{authMember.employee.fullName}
</Combobox.Option>
)}

{teamMembers
.filter((member) => member.employee.user?.id != user?.id)
.map((member) => (
<Combobox.Option
key={member.id}
className={({ active }) =>
`relative cursor-pointer select-none py-2 pl-10 pr-4 ${
active ? 'bg-primary/5' : 'text-gray-900'
}`
}
onClick={() => {
const isAssigned = assignees.current.map(el => el.id).includes(member.employee.id);


if (isAssigned) {
assignees.current = assignees.current.filter((el) => el.id != member.employee.id)
} else {
assignees.current = [...assignees.current, {id:member.employee.id}];
}
}}
value={member}
>
{assignees.current.map(el => el.id).includes(member.employee.id) && (
<span className={`absolute inset-y-0 left-0 flex items-center pl-3 `}>
<CheckIcon className="h-5 w-5" aria-hidden="true" />
</span>
)}

{member.employee.fullName}
</Combobox.Option>
))}
</Combobox.Options>
</Transition>
</div>
</Combobox>
</div>
);
}

0 comments on commit 681714b

Please sign in to comment.