chore: refactored and resolved build issues on the issues and issue detail page (#3340)

* fix: handled undefined issue_id in list layout

* dev: issue detail store and optimization

* dev: issue filter and list operations

* fix: typo on labels update

* dev: Handled all issues in the list layout in project issues

* dev: handled kanban and auick add issue in swimlanes

* chore: fixed peekoverview in kanban

* chore: fixed peekoverview in calendar

* chore: fixed peekoverview in gantt

* chore: updated quick add in the gantt chart

* chore: handled issue detail properties and resolved build issues

---------

Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
This commit is contained in:
guru_sainath 2024-01-10 20:09:45 +05:30 committed by sriram veeraghanta
parent e6b31e2550
commit 4611ec0b83
112 changed files with 3303 additions and 2560 deletions

View file

@ -4,12 +4,13 @@ import { observer } from "mobx-react-lite";
import { Draggable } from "@hello-pangea/dnd";
import { MoreHorizontal } from "lucide-react";
// components
import { Tooltip } from "@plane/ui";
import { Tooltip, ControlLink } from "@plane/ui";
// hooks
import useOutsideClickDetector from "hooks/use-outside-click-detector";
// ui
// types
import { TIssue, TIssueMap } from "@plane/types";
import { useProject, useProjectState } from "hooks/store";
import { useApplication, useIssueDetail, useProject, useProjectState } from "hooks/store";
type Props = {
issues: TIssueMap | undefined;
@ -23,21 +24,23 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
// router
const router = useRouter();
// hooks
const {
router: { workspaceSlug, projectId },
} = useApplication();
const { getProjectById } = useProject();
const { getProjectStates } = useProjectState();
const { setPeekIssue } = useIssueDetail();
// states
const [isMenuActive, setIsMenuActive] = useState(false);
const menuActionRef = useRef<HTMLDivElement | null>(null);
const handleIssuePeekOverview = (issue: TIssue) => {
const { query } = router;
router.push({
pathname: router.pathname,
query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project_id },
});
};
const handleIssuePeekOverview = (issue: TIssue) =>
workspaceSlug &&
issue &&
issue.project_id &&
issue.id &&
setPeekIssue({ workspaceSlug, projectId: issue.project_id, issueId: issue.id });
useOutsideClickDetector(menuActionRef, () => setIsMenuActive(false));
@ -67,45 +70,53 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
{...provided.draggableProps}
{...provided.dragHandleProps}
ref={provided.innerRef}
onClick={() => handleIssuePeekOverview(issue)}
>
{issue?.tempId !== undefined && (
<div className="absolute left-0 top-0 z-[99999] h-full w-full animate-pulse bg-custom-background-100/20" />
)}
<div
className={`group/calendar-block flex h-8 w-full items-center justify-between gap-1.5 rounded border-[0.5px] border-custom-border-100 px-1 py-1.5 shadow-custom-shadow-2xs ${
snapshot.isDragging
? "bg-custom-background-90 shadow-custom-shadow-rg"
: "bg-custom-background-100 hover:bg-custom-background-90"
}`}
<ControlLink
href={`/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`}
target="_blank"
onClick={() => handleIssuePeekOverview(issue)}
className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100"
>
<div className="flex h-full items-center gap-1.5">
<span
className="h-full w-0.5 flex-shrink-0 rounded"
style={{
backgroundColor: getProjectStates(issue?.project_id).find(
(state) => state?.id == issue?.state_id
)?.color,
}}
/>
<div className="flex-shrink-0 text-xs text-custom-text-300">
{getProjectById(issue?.project_id)?.identifier}-{issue.sequence_id}
<>
{issue?.tempId !== undefined && (
<div className="absolute left-0 top-0 z-[99999] h-full w-full animate-pulse bg-custom-background-100/20" />
)}
<div
className={`group/calendar-block flex h-8 w-full items-center justify-between gap-1.5 rounded border-[0.5px] border-custom-border-100 px-1 py-1.5 shadow-custom-shadow-2xs ${
snapshot.isDragging
? "bg-custom-background-90 shadow-custom-shadow-rg"
: "bg-custom-background-100 hover:bg-custom-background-90"
}`}
>
<div className="flex h-full items-center gap-1.5">
<span
className="h-full w-0.5 flex-shrink-0 rounded"
style={{
backgroundColor: getProjectStates(issue?.project_id).find(
(state) => state?.id == issue?.state_id
)?.color,
}}
/>
<div className="flex-shrink-0 text-xs text-custom-text-300">
{getProjectById(issue?.project_id)?.identifier}-{issue.sequence_id}
</div>
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
<div className="truncate text-xs">{issue.name}</div>
</Tooltip>
</div>
<div
className={`hidden h-5 w-5 group-hover/calendar-block:block ${isMenuActive ? "!block" : ""}`}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
{quickActions(issue, customActionButton)}
</div>
</div>
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
<div className="truncate text-xs">{issue.name}</div>
</Tooltip>
</div>
<div
className={`hidden h-5 w-5 group-hover/calendar-block:block ${isMenuActive ? "!block" : ""}`}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
{quickActions(issue, customActionButton)}
</div>
</div>
</>
</ControlLink>
</div>
)}
</Draggable>

View file

@ -1,25 +1,26 @@
import { useRouter } from "next/router";
// ui
import { Tooltip, StateGroupIcon } from "@plane/ui";
import { Tooltip, StateGroupIcon, ControlLink } from "@plane/ui";
// helpers
import { renderFormattedDate } from "helpers/date-time.helper";
// types
import { TIssue } from "@plane/types";
import { useProject, useProjectState } from "hooks/store";
import { useApplication, useIssueDetail, useProject, useProjectState } from "hooks/store";
export const IssueGanttBlock = ({ data }: { data: TIssue }) => {
const router = useRouter();
// hooks
const {
router: { workspaceSlug },
} = useApplication();
const { getProjectStates } = useProjectState();
const { setPeekIssue } = useIssueDetail();
const handleIssuePeekOverview = () => {
const { query } = router;
router.push({
pathname: router.pathname,
query: { ...query, peekIssueId: data?.id, peekProjectId: data?.project_id },
});
};
const handleIssuePeekOverview = () =>
workspaceSlug &&
data &&
data.project_id &&
data.id &&
setPeekIssue({ workspaceSlug, projectId: data.project_id, issueId: data.id });
return (
<div
@ -49,34 +50,42 @@ export const IssueGanttBlock = ({ data }: { data: TIssue }) => {
// rendering issues on gantt sidebar
export const IssueGanttSidebarBlock = ({ data }: { data: TIssue }) => {
const router = useRouter();
// hooks
const { getProjectStates } = useProjectState();
const { getProjectById } = useProject();
const {
router: { workspaceSlug },
} = useApplication();
const { setPeekIssue } = useIssueDetail();
const handleIssuePeekOverview = () => {
const { query } = router;
router.push({
pathname: router.pathname,
query: { ...query, peekIssueId: data?.id, peekProjectId: data?.project_id },
});
};
const handleIssuePeekOverview = () =>
workspaceSlug &&
data &&
data.project_id &&
data.id &&
setPeekIssue({ workspaceSlug, projectId: data.project_id, issueId: data.id });
const currentStateDetails =
getProjectStates(data?.project_id)?.find((state) => state?.id == data?.state_id) || undefined;
return (
<div className="relative flex h-full w-full cursor-pointer items-center gap-2" onClick={handleIssuePeekOverview}>
{currentStateDetails != undefined && (
<StateGroupIcon stateGroup={currentStateDetails?.group} color={currentStateDetails?.color} />
)}
<div className="flex-shrink-0 text-xs text-custom-text-300">
{getProjectById(data?.project_id)?.identifier} {data?.sequence_id}
<ControlLink
href={`/${workspaceSlug}/projects/${data.project_id}/issues/${data.id}`}
target="_blank"
onClick={handleIssuePeekOverview}
className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100"
>
<div className="relative flex h-full w-full cursor-pointer items-center gap-2" onClick={handleIssuePeekOverview}>
{currentStateDetails != undefined && (
<StateGroupIcon stateGroup={currentStateDetails?.group} color={currentStateDetails?.color} />
)}
<div className="flex-shrink-0 text-xs text-custom-text-300">
{getProjectById(data?.project_id)?.identifier} {data?.sequence_id}
</div>
<Tooltip tooltipHeading="Title" tooltipContent={data.name}>
<span className="flex-grow truncate text-sm font-medium">{data?.name}</span>
</Tooltip>
</div>
<Tooltip tooltipHeading="Title" tooltipContent={data.name}>
<span className="flex-grow truncate text-sm font-medium">{data?.name}</span>
</Tooltip>
</div>
</ControlLink>
);
};

View file

@ -1,10 +1,10 @@
import { useEffect, useState, useRef } from "react";
import { useEffect, useState, useRef, FC } from "react";
import { useRouter } from "next/router";
import { useForm } from "react-hook-form";
import { observer } from "mobx-react-lite";
import { PlusIcon } from "lucide-react";
// hooks
import { useProject, useWorkspace } from "hooks/store";
import { useProject } from "hooks/store";
import useToast from "hooks/use-toast";
import useKeypress from "hooks/use-keypress";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
@ -12,9 +12,38 @@ import useOutsideClickDetector from "hooks/use-outside-click-detector";
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
import { createIssuePayload } from "helpers/issue.helper";
// types
import { TIssue } from "@plane/types";
import { IProject, TIssue } from "@plane/types";
type Props = {
interface IInputProps {
formKey: string;
register: any;
setFocus: any;
projectDetail: IProject | null;
}
const Inputs: FC<IInputProps> = (props) => {
const { formKey, register, setFocus, projectDetail } = props;
useEffect(() => {
setFocus(formKey);
}, [formKey, setFocus]);
return (
<div className="flex w-full items-center gap-3">
<div className="text-xs font-medium text-custom-text-400">{projectDetail?.identifier ?? "..."}</div>
<input
type="text"
autoComplete="off"
placeholder="Issue Title"
{...register(formKey, {
required: "Issue title is required.",
})}
className="w-full rounded-md bg-transparent px-2 py-3 text-sm font-medium leading-5 text-custom-text-200 outline-none"
/>
</div>
);
};
type IGanttQuickAddIssueForm = {
prePopulatedData?: Partial<TIssue>;
onSuccess?: (data: TIssue) => Promise<void> | void;
quickAddCallback?: (
@ -30,34 +59,25 @@ const defaultValues: Partial<TIssue> = {
name: "",
};
const Inputs = (props: any) => {
const { register, setFocus } = props;
useEffect(() => {
setFocus("name");
}, [setFocus]);
return (
<input
type="text"
autoComplete="off"
placeholder="Issue Title"
{...register("name", {
required: "Issue title is required.",
})}
className="w-full rounded-md bg-transparent px-2 text-sm font-medium leading-5 text-custom-text-200 outline-none"
/>
);
};
export const GanttInlineCreateIssueForm: React.FC<Props> = observer((props) => {
export const GanttQuickAddIssueForm: React.FC<IGanttQuickAddIssueForm> = observer((props) => {
const { prePopulatedData, quickAddCallback, viewId } = props;
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// store hooks
const { getWorkspaceBySlug } = useWorkspace();
const { currentProjectDetails } = useProject();
// hooks
const { getProjectById } = useProject();
const { setToastAlert } = useToast();
const projectDetail = (projectId && getProjectById(projectId.toString())) || undefined;
const ref = useRef<HTMLFormElement>(null);
const [isOpen, setIsOpen] = useState(false);
const handleClose = () => setIsOpen(false);
useKeypress("Escape", handleClose);
useOutsideClickDetector(ref, handleClose);
// form info
const {
reset,
@ -67,103 +87,67 @@ export const GanttInlineCreateIssueForm: React.FC<Props> = observer((props) => {
formState: { errors, isSubmitting },
} = useForm<TIssue>({ defaultValues });
// ref
const ref = useRef<HTMLFormElement>(null);
// states
const [isOpen, setIsOpen] = useState(false);
const handleClose = () => setIsOpen(false);
// hooks
useKeypress("Escape", handleClose);
useOutsideClickDetector(ref, handleClose);
const { setToastAlert } = useToast();
// derived values
const workspaceDetail = getWorkspaceBySlug(workspaceSlug?.toString()!);
useEffect(() => {
if (!isOpen) reset({ ...defaultValues });
}, [isOpen, reset]);
useEffect(() => {
if (!errors) return;
Object.keys(errors).forEach((key) => {
const error = errors[key as keyof TIssue];
setToastAlert({
type: "error",
title: "Error!",
message: error?.message?.toString() || "Some error occurred. Please try again.",
});
});
}, [errors, setToastAlert]);
const onSubmitHandler = async (formData: TIssue) => {
if (isSubmitting || !workspaceSlug || !projectId) return;
reset({ ...defaultValues });
const targetDate = new Date();
targetDate.setDate(targetDate.getDate() + 1);
const payload = createIssuePayload(projectId.toString(), {
...(prePopulatedData ?? {}),
...formData,
start_date: renderFormattedPayloadDate(new Date()),
target_date: renderFormattedPayloadDate(targetDate),
});
try {
if (quickAddCallback) {
await quickAddCallback(workspaceSlug.toString(), projectId.toString(), payload, viewId);
}
quickAddCallback &&
(await quickAddCallback(workspaceSlug.toString(), projectId.toString(), { ...payload }, viewId));
setToastAlert({
type: "success",
title: "Success!",
message: "Issue created successfully.",
});
} catch (err: any) {
Object.keys(err || {}).forEach((key) => {
const error = err?.[key];
const errorTitle = error ? (Array.isArray(error) ? error.join(", ") : error) : null;
setToastAlert({
type: "error",
title: "Error!",
message: errorTitle || "Some error occurred. Please try again.",
});
setToastAlert({
type: "error",
title: "Error!",
message: err?.message || "Some error occurred. Please try again.",
});
}
};
return (
<>
{isOpen && (
<form
ref={ref}
className="mr-2.5 flex items-center gap-x-2 rounded border-[0.5px] border-custom-border-100 bg-custom-background-100 px-2 py-3 shadow-custom-shadow-2xs"
onSubmit={handleSubmit(onSubmitHandler)}
>
<div className="h-3 w-3 flex-shrink-0 rounded-full border border-custom-border-1000" />
<h4 className="text-xs text-custom-text-400">{currentProjectDetails?.identifier ?? "..."}</h4>
<Inputs register={register} setFocus={setFocus} />
</form>
)}
{isOpen && (
<p className="ml-3 mt-3 text-xs italic text-custom-text-200">
Press {"'"}Enter{"'"} to add another issue
</p>
)}
{!isOpen && (
<button
type="button"
className="flex items-center gap-x-[6px] rounded-md px-2 py-1 text-custom-primary-100"
onClick={() => setIsOpen(true)}
>
<PlusIcon className="h-3.5 w-3.5 stroke-2" />
<span className="text-sm font-medium text-custom-primary-100">New Issue</span>
</button>
)}
<div
className={`${errors && errors?.name && errors?.name?.message ? `border border-red-500/20 bg-red-500/10` : ``}`}
>
{isOpen ? (
<div className="shadow-custom-shadow-sm">
<form
ref={ref}
onSubmit={handleSubmit(onSubmitHandler)}
className="flex w-full items-center gap-x-3 border-[0.5px] border-custom-border-100 bg-custom-background-100 px-3"
>
<Inputs formKey={"name"} register={register} setFocus={setFocus} projectDetail={projectDetail ?? null} />
</form>
<div className="px-3 py-2 text-xs italic text-custom-text-200">{`Press 'Enter' to add another issue`}</div>
</div>
) : (
<div
className="flex w-full cursor-pointer items-center gap-2 p-3 py-3 text-custom-primary-100"
onClick={() => setIsOpen(true)}
>
<PlusIcon className="h-3.5 w-3.5 stroke-2" />
<span className="text-sm font-medium text-custom-primary-100">New Issue</span>
</div>
)}
</div>
</>
);
});

View file

@ -15,17 +15,16 @@ import { IProjectIssues, IProjectIssuesFilter } from "store/issue/project";
//components
import { KanBan } from "./default";
import { KanBanSwimLanes } from "./swimlanes";
import { DeleteIssueModal, IssuePeekOverview } from "components/issues";
import { DeleteIssueModal } from "components/issues";
import { EUserProjectRoles } from "constants/project";
import { useIssues } from "hooks/store/use-issues";
import { handleDragDrop } from "./utils";
import { IssueKanBanViewStore } from "store/issue/issue_kanban_view.store";
import { ICycleIssues, ICycleIssuesFilter } from "store/issue/cycle";
import { IDraftIssues, IDraftIssuesFilter } from "store/issue/draft";
import { IProfileIssues, IProfileIssuesFilter } from "store/issue/profile";
import { IModuleIssues, IModuleIssuesFilter } from "store/issue/module";
import { IProjectViewIssues, IProjectViewIssuesFilter } from "store/issue/project-views";
import { TCreateModalStoreTypes } from "constants/issue";
import { EIssueFilterType, TCreateModalStoreTypes } from "constants/issue";
export interface IBaseKanBanLayout {
issues: IProjectIssues | ICycleIssues | IDraftIssues | IModuleIssues | IProjectViewIssues | IProfileIssues;
@ -69,7 +68,7 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
} = props;
// router
const router = useRouter();
const { workspaceSlug, projectId, peekIssueId, peekProjectId } = router.query;
const { workspaceSlug, projectId } = router.query;
// store hooks
const {
membership: { currentProjectRole },
@ -78,9 +77,6 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
// toast alert
const { setToastAlert } = useToast();
// FIXME get from filters
const kanbanViewStore: IssueKanBanViewStore = {} as IssueKanBanViewStore;
const issueIds = issues?.groupedIssueIds || [];
const displayFilters = issuesFilter?.issueFilters?.displayFilters;
@ -211,10 +207,19 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
});
};
const handleKanBanToggle = (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => {
kanbanViewStore.handleKanBanToggle(toggle, value);
const handleKanbanFilters = (toggle: "group_by" | "sub_group_by", value: string) => {
if (workspaceSlug && projectId) {
let _kanbanFilters = issuesFilter?.issueFilters?.kanbanFilters?.[toggle] || [];
if (_kanbanFilters.includes(value)) _kanbanFilters = _kanbanFilters.filter((_value) => _value != value);
else _kanbanFilters.push(value);
issuesFilter.updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.KANBAN_FILTERS, {
[toggle]: _kanbanFilters,
});
}
};
const kanbanFilters = issuesFilter?.issueFilters?.kanbanFilters || { group_by: [], sub_group_by: [] };
return (
<>
<DeleteIssueModal
@ -230,8 +235,9 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
</div>
)}
<div className={`relative h-max min-h-full w-max min-w-full bg-custom-background-90 px-3`}>
<div className="relative h-full w-max min-w-full bg-custom-background-90 px-2">
<DragDropContext onDragStart={onDragStart} onDragEnd={onDragEnd}>
{/* drag and delete component */}
<div
className={`fixed left-1/2 -translate-x-1/2 ${
isDragStarted ? "z-40" : ""
@ -262,8 +268,8 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
group_by={group_by}
handleIssues={handleIssues}
quickActions={renderQuickActions}
kanBanToggle={kanbanViewStore?.kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
handleKanbanFilters={handleKanbanFilters}
kanbanFilters={kanbanFilters}
enableQuickIssueCreate={enableQuickAdd}
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
quickAddCallback={issues?.quickAddIssue}
@ -275,15 +281,6 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
/>
</DragDropContext>
</div>
{/* {workspaceSlug && peekIssueId && peekProjectId && (
<IssuePeekOverview
workspaceSlug={workspaceSlug.toString()}
projectId={peekProjectId.toString()}
issueId={peekIssueId.toString()}
handleIssue={async (issueToUpdate) => await handleIssues(issueToUpdate as TIssue, EIssueActions.UPDATE)}
/>
)} */}
</>
);
});

View file

@ -5,12 +5,11 @@ import { observer } from "mobx-react-lite";
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
import { IssueProperties } from "../properties/all-properties";
// ui
import { Tooltip } from "@plane/ui";
import { Tooltip, ControlLink } from "@plane/ui";
// types
import { TIssue, IIssueDisplayProperties, IIssueMap } from "@plane/types";
import { EIssueActions } from "../types";
import { useRouter } from "next/router";
import { useProject } from "hooks/store";
import { useApplication, useIssueDetail, useProject } from "hooks/store";
interface IssueBlockProps {
issueId: string;
@ -34,24 +33,23 @@ interface IssueDetailsBlockProps {
const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = observer((props: IssueDetailsBlockProps) => {
const { issue, handleIssues, quickActions, isReadOnly, displayProperties } = props;
const router = useRouter();
// hooks
const { getProjectById } = useProject();
const {
router: { workspaceSlug, projectId },
} = useApplication();
const { setPeekIssue } = useIssueDetail();
const updateIssue = (issueToUpdate: TIssue) => {
if (issueToUpdate) handleIssues(issueToUpdate, EIssueActions.UPDATE);
};
const handleIssuePeekOverview = () => {
const { query } = router;
router.push({
pathname: router.pathname,
query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project_id },
});
};
const handleIssuePeekOverview = (issue: TIssue) =>
workspaceSlug &&
issue &&
issue.project_id &&
issue.id &&
setPeekIssue({ workspaceSlug, projectId: issue.project_id, issueId: issue.id });
return (
<>
@ -63,11 +61,18 @@ const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = observer((prop
<div className="absolute -top-1 right-0 hidden group-hover/kanban-block:block">{quickActions(issue)}</div>
</div>
</WithDisplayPropertiesHOC>
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
<div className="line-clamp-2 text-sm font-medium text-custom-text-100" onClick={handleIssuePeekOverview}>
{issue.name}
</div>
</Tooltip>
<ControlLink
href={`/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`}
target="_blank"
onClick={() => handleIssuePeekOverview(issue)}
className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100"
>
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
<span>{issue.name}</span>
</Tooltip>
</ControlLink>
<IssueProperties
className="flex flex-wrap items-center gap-2 whitespace-nowrap"
issue={issue}

View file

@ -14,6 +14,7 @@ import {
IIssueMap,
TSubGroupedIssues,
TUnGroupedIssues,
TIssueKanbanFilters,
} from "@plane/types";
// constants
import { EIssueActions } from "../types";
@ -30,8 +31,8 @@ export interface IGroupByKanBan {
isDragDisabled: boolean;
handleIssues: (issue: TIssue, action: EIssueActions) => void;
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
kanBanToggle: any;
handleKanBanToggle: any;
kanbanFilters: TIssueKanbanFilters;
handleKanbanFilters: any;
enableQuickIssueCreate?: boolean;
quickAddCallback?: (
workspaceSlug: string,
@ -57,8 +58,8 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
isDragDisabled,
handleIssues,
quickActions,
kanBanToggle,
handleKanBanToggle,
kanbanFilters,
handleKanbanFilters,
enableQuickIssueCreate,
quickAddCallback,
viewId,
@ -77,58 +78,63 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
if (!list) return null;
const verticalAlignPosition = (_list: IGroupByColumn) => kanBanToggle?.groupByHeaderMinMax.includes(_list.id);
const visibilityGroupBy = (_list: IGroupByColumn) =>
sub_group_by ? false : kanbanFilters?.group_by.includes(_list.id) ? true : false;
const isGroupByCreatedBy = group_by === "created_by";
return (
<div className="relative flex h-full w-full gap-3">
<div className={`relative w-full flex gap-3 overflow-hidden ${sub_group_by ? "h-full" : "h-full"}`}>
{list &&
list.length > 0 &&
list.map((_list: IGroupByColumn) => {
const verticalPosition = verticalAlignPosition(_list);
const groupByVisibilityToggle = visibilityGroupBy(_list);
return (
<div
className={`relative flex flex-shrink-0 flex-col ${!verticalPosition ? `w-[340px]` : ``} group`}
key={_list.id}
className={`relative flex flex-shrink-0 flex-col h-full group ${
groupByVisibilityToggle ? `` : `w-[340px]`
}`}
>
{sub_group_by === null && (
<div className="sticky top-0 z-[2] w-full flex-shrink-0 bg-custom-background-90 py-1">
<div className="flex-shrink-0 sticky top-0 z-[2] w-full bg-custom-background-90 py-1">
<HeaderGroupByCard
sub_group_by={sub_group_by}
group_by={group_by}
column_id={_list.id}
icon={_list.Icon}
icon={_list.icon}
title={_list.name}
count={(issueIds as TGroupedIssues)?.[_list.id]?.length || 0}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
issuePayload={_list.payload}
disableIssueCreation={disableIssueCreation || isGroupByCreatedBy}
currentStore={currentStore}
addIssuesToView={addIssuesToView}
kanbanFilters={kanbanFilters}
handleKanbanFilters={handleKanbanFilters}
/>
</div>
)}
<KanbanGroup
groupId={_list.id}
issuesMap={issuesMap}
issueIds={issueIds}
displayProperties={displayProperties}
sub_group_by={sub_group_by}
group_by={group_by}
sub_group_id={sub_group_id}
isDragDisabled={isDragDisabled}
handleIssues={handleIssues}
quickActions={quickActions}
enableQuickIssueCreate={enableQuickIssueCreate}
quickAddCallback={quickAddCallback}
viewId={viewId}
disableIssueCreation={disableIssueCreation}
canEditProperties={canEditProperties}
verticalPosition={verticalPosition}
/>
{!groupByVisibilityToggle && (
<KanbanGroup
groupId={_list.id}
issuesMap={issuesMap}
issueIds={issueIds}
displayProperties={displayProperties}
sub_group_by={sub_group_by}
group_by={group_by}
sub_group_id={sub_group_id}
isDragDisabled={isDragDisabled}
handleIssues={handleIssues}
quickActions={quickActions}
enableQuickIssueCreate={enableQuickIssueCreate}
quickAddCallback={quickAddCallback}
viewId={viewId}
disableIssueCreation={disableIssueCreation}
canEditProperties={canEditProperties}
groupByVisibilityToggle={groupByVisibilityToggle}
/>
)}
</div>
);
})}
@ -145,8 +151,8 @@ export interface IKanBan {
sub_group_id?: string;
handleIssues: (issue: TIssue, action: EIssueActions) => void;
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
kanBanToggle: any;
handleKanBanToggle: any;
kanbanFilters: TIssueKanbanFilters;
handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void;
showEmptyGroup: boolean;
enableQuickIssueCreate?: boolean;
quickAddCallback?: (
@ -172,8 +178,8 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
sub_group_id = "null",
handleIssues,
quickActions,
kanBanToggle,
handleKanBanToggle,
kanbanFilters,
handleKanbanFilters,
enableQuickIssueCreate,
quickAddCallback,
viewId,
@ -186,27 +192,25 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
const issueKanBanView = useKanbanView();
return (
<div className="relative h-full w-full">
<GroupByKanBan
issuesMap={issuesMap}
issueIds={issueIds}
displayProperties={displayProperties}
group_by={group_by}
sub_group_by={sub_group_by}
sub_group_id={sub_group_id}
isDragDisabled={!issueKanBanView?.canUserDragDrop}
handleIssues={handleIssues}
quickActions={quickActions}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
enableQuickIssueCreate={enableQuickIssueCreate}
quickAddCallback={quickAddCallback}
viewId={viewId}
disableIssueCreation={disableIssueCreation}
currentStore={currentStore}
addIssuesToView={addIssuesToView}
canEditProperties={canEditProperties}
/>
</div>
<GroupByKanBan
issuesMap={issuesMap}
issueIds={issueIds}
displayProperties={displayProperties}
group_by={group_by}
sub_group_by={sub_group_by}
sub_group_id={sub_group_id}
isDragDisabled={!issueKanBanView?.canUserDragDrop}
handleIssues={handleIssues}
quickActions={quickActions}
kanbanFilters={kanbanFilters}
handleKanbanFilters={handleKanbanFilters}
enableQuickIssueCreate={enableQuickIssueCreate}
quickAddCallback={quickAddCallback}
viewId={viewId}
disableIssueCreation={disableIssueCreation}
currentStore={currentStore}
addIssuesToView={addIssuesToView}
canEditProperties={canEditProperties}
/>
);
});

View file

@ -11,7 +11,7 @@ import useToast from "hooks/use-toast";
// mobx
import { observer } from "mobx-react-lite";
// types
import { TIssue, ISearchIssueResponse } from "@plane/types";
import { TIssue, ISearchIssueResponse, TIssueKanbanFilters } from "@plane/types";
import { TCreateModalStoreTypes } from "constants/issue";
interface IHeaderGroupByCard {
@ -21,8 +21,8 @@ interface IHeaderGroupByCard {
icon?: React.ReactNode;
title: string;
count: number;
kanBanToggle: any;
handleKanBanToggle: any;
kanbanFilters: TIssueKanbanFilters;
handleKanbanFilters: any;
issuePayload: Partial<TIssue>;
disableIssueCreation?: boolean;
currentStore?: TCreateModalStoreTypes;
@ -36,14 +36,14 @@ export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => {
icon,
title,
count,
kanBanToggle,
handleKanBanToggle,
kanbanFilters,
handleKanbanFilters,
issuePayload,
disableIssueCreation,
currentStore,
addIssuesToView,
} = props;
const verticalAlignPosition = kanBanToggle?.groupByHeaderMinMax.includes(column_id);
const verticalAlignPosition = sub_group_by ? false : kanbanFilters?.group_by.includes(column_id);
const [isOpen, setIsOpen] = React.useState(false);
const [openExistingIssueListModal, setOpenExistingIssueListModal] = React.useState(false);
@ -117,7 +117,7 @@ export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => {
{sub_group_by === null && (
<div
className="flex h-[20px] w-[20px] flex-shrink-0 cursor-pointer items-center justify-center overflow-hidden rounded-sm transition-all hover:bg-custom-background-80"
onClick={() => handleKanBanToggle("groupByHeaderMinMax", column_id)}
onClick={() => handleKanbanFilters("group_by", column_id)}
>
{verticalAlignPosition ? (
<Maximize2 width={14} strokeWidth={2} />

View file

@ -1,26 +1,26 @@
import React from "react";
// lucide icons
import { Circle, ChevronDown, ChevronUp } from "lucide-react";
// mobx
import { observer } from "mobx-react-lite";
import { TIssueKanbanFilters } from "@plane/types";
interface IHeaderSubGroupByCard {
icon?: React.ReactNode;
title: string;
count: number;
column_id: string;
kanBanToggle: any;
handleKanBanToggle: any;
kanbanFilters: TIssueKanbanFilters;
handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void;
}
export const HeaderSubGroupByCard = observer(
({ icon, title, count, column_id, kanBanToggle, handleKanBanToggle }: IHeaderSubGroupByCard) => (
({ icon, title, count, column_id, kanbanFilters, handleKanbanFilters }: IHeaderSubGroupByCard) => (
<div className={`relative flex w-full flex-shrink-0 flex-row items-center gap-2 rounded-sm p-1.5`}>
<div
className="flex h-[20px] w-[20px] flex-shrink-0 cursor-pointer items-center justify-center overflow-hidden rounded-sm transition-all hover:bg-custom-background-80"
onClick={() => handleKanBanToggle("subgroupByIssuesVisibility", column_id)}
onClick={() => handleKanbanFilters("sub_group_by", column_id)}
>
{kanBanToggle?.subgroupByIssuesVisibility.includes(column_id) ? (
{kanbanFilters?.sub_group_by.includes(column_id) ? (
<ChevronDown width={14} strokeWidth={2} />
) : (
<ChevronUp width={14} strokeWidth={2} />

View file

@ -1,4 +1,8 @@
import { Droppable } from "@hello-pangea/dnd";
// hooks
import { useProjectState } from "hooks/store";
//components
import { KanbanIssueBlocksList, KanBanQuickAddIssueForm } from ".";
//types
import {
TGroupedIssues,
@ -9,10 +13,6 @@ import {
TUnGroupedIssues,
} from "@plane/types";
import { EIssueActions } from "../types";
// hooks
import { useProjectState } from "hooks/store";
//components
import { KanBanQuickAddIssueForm, KanbanIssueBlocksList } from ".";
interface IKanbanGroup {
groupId: string;
@ -35,7 +35,7 @@ interface IKanbanGroup {
viewId?: string;
disableIssueCreation?: boolean;
canEditProperties: (projectId: string | undefined) => boolean;
verticalPosition: any;
groupByVisibilityToggle: boolean;
}
export const KanbanGroup = (props: IKanbanGroup) => {
@ -46,7 +46,6 @@ export const KanbanGroup = (props: IKanbanGroup) => {
sub_group_by,
issuesMap,
displayProperties,
verticalPosition,
issueIds,
isDragDisabled,
handleIssues,
@ -56,80 +55,97 @@ export const KanbanGroup = (props: IKanbanGroup) => {
disableIssueCreation,
quickAddCallback,
viewId,
groupByVisibilityToggle,
} = props;
// hooks
const projectState = useProjectState();
const prePopulateQuickAddData = (groupByKey: string | null, value: string) => {
const prePopulateQuickAddData = (
groupByKey: string | null,
subGroupByKey: string | null,
groupValue: string,
subGroupValue: string
) => {
const defaultState = projectState.projectStates?.find((state) => state.default);
let preloadedData: object = { state_id: defaultState?.id };
if (groupByKey) {
if (groupByKey === "state") {
preloadedData = { ...preloadedData, state_id: value };
preloadedData = { ...preloadedData, state_id: groupValue };
} else if (groupByKey === "priority") {
preloadedData = { ...preloadedData, priority: value };
} else if (groupByKey === "labels" && value != "None") {
preloadedData = { ...preloadedData, label_ids: [value] };
} else if (groupByKey === "assignees" && value != "None") {
preloadedData = { ...preloadedData, assignee_ids: [value] };
preloadedData = { ...preloadedData, priority: groupValue };
} else if (groupByKey === "labels" && groupValue != "None") {
preloadedData = { ...preloadedData, label_ids: [groupValue] };
} else if (groupByKey === "assignees" && groupValue != "None") {
preloadedData = { ...preloadedData, assignee_ids: [groupValue] };
} else if (groupByKey === "created_by") {
preloadedData = { ...preloadedData };
} else {
preloadedData = { ...preloadedData, [groupByKey]: value };
preloadedData = { ...preloadedData, [groupByKey]: groupValue };
}
}
if (subGroupByKey) {
if (subGroupByKey === "state") {
preloadedData = { ...preloadedData, state_id: subGroupValue };
} else if (subGroupByKey === "priority") {
preloadedData = { ...preloadedData, priority: subGroupValue };
} else if (subGroupByKey === "labels" && subGroupValue != "None") {
preloadedData = { ...preloadedData, label_ids: [subGroupValue] };
} else if (subGroupByKey === "assignees" && subGroupValue != "None") {
preloadedData = { ...preloadedData, assignee_ids: [subGroupValue] };
} else if (subGroupByKey === "created_by") {
preloadedData = { ...preloadedData };
} else {
preloadedData = { ...preloadedData, [subGroupByKey]: subGroupValue };
}
}
return preloadedData;
};
const isGroupByCreatedBy = group_by === "created_by";
return (
<div className={`${verticalPosition ? `min-h-[150px] w-[0px] overflow-hidden` : `w-full transition-all`}`}>
<div className={`relative w-full h-full overflow-hidden transition-all`}>
<Droppable droppableId={`${groupId}__${sub_group_id}`}>
{(provided: any, snapshot: any) => (
<div
className={`relative h-full w-full transition-all ${
snapshot.isDraggingOver ? `bg-custom-background-80` : ``
}`}
sub_group_by ? `` : `overflow-hidden overflow-y-auto`
} ${snapshot.isDraggingOver ? `bg-custom-background-80` : ``}`}
{...provided.droppableProps}
ref={provided.innerRef}
>
{!verticalPosition ? (
<KanbanIssueBlocksList
sub_group_id={sub_group_id}
columnId={groupId}
issuesMap={issuesMap}
issueIds={(issueIds as TGroupedIssues)?.[groupId] || []}
displayProperties={displayProperties}
isDragDisabled={isDragDisabled}
handleIssues={handleIssues}
quickActions={quickActions}
canEditProperties={canEditProperties}
/>
) : null}
<KanbanIssueBlocksList
sub_group_id={sub_group_id}
columnId={groupId}
issuesMap={issuesMap}
issueIds={(issueIds as TGroupedIssues)?.[groupId] || []}
displayProperties={displayProperties}
isDragDisabled={isDragDisabled}
handleIssues={handleIssues}
quickActions={quickActions}
canEditProperties={canEditProperties}
/>
{provided.placeholder}
{enableQuickIssueCreate && !disableIssueCreation && (
<div className="w-full bg-custom-background-90 py-0.5 sticky bottom-0">
<KanBanQuickAddIssueForm
formKey="name"
groupId={groupId}
subGroupId={sub_group_id}
prePopulatedData={{
...(group_by && prePopulateQuickAddData(group_by, sub_group_by, groupId, sub_group_id)),
}}
quickAddCallback={quickAddCallback}
viewId={viewId}
/>
</div>
)}
</div>
)}
</Droppable>
<div className="sticky bottom-0 z-[0] w-full flex-shrink-0 bg-custom-background-90 py-1">
{enableQuickIssueCreate && !disableIssueCreation && !isGroupByCreatedBy && (
<KanBanQuickAddIssueForm
formKey="name"
groupId={groupId}
subGroupId={sub_group_id}
prePopulatedData={{
...(group_by && prePopulateQuickAddData(group_by, groupId)),
...(sub_group_by && sub_group_id !== "null" && { [sub_group_by]: sub_group_id }),
}}
quickAddCallback={quickAddCallback}
viewId={viewId}
/>
)}
</div>
</div>
);
};

View file

@ -120,13 +120,13 @@ export const KanBanQuickAddIssueForm: React.FC<IKanBanQuickAddIssueForm> = obser
};
return (
<div>
<>
{isOpen ? (
<div className="shadow-custom-shadow-sm">
<div className="shadow-custom-shadow-sm m-1.5 rounded overflow-hidden">
<form
ref={ref}
onSubmit={handleSubmit(onSubmitHandler)}
className="flex w-full items-center gap-x-3 border-[0.5px] border-t-0 border-custom-border-100 bg-custom-background-100 px-3"
className="flex w-full items-center gap-x-3 bg-custom-background-100 p-3"
>
<Inputs formKey={formKey} register={register} setFocus={setFocus} projectDetail={projectDetail} />
</form>
@ -141,6 +141,6 @@ export const KanBanQuickAddIssueForm: React.FC<IKanBanQuickAddIssueForm> = obser
<span className="text-sm font-medium text-custom-primary-100">New Issue</span>
</div>
)}
</div>
</>
);
});

View file

@ -13,6 +13,7 @@ import {
IIssueMap,
TSubGroupedIssues,
TUnGroupedIssues,
TIssueKanbanFilters,
} from "@plane/types";
// constants
import { EIssueActions } from "../types";
@ -25,16 +26,16 @@ interface ISubGroupSwimlaneHeader {
sub_group_by: string | null;
group_by: string | null;
list: IGroupByColumn[];
kanBanToggle: any;
handleKanBanToggle: any;
kanbanFilters: TIssueKanbanFilters;
handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void;
}
const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({
issueIds,
sub_group_by,
group_by,
list,
kanBanToggle,
handleKanBanToggle,
kanbanFilters,
handleKanbanFilters,
}) => (
<div className="relative flex h-max min-h-full w-full items-center">
{list &&
@ -45,11 +46,11 @@ const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({
sub_group_by={sub_group_by}
group_by={group_by}
column_id={_list.id}
icon={_list.Icon}
icon={_list.icon}
title={_list.name}
count={(issueIds as TGroupedIssues)?.[_list.id]?.length || 0}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
kanbanFilters={kanbanFilters}
handleKanbanFilters={handleKanbanFilters}
issuePayload={_list.payload}
/>
</div>
@ -64,8 +65,8 @@ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader {
displayProperties: IIssueDisplayProperties | undefined;
handleIssues: (issue: TIssue, action: EIssueActions) => void;
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
kanBanToggle: any;
handleKanBanToggle: any;
kanbanFilters: TIssueKanbanFilters;
handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void;
isDragStarted?: boolean;
disableIssueCreation?: boolean;
currentStore?: TCreateModalStoreTypes;
@ -90,8 +91,8 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
handleIssues,
quickActions,
displayProperties,
kanBanToggle,
handleKanBanToggle,
kanbanFilters,
handleKanbanFilters,
showEmptyGroup,
enableQuickIssueCreate,
canEditProperties,
@ -123,13 +124,14 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
icon={_list.Icon}
title={_list.name || ""}
count={calculateIssueCount(_list.id)}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
kanbanFilters={kanbanFilters}
handleKanbanFilters={handleKanbanFilters}
/>
</div>
<div className="w-full border-b border-dashed border-custom-border-400" />
</div>
{!kanBanToggle?.subgroupByIssuesVisibility.includes(_list.id) && (
{!kanbanFilters?.sub_group_by.includes(_list.id) && (
<div className="relative">
<KanBan
issuesMap={issuesMap}
@ -140,8 +142,8 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
sub_group_id={_list.id}
handleIssues={handleIssues}
quickActions={quickActions}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
kanbanFilters={kanbanFilters}
handleKanbanFilters={handleKanbanFilters}
showEmptyGroup={showEmptyGroup}
enableQuickIssueCreate={enableQuickIssueCreate}
canEditProperties={canEditProperties}
@ -165,8 +167,8 @@ export interface IKanBanSwimLanes {
group_by: string | null;
handleIssues: (issue: TIssue, action: EIssueActions) => void;
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
kanBanToggle: any;
handleKanBanToggle: any;
kanbanFilters: TIssueKanbanFilters;
handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void;
showEmptyGroup: boolean;
isDragStarted?: boolean;
disableIssueCreation?: boolean;
@ -192,8 +194,8 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
group_by,
handleIssues,
quickActions,
kanBanToggle,
handleKanBanToggle,
kanbanFilters,
handleKanbanFilters,
showEmptyGroup,
isDragStarted,
disableIssueCreation,
@ -227,8 +229,8 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
issueIds={issueIds}
group_by={group_by}
sub_group_by={sub_group_by}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
kanbanFilters={kanbanFilters}
handleKanbanFilters={handleKanbanFilters}
list={groupByList}
/>
</div>
@ -243,8 +245,8 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
sub_group_by={sub_group_by}
handleIssues={handleIssues}
quickActions={quickActions}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
kanbanFilters={kanbanFilters}
handleKanbanFilters={handleKanbanFilters}
showEmptyGroup={showEmptyGroup}
isDragStarted={isDragStarted}
disableIssueCreation={disableIssueCreation}

View file

@ -8,6 +8,41 @@ import { IProjectViewIssues } from "store/issue/project-views";
import { IWorkspaceIssues } from "store/issue/workspace";
import { TGroupedIssues, IIssueMap, TSubGroupedIssues, TUnGroupedIssues } from "@plane/types";
const handleSortOrder = (destinationIssues: string[], destinationIndex: number, issueMap: IIssueMap) => {
const sortOrderDefaultValue = 65535;
let currentIssueState = {};
if (destinationIssues && destinationIssues.length > 0) {
if (destinationIndex === 0) {
const destinationIssueId = destinationIssues[destinationIndex];
currentIssueState = {
...currentIssueState,
sort_order: issueMap[destinationIssueId].sort_order - sortOrderDefaultValue,
};
} else if (destinationIndex === destinationIssues.length) {
const destinationIssueId = destinationIssues[destinationIndex - 1];
currentIssueState = {
...currentIssueState,
sort_order: issueMap[destinationIssueId].sort_order + sortOrderDefaultValue,
};
} else {
const destinationTopIssueId = destinationIssues[destinationIndex - 1];
const destinationBottomIssueId = destinationIssues[destinationIndex];
currentIssueState = {
...currentIssueState,
sort_order: (issueMap[destinationTopIssueId].sort_order + issueMap[destinationBottomIssueId].sort_order) / 2,
};
}
} else {
currentIssueState = {
...currentIssueState,
sort_order: sortOrderDefaultValue,
};
}
return currentIssueState;
};
export const handleDragDrop = async (
source: DraggableLocation | null | undefined,
destination: DraggableLocation | null | undefined,
@ -50,7 +85,7 @@ export const handleDragDrop = async (
!sourceGroupByColumnId ||
!destinationGroupByColumnId ||
!sourceSubGroupByColumnId ||
!sourceGroupByColumnId
!destinationSubGroupByColumnId
)
return;
@ -76,92 +111,49 @@ export const handleDragDrop = async (
const [removed] = sourceIssues.splice(source.index, 1);
const removedIssueDetail = issueMap[removed];
updateIssue = {
id: removedIssueDetail?.id,
project_id: removedIssueDetail?.project_id,
};
// for both horizontal and vertical dnd
updateIssue = {
...updateIssue,
...handleSortOrder(destinationIssues, destination.index, issueMap),
};
if (subGroupBy && sourceSubGroupByColumnId && destinationSubGroupByColumnId) {
updateIssue = {
id: removedIssueDetail?.id,
};
// for both horizontal and vertical dnd
updateIssue = {
...updateIssue,
...handleSortOrder(destinationIssues, destination.index, issueMap),
};
if (sourceSubGroupByColumnId === destinationSubGroupByColumnId) {
if (sourceGroupByColumnId != destinationGroupByColumnId) {
if (groupBy === "state") updateIssue = { ...updateIssue, state: destinationGroupByColumnId };
if (groupBy === "state") updateIssue = { ...updateIssue, state_id: destinationGroupByColumnId };
if (groupBy === "priority") updateIssue = { ...updateIssue, priority: destinationGroupByColumnId };
}
} else {
if (subGroupBy === "state")
updateIssue = {
...updateIssue,
state: destinationSubGroupByColumnId,
state_id: destinationSubGroupByColumnId,
priority: destinationGroupByColumnId,
};
if (subGroupBy === "priority")
updateIssue = {
...updateIssue,
state: destinationGroupByColumnId,
state_id: destinationGroupByColumnId,
priority: destinationSubGroupByColumnId,
};
}
} else {
updateIssue = {
id: removedIssueDetail?.id,
};
// for both horizontal and vertical dnd
updateIssue = {
...updateIssue,
...handleSortOrder(destinationIssues, destination.index, issueMap),
};
// for horizontal dnd
if (sourceColumnId != destinationColumnId) {
if (groupBy === "state") updateIssue = { ...updateIssue, state: destinationGroupByColumnId };
if (groupBy === "state") updateIssue = { ...updateIssue, state_id: destinationGroupByColumnId };
if (groupBy === "priority") updateIssue = { ...updateIssue, priority: destinationGroupByColumnId };
}
}
if (updateIssue && updateIssue?.id) {
if (viewId) return await store?.updateIssue(workspaceSlug, projectId, updateIssue.id, updateIssue); //, viewId);
else return await store?.updateIssue(workspaceSlug, projectId, updateIssue.id, updateIssue);
if (viewId)
return await store?.updateIssue(workspaceSlug, updateIssue.project_id, updateIssue.id, updateIssue, viewId);
else return await store?.updateIssue(workspaceSlug, updateIssue.project_id, updateIssue.id, updateIssue);
}
}
};
const handleSortOrder = (destinationIssues: string[], destinationIndex: number, issueMap: IIssueMap) => {
const sortOrderDefaultValue = 65535;
let currentIssueState = {};
if (destinationIssues && destinationIssues.length > 0) {
if (destinationIndex === 0) {
const destinationIssueId = destinationIssues[destinationIndex];
currentIssueState = {
...currentIssueState,
sort_order: issueMap[destinationIssueId].sort_order - sortOrderDefaultValue,
};
} else if (destinationIndex === destinationIssues.length) {
const destinationIssueId = destinationIssues[destinationIndex - 1];
currentIssueState = {
...currentIssueState,
sort_order: issueMap[destinationIssueId].sort_order + sortOrderDefaultValue,
};
} else {
const destinationTopIssueId = destinationIssues[destinationIndex - 1];
const destinationBottomIssueId = destinationIssues[destinationIndex];
currentIssueState = {
...currentIssueState,
sort_order: (issueMap[destinationTopIssueId].sort_order + issueMap[destinationBottomIssueId].sort_order) / 2,
};
}
} else {
currentIssueState = {
...currentIssueState,
sort_order: sortOrderDefaultValue,
};
}
return currentIssueState;
};

View file

@ -138,15 +138,6 @@ export const BaseListRoot = observer((props: IBaseListRoot) => {
addIssuesToView={addIssuesToView}
/>
</div>
{/* {workspaceSlug && peekIssueId && peekProjectId && (
<IssuePeekOverview
workspaceSlug={workspaceSlug.toString()}
projectId={peekProjectId.toString()}
issueId={peekIssueId.toString()}
handleIssue={async (issueToUpdate) => await handleIssues(issueToUpdate as TIssue, EIssueActions.UPDATE)}
/>
)} */}
</>
);
});

View file

@ -62,7 +62,7 @@ export const IssueBlock: React.FC<IssueBlockProps> = observer((props: IssueBlock
href={`/${workspaceSlug}/projects/${projectId}/issues/${issueId}`}
target="_blank"
onClick={() => handleIssuePeekOverview(issue)}
className="w-full line-clamp-1 cursor-pointer text-sm font-medium text-custom-text-100"
className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100"
>
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
<span>{issue.name}</span>

View file

@ -1,7 +1,7 @@
import { observer } from "mobx-react-lite";
import { CalendarCheck2, CalendarClock, Layers, Link, Paperclip } from "lucide-react";
// hooks
import { useLabel } from "hooks/store";
import { useEstimate, useLabel } from "hooks/store";
// components
import { IssuePropertyLabels } from "../properties/labels";
import { Tooltip } from "@plane/ui";
@ -29,6 +29,7 @@ export interface IIssueProperties {
export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
const { issue, handleIssues, displayProperties, isReadOnly, className } = props;
const { labelMap } = useLabel();
const { areEstimatesEnabledForCurrentProject } = useEstimate();
const handleState = (stateId: string) => {
handleIssues({ ...issue, state_id: stateId });
@ -92,7 +93,6 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
</WithDisplayPropertiesHOC>
{/* label */}
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="labels">
<IssuePropertyLabels
projectId={issue?.project_id || null}
@ -122,6 +122,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="due_date">
<div className="h-5">
<DateDropdown
minDate={issue.start_date ? new Date(issue.start_date) : undefined}
value={issue?.target_date ?? null}
onChange={handleTargetDate}
icon={<CalendarCheck2 className="h-3 w-3 flex-shrink-0" />}
@ -148,17 +149,19 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
</WithDisplayPropertiesHOC>
{/* estimates */}
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="estimate">
<div className="h-5">
<EstimateDropdown
value={issue.estimate_point}
onChange={handleEstimate}
projectId={issue.project_id}
disabled={isReadOnly}
buttonVariant="border-with-text"
/>
</div>
</WithDisplayPropertiesHOC>
{areEstimatesEnabledForCurrentProject && (
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="estimate">
<div className="h-5">
<EstimateDropdown
value={issue.estimate_point}
onChange={handleEstimate}
projectId={issue.project_id}
disabled={isReadOnly}
buttonVariant="border-with-text"
/>
</div>
</WithDisplayPropertiesHOC>
)}
{/* extra render properties */}
{/* sub-issues */}

View file

@ -5,31 +5,53 @@ import useSWR from "swr";
// mobx store
import { useIssues } from "hooks/store";
// components
import { ArchivedIssueListLayout, ArchivedIssueAppliedFiltersRoot } from "components/issues";
import { ArchivedIssueListLayout, ArchivedIssueAppliedFiltersRoot, ProjectEmptyState } from "components/issues";
import { EIssuesStoreType } from "constants/issue";
// ui
import { Spinner } from "@plane/ui";
export const ArchivedIssueLayoutRoot: React.FC = observer(() => {
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string };
const { workspaceSlug, projectId } = router.query;
// hooks
const { issues, issuesFilter } = useIssues(EIssuesStoreType.ARCHIVED);
const {
issues: { groupedIssueIds, fetchIssues },
issuesFilter: { fetchFilters },
} = useIssues(EIssuesStoreType.ARCHIVED);
useSWR(workspaceSlug && projectId ? `ARCHIVED_FILTERS_AND_ISSUES_${projectId.toString()}` : null, async () => {
if (workspaceSlug && projectId) {
await fetchFilters(workspaceSlug, projectId);
await fetchIssues(workspaceSlug, projectId, groupedIssueIds ? "mutation" : "init-loader");
useSWR(
workspaceSlug && projectId ? `ARCHIVED_ISSUES_${workspaceSlug.toString()}_${projectId.toString()}` : null,
async () => {
if (workspaceSlug && projectId) {
await issuesFilter?.fetchFilters(workspaceSlug.toString(), projectId.toString());
await issues?.fetchIssues(
workspaceSlug.toString(),
projectId.toString(),
issues?.groupedIssueIds ? "mutation" : "init-loader"
);
}
}
});
);
if (!workspaceSlug || !projectId) return <></>;
return (
<div className="relative flex h-full w-full flex-col overflow-hidden">
<ArchivedIssueAppliedFiltersRoot />
<div className="h-full w-full overflow-auto">
<ArchivedIssueListLayout />
</div>
{issues?.loader === "init-loader" ? (
<div className="flex h-full w-full items-center justify-center">
<Spinner />
</div>
) : (
<>
{!issues?.groupedIssueIds ? (
// TODO: Replace this with project view empty state
<ProjectEmptyState />
) : (
<div className="relative h-full w-full overflow-auto">
<ArchivedIssueListLayout />
</div>
)}
</>
)}
</div>
);
});

View file

@ -21,36 +21,37 @@ import { Spinner } from "@plane/ui";
import { EIssuesStoreType } from "constants/issue";
export const CycleLayoutRoot: React.FC = observer(() => {
const router = useRouter();
const { workspaceSlug, projectId, cycleId } = router.query;
// store hooks
const { issues, issuesFilter } = useIssues(EIssuesStoreType.CYCLE);
const { getCycleById } = useCycle();
// state
const [transferIssuesModal, setTransferIssuesModal] = useState(false);
const router = useRouter();
const { workspaceSlug, projectId, cycleId } = router.query as {
workspaceSlug: string;
projectId: string;
cycleId: string;
};
// store hooks
const {
issues: { loader, groupedIssueIds, fetchIssues },
issuesFilter: { issueFilters, fetchFilters },
} = useIssues(EIssuesStoreType.CYCLE);
const { getCycleById } = useCycle();
useSWR(
workspaceSlug && projectId && cycleId ? `CYCLE_ISSUES_V3_${workspaceSlug}_${projectId}_${cycleId}` : null,
workspaceSlug && projectId && cycleId
? `CYCLE_ISSUES_${workspaceSlug.toString()}_${projectId.toString()}_${cycleId.toString()}`
: null,
async () => {
if (workspaceSlug && projectId && cycleId) {
await fetchFilters(workspaceSlug, projectId, cycleId);
await fetchIssues(workspaceSlug, projectId, groupedIssueIds ? "mutation" : "init-loader", cycleId);
await issuesFilter?.fetchFilters(workspaceSlug.toString(), projectId.toString(), cycleId.toString());
await issues?.fetchIssues(
workspaceSlug.toString(),
projectId.toString(),
issues?.groupedIssueIds ? "mutation" : "init-loader",
cycleId.toString()
);
}
}
);
const activeLayout = issueFilters?.displayFilters?.layout;
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
const cycleDetails = cycleId ? getCycleById(cycleId) : undefined;
const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined;
const cycleStatus = cycleDetails?.status.toLocaleLowerCase() ?? "draft";
if (!workspaceSlug || !projectId || !cycleId) return <></>;
return (
<>
<TransferIssuesModal handleClose={() => setTransferIssuesModal(false)} isOpen={transferIssuesModal} />
@ -59,14 +60,18 @@ export const CycleLayoutRoot: React.FC = observer(() => {
{cycleStatus === "completed" && <TransferIssues handleClick={() => setTransferIssuesModal(true)} />}
<CycleAppliedFiltersRoot />
{loader === "init-loader" || !groupedIssueIds ? (
{issues?.loader === "init-loader" ? (
<div className="flex h-full w-full items-center justify-center">
<Spinner />
</div>
) : (
<>
{Object.keys(groupedIssueIds ?? {}).length == 0 ? (
<CycleEmptyState workspaceSlug={workspaceSlug} projectId={projectId} cycleId={cycleId} />
{!issues?.groupedIssueIds ? (
<CycleEmptyState
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
cycleId={cycleId.toString()}
/>
) : (
<div className="h-full w-full overflow-auto">
{activeLayout === "list" ? (

View file

@ -2,49 +2,64 @@ import React from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
// mobx store
// hooks
import { useIssues } from "hooks/store";
// components
import { DraftIssueAppliedFiltersRoot } from "../filters/applied-filters/roots/draft-issue";
import { DraftIssueListLayout } from "../list/roots/draft-issue-root";
import { ProjectEmptyState } from "../empty-states";
// ui
import { Spinner } from "@plane/ui";
import { DraftKanBanLayout } from "../kanban/roots/draft-issue-root";
// constants
import { EIssuesStoreType } from "constants/issue";
export const DraftIssueLayoutRoot: React.FC = observer(() => {
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string };
const { workspaceSlug, projectId } = router.query;
// hooks
const { issues, issuesFilter } = useIssues(EIssuesStoreType.DRAFT);
const {
issues: { loader, groupedIssueIds, fetchIssues },
issuesFilter: { issueFilters, fetchFilters },
} = useIssues(EIssuesStoreType.DRAFT);
useSWR(workspaceSlug && projectId ? `DRAFT_FILTERS_AND_ISSUES_${projectId.toString()}` : null, async () => {
if (workspaceSlug && projectId) {
await fetchFilters(workspaceSlug, projectId);
await fetchIssues(workspaceSlug, projectId, groupedIssueIds ? "mutation" : "init-loader");
useSWR(
workspaceSlug && projectId ? `DRAFT_ISSUES_${workspaceSlug.toString()}_${projectId.toString()}` : null,
async () => {
if (workspaceSlug && projectId) {
await issuesFilter?.fetchFilters(workspaceSlug.toString(), projectId.toString());
await issues?.fetchIssues(
workspaceSlug.toString(),
projectId.toString(),
issues?.groupedIssueIds ? "mutation" : "init-loader"
);
}
}
});
);
const activeLayout = issueFilters?.displayFilters?.layout || undefined;
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout || undefined;
if (!workspaceSlug || !projectId) return <></>;
return (
<div className="relative flex h-full w-full flex-col overflow-hidden">
<DraftIssueAppliedFiltersRoot />
{loader === "init-loader" ? (
{issues?.loader === "init-loader" ? (
<div className="flex h-full w-full items-center justify-center">
<Spinner />
</div>
) : (
<>
<div className="relative h-full w-full overflow-auto">
{activeLayout === "list" ? (
<DraftIssueListLayout />
) : activeLayout === "kanban" ? (
<DraftKanBanLayout />
) : null}
</div>
{!issues?.groupedIssueIds ? (
// TODO: Replace this with project view empty state
<ProjectEmptyState />
) : (
<div className="relative h-full w-full overflow-auto">
{activeLayout === "list" ? (
<DraftIssueListLayout />
) : activeLayout === "kanban" ? (
<DraftKanBanLayout />
) : null}
</div>
)}
</>
)}
</div>

View file

@ -1,6 +1,7 @@
export * from "./cycle-layout-root";
export * from "./all-issue-layout-root";
export * from "./module-layout-root";
export * from "./project-layout-root";
export * from "./module-layout-root";
export * from "./cycle-layout-root";
export * from "./project-view-layout-root";
export * from "./archived-issue-layout-root";
export * from "./draft-issue-layout-root";
export * from "./all-issue-layout-root";

View file

@ -2,7 +2,6 @@ import React from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
// mobx store
import { useIssues } from "hooks/store";
// components
@ -20,42 +19,48 @@ import { Spinner } from "@plane/ui";
import { EIssuesStoreType } from "constants/issue";
export const ModuleLayoutRoot: React.FC = observer(() => {
// router
const router = useRouter();
const { workspaceSlug, projectId, moduleId } = router.query as {
workspaceSlug: string;
projectId: string;
moduleId: string;
};
const {
issues: { loader, groupedIssueIds, fetchIssues },
issuesFilter: { issueFilters, fetchFilters },
} = useIssues(EIssuesStoreType.MODULE);
const { workspaceSlug, projectId, moduleId } = router.query;
// hooks
const { issues, issuesFilter } = useIssues(EIssuesStoreType.MODULE);
useSWR(
workspaceSlug && projectId && moduleId ? `MODULE_ISSUES_V3_${workspaceSlug}_${projectId}_${moduleId}` : null,
workspaceSlug && projectId && moduleId
? `MODULE_ISSUES_${workspaceSlug.toString()}_${projectId.toString()}_${moduleId.toString()}`
: null,
async () => {
if (workspaceSlug && projectId && moduleId) {
await fetchFilters(workspaceSlug, projectId, moduleId);
await fetchIssues(workspaceSlug, projectId, groupedIssueIds ? "mutation" : "init-loader", moduleId);
await issuesFilter?.fetchFilters(workspaceSlug.toString(), projectId.toString(), moduleId.toString());
await issues?.fetchIssues(
workspaceSlug.toString(),
projectId.toString(),
issues?.groupedIssueIds ? "mutation" : "init-loader",
moduleId.toString()
);
}
}
);
const activeLayout = issueFilters?.displayFilters?.layout || undefined;
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout || undefined;
if (!workspaceSlug || !projectId || !moduleId) return <></>;
return (
<div className="relative flex h-full w-full flex-col overflow-hidden">
<ModuleAppliedFiltersRoot />
{loader === "init-loader" || !groupedIssueIds ? (
{issues?.loader === "init-loader" ? (
<div className="flex h-full w-full items-center justify-center">
<Spinner />
</div>
) : (
<>
{Object.keys(groupedIssueIds ?? {}).length == 0 ? (
<ModuleEmptyState workspaceSlug={workspaceSlug} projectId={projectId} moduleId={moduleId} />
{!issues?.groupedIssueIds ? (
<ModuleEmptyState
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
moduleId={moduleId.toString()}
/>
) : (
<div className="h-full w-full overflow-auto">
{activeLayout === "list" ? (
@ -71,7 +76,6 @@ export const ModuleLayoutRoot: React.FC = observer(() => {
) : null}
</div>
)}
{/* <ModuleEmptyState workspaceSlug={workspaceSlug} projectId={projectId} moduleId={moduleId} /> */}
</>
)}
</div>

View file

@ -1,4 +1,5 @@
import { FC } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
// components
@ -15,23 +16,27 @@ import {
// ui
import { Spinner } from "@plane/ui";
// hooks
import { useApplication, useIssues } from "hooks/store";
import { useIssues } from "hooks/store";
// constants
import { EIssuesStoreType } from "constants/issue";
export const ProjectLayoutRoot: FC = observer(() => {
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// hooks
const {
router: { workspaceSlug, projectId },
} = useApplication();
const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT);
useSWR(
const {} = useSWR(
workspaceSlug && projectId ? `PROJECT_ISSUES_${workspaceSlug}_${projectId}` : null,
async () => {
if (workspaceSlug && projectId) {
await issuesFilter?.fetchFilters(workspaceSlug, projectId);
await issues?.fetchIssues(workspaceSlug, projectId, issues?.groupedIssueIds ? "mutation" : "init-loader");
await issuesFilter?.fetchFilters(workspaceSlug.toString(), projectId.toString());
await issues?.fetchIssues(
workspaceSlug.toString(),
projectId.toString(),
issues?.groupedIssueIds ? "mutation" : "init-loader"
);
}
},
{ revalidateOnFocus: false, refreshInterval: 600000, revalidateOnMount: true }
@ -39,6 +44,7 @@ export const ProjectLayoutRoot: FC = observer(() => {
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
if (!workspaceSlug || !projectId) return <></>;
return (
<div className="relative flex h-full w-full flex-col overflow-hidden">
<ProjectAppliedFiltersRoot />
@ -56,6 +62,13 @@ export const ProjectLayoutRoot: FC = observer(() => {
) : (
<>
<div className="relative h-full w-full overflow-auto bg-custom-background-90">
{/* mutation loader */}
{issues?.loader === "mutation" && (
<div className="fixed w-[40px] h-[40px] z-50 right-[20px] top-[70px] flex justify-center items-center bg-custom-background-80 shadow-sm rounded">
<Spinner className="w-4 h-4" />
</div>
)}
{activeLayout === "list" ? (
<ListLayout />
) : activeLayout === "kanban" ? (

View file

@ -6,6 +6,7 @@ import useSWR from "swr";
import { useIssues } from "hooks/store";
// components
import {
ProjectEmptyState,
ProjectViewAppliedFiltersRoot,
ProjectViewCalendarLayout,
ProjectViewGanttLayout,
@ -17,53 +18,58 @@ import { Spinner } from "@plane/ui";
import { EIssuesStoreType } from "constants/issue";
export const ProjectViewLayoutRoot: React.FC = observer(() => {
// router
const router = useRouter();
const { workspaceSlug, projectId, viewId } = router.query as {
workspaceSlug: string;
projectId: string;
viewId?: string;
};
const {
issues: { loader, groupedIssueIds, fetchIssues },
issuesFilter: { issueFilters, fetchFilters },
} = useIssues(EIssuesStoreType.PROJECT_VIEW);
const { workspaceSlug, projectId, viewId } = router.query;
// hooks
const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT_VIEW);
useSWR(
workspaceSlug && projectId && viewId ? `PROJECT_VIEW_ISSUES_${workspaceSlug}_${projectId}` : null,
async () => {
if (workspaceSlug && projectId && viewId) {
await fetchFilters(workspaceSlug, projectId, viewId);
await fetchIssues(workspaceSlug, projectId, groupedIssueIds ? "mutation" : "init-loader");
await issuesFilter?.fetchFilters(workspaceSlug.toString(), projectId.toString(), viewId.toString());
await issues?.fetchIssues(
workspaceSlug.toString(),
projectId.toString(),
issues?.groupedIssueIds ? "mutation" : "init-loader",
viewId.toString()
);
}
}
);
const activeLayout = issueFilters?.displayFilters?.layout;
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
if (!workspaceSlug || !projectId || !viewId) return <></>;
return (
<div className="relative flex h-full w-full flex-col overflow-hidden">
<ProjectViewAppliedFiltersRoot />
{loader === "init-loader" ? (
{issues?.loader === "init-loader" ? (
<div className="flex h-full w-full items-center justify-center">
<Spinner />
</div>
) : (
<>
<div className="relative h-full w-full overflow-auto">
{activeLayout === "list" ? (
<ProjectViewListLayout />
) : activeLayout === "kanban" ? (
<ProjectViewKanBanLayout />
) : activeLayout === "calendar" ? (
<ProjectViewCalendarLayout />
) : activeLayout === "gantt_chart" ? (
<ProjectViewGanttLayout />
) : activeLayout === "spreadsheet" ? (
<ProjectViewSpreadsheetLayout />
) : null}
</div>
{!issues?.groupedIssueIds ? (
// TODO: Replace this with project view empty state
<ProjectEmptyState />
) : (
<div className="relative h-full w-full overflow-auto">
{activeLayout === "list" ? (
<ProjectViewListLayout />
) : activeLayout === "kanban" ? (
<ProjectViewKanBanLayout />
) : activeLayout === "calendar" ? (
<ProjectViewCalendarLayout />
) : activeLayout === "gantt_chart" ? (
<ProjectViewGanttLayout />
) : activeLayout === "spreadsheet" ? (
<ProjectViewSpreadsheetLayout />
) : null}
</div>
)}
</>
)}
</div>

View file

@ -31,7 +31,7 @@ export const getGroupByColumns = (
case "created_by":
return getCreatedByColumns(member) as any;
default:
if (includeNone) return [{ id: `null`, name: `All Issues`, payload: {}, Icon: undefined }];
if (includeNone) return [{ id: `null`, name: `All Issues`, payload: {}, icon: undefined }];
}
};
@ -48,7 +48,7 @@ const getProjectColumns = (project: IProjectStore): IGroupByColumn[] | undefined
return {
id: project.id,
name: project.name,
Icon: <div className="w-6 h-6">{renderEmoji(project.emoji || "")}</div>,
icon: <div className="w-6 h-6">{renderEmoji(project.emoji || "")}</div>,
payload: { project_id: project.id },
};
}) as any;
@ -61,7 +61,7 @@ const getStateColumns = (projectState: IStateStore): IGroupByColumn[] | undefine
return projectStates.map((state) => ({
id: state.id,
name: state.name,
Icon: (
icon: (
<div className="w-3.5 h-3.5 rounded-full">
<StateGroupIcon stateGroup={state.group} color={state.color} width="14" height="14" />
</div>
@ -76,7 +76,7 @@ const getStateGroupColumns = () => {
return stateGroups.map((stateGroup) => ({
id: stateGroup.key,
name: stateGroup.title,
Icon: (
icon: (
<div className="w-3.5 h-3.5 rounded-full">
<StateGroupIcon stateGroup={stateGroup.key} width="14" height="14" />
</div>
@ -91,7 +91,7 @@ const getPriorityColumns = () => {
return priorities.map((priority) => ({
id: priority.key,
name: priority.title,
Icon: <PriorityIcon priority={priority?.key} />,
icon: <PriorityIcon priority={priority?.key} />,
payload: { priority: priority.key },
}));
};
@ -108,7 +108,7 @@ const getLabelsColumns = (projectLabel: ILabelRootStore) => {
return labels.map((label) => ({
id: label.id,
name: label.name,
Icon: (
icon: (
<div className="w-[12px] h-[12px] rounded-full" style={{ backgroundColor: label.color ? label.color : "#666" }} />
),
payload: label?.id === "None" ? {} : { label_ids: [label.id] },
@ -128,12 +128,12 @@ const getAssigneeColumns = (member: IMemberRootStore) => {
return {
id: memberId,
name: member?.display_name || "",
Icon: <Avatar name={member?.display_name} src={member?.avatar} size="md" />,
icon: <Avatar name={member?.display_name} src={member?.avatar} size="md" />,
payload: { assignee_ids: [memberId] },
};
});
assigneeColumns.push({ id: "None", name: "None", Icon: <Avatar size="md" />, payload: {} });
assigneeColumns.push({ id: "None", name: "None", icon: <Avatar size="md" />, payload: {} });
return assigneeColumns;
};
@ -151,7 +151,7 @@ const getCreatedByColumns = (member: IMemberRootStore) => {
return {
id: memberId,
name: member?.display_name || "",
Icon: <Avatar name={member?.display_name} src={member?.avatar} size="md" />,
icon: <Avatar name={member?.display_name} src={member?.avatar} size="md" />,
payload: {},
};
});