fix: issue layouts bugs and ui fixes (#3012)

* fix: initial issue creation issue in the list layout

* fix kanban drag n drop and updating properties

* reduce z index of spreadsheet bottom row to not overlap with other elements

* fix state update by using state id instead of state detail's id

* fix add default use state for description

* add create issue button for project views to be at par with production

* save draft issues from modal

* chore: added save view button in all layouts applied filters

* use useEffect instead of swr for fetching issue details for peek overview

* fix: resolved kanban dnd

---------

Co-authored-by: rahulramesha <rahulramesham@gmail.com>
This commit is contained in:
guru_sainath 2023-12-06 19:58:47 +05:30 committed by sriram veeraghanta
parent 55ce748aa1
commit e585255c4c
16 changed files with 213 additions and 148 deletions

View file

@ -4,14 +4,14 @@ import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { AppliedFiltersList } from "components/issues";
import { AppliedFiltersList, SaveFilterView } from "components/issues";
// types
import { IIssueFilterOptions } from "types";
import { EFilterType } from "store/issues/types";
export const ArchivedIssueAppliedFiltersRoot: React.FC = observer(() => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string };
const {
projectArchivedIssuesFilter: { issueFilters, updateFilters },
@ -69,7 +69,7 @@ export const ArchivedIssueAppliedFiltersRoot: React.FC = observer(() => {
if (Object.keys(appliedFilters).length === 0) return null;
return (
<div className="p-4">
<div className="p-4 flex items-center justify-between">
<AppliedFiltersList
appliedFilters={appliedFilters}
handleClearAllFilters={handleClearAllFilters}
@ -78,6 +78,8 @@ export const ArchivedIssueAppliedFiltersRoot: React.FC = observer(() => {
members={projectMembers?.map((m) => m.member)}
states={projectStateStore.states?.[projectId?.toString() ?? ""]}
/>
<SaveFilterView workspaceSlug={workspaceSlug} projectId={projectId} filterParams={appliedFilters} />
</div>
);
});

View file

@ -1,9 +1,10 @@
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { AppliedFiltersList } from "components/issues";
import { AppliedFiltersList, SaveFilterView } from "components/issues";
// types
import { IIssueFilterOptions } from "types";
import { EFilterType } from "store/issues/types";
@ -75,7 +76,7 @@ export const CycleAppliedFiltersRoot: React.FC = observer(() => {
if (Object.keys(appliedFilters).length === 0) return null;
return (
<div className="p-4">
<div className="p-4 flex items-center justify-between">
<AppliedFiltersList
appliedFilters={appliedFilters}
handleClearAllFilters={handleClearAllFilters}
@ -84,6 +85,8 @@ export const CycleAppliedFiltersRoot: React.FC = observer(() => {
members={projectMembers?.map((m) => m.member)}
states={projectStateStore.states?.[cycleId ?? ""]}
/>
<SaveFilterView workspaceSlug={workspaceSlug} projectId={projectId} filterParams={appliedFilters} />
</div>
);
});

View file

@ -4,14 +4,14 @@ import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { AppliedFiltersList } from "components/issues";
import { AppliedFiltersList, SaveFilterView } from "components/issues";
// types
import { IIssueFilterOptions } from "types";
import { EFilterType } from "store/issues/types";
export const DraftIssueAppliedFiltersRoot: React.FC = observer(() => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string };
const {
projectDraftIssuesFilter: { issueFilters, updateFilters },
@ -64,7 +64,7 @@ export const DraftIssueAppliedFiltersRoot: React.FC = observer(() => {
if (Object.keys(appliedFilters).length === 0) return null;
return (
<div className="p-4">
<div className="p-4 flex items-center justify-between">
<AppliedFiltersList
appliedFilters={appliedFilters}
handleClearAllFilters={handleClearAllFilters}
@ -73,6 +73,8 @@ export const DraftIssueAppliedFiltersRoot: React.FC = observer(() => {
members={projectMembers?.map((m) => m.member)}
states={projectStateStore.states?.[projectId?.toString() ?? ""]}
/>
<SaveFilterView workspaceSlug={workspaceSlug} projectId={projectId} filterParams={appliedFilters} />
</div>
);
});

View file

@ -4,7 +4,7 @@ import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { AppliedFiltersList } from "components/issues";
import { AppliedFiltersList, SaveFilterView } from "components/issues";
// types
import { IIssueFilterOptions } from "types";
import { EFilterType } from "store/issues/types";
@ -76,7 +76,7 @@ export const ModuleAppliedFiltersRoot: React.FC = observer(() => {
if (Object.keys(appliedFilters).length === 0) return null;
return (
<div className="p-4">
<div className="p-4 flex items-center justify-between">
<AppliedFiltersList
appliedFilters={appliedFilters}
handleClearAllFilters={handleClearAllFilters}
@ -85,6 +85,8 @@ export const ModuleAppliedFiltersRoot: React.FC = observer(() => {
members={projectMembers?.map((m) => m.member)}
states={projectStateStore.states?.[moduleId ?? ""]}
/>
<SaveFilterView workspaceSlug={workspaceSlug} projectId={projectId} filterParams={appliedFilters} />
</div>
);
});

View file

@ -4,14 +4,18 @@ import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { AppliedFiltersList } from "components/issues";
import { AppliedFiltersList, SaveFilterView } from "components/issues";
// types
import { IIssueFilterOptions } from "types";
import { EFilterType } from "store/issues/types";
export const ProjectAppliedFiltersRoot: React.FC = observer(() => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { workspaceSlug, projectId } = router.query as {
workspaceSlug: string;
projectId: string;
};
const {
projectLabel: { projectLabels },
@ -60,7 +64,7 @@ export const ProjectAppliedFiltersRoot: React.FC = observer(() => {
if (Object.keys(appliedFilters).length === 0) return null;
return (
<div className="p-4">
<div className="p-4 flex items-center justify-between">
<AppliedFiltersList
appliedFilters={appliedFilters}
handleClearAllFilters={handleClearAllFilters}
@ -69,6 +73,8 @@ export const ProjectAppliedFiltersRoot: React.FC = observer(() => {
members={projectMembers?.map((m) => m.member)}
states={projectStateStore.states?.[projectId?.toString() ?? ""]}
/>
<SaveFilterView workspaceSlug={workspaceSlug} projectId={projectId} filterParams={appliedFilters} />
</div>
);
});

View file

@ -31,7 +31,6 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => {
} = useMobxStore();
const viewDetails = viewId ? projectViewsStore.viewDetails[viewId.toString()] : undefined;
const storedFilters = viewId ? projectViewFiltersStore.storedFilters[viewId.toString()] : undefined;
const userFilters = issueFilters?.filters;
// filters whose value not null or empty array
@ -89,7 +88,7 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => {
projectViewsStore.updateView(workspaceSlug.toString(), projectId.toString(), viewId.toString(), {
query_data: {
...viewDetails.query_data,
...(storedFilters ?? {}),
...(appliedFilters ?? {}),
},
});
};
@ -104,13 +103,16 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => {
members={projectMembers?.map((m) => m.member)}
states={projectStateStore.states?.[projectId?.toString() ?? ""]}
/>
{storedFilters && viewDetails && areFiltersDifferent(storedFilters, viewDetails.query_data ?? {}) && (
<div className="flex items-center justify-center flex-shrink-0">
<Button variant="primary" size="sm" onClick={handleUpdateView}>
Update view
</Button>
</div>
)}
{appliedFilters &&
viewDetails?.query_data &&
areFiltersDifferent(appliedFilters, viewDetails?.query_data ?? {}) && (
<div className="flex items-center justify-center flex-shrink-0">
<Button variant="primary" size="sm" onClick={handleUpdateView}>
Update view
</Button>
</div>
)}
</div>
);
});

View file

@ -15,3 +15,6 @@ export * from "./spreadsheet";
// properties
export * from "./properties";
// save view
export * from "./save-filter-view";

View file

@ -1,16 +1,14 @@
import { memo, useRef, useState } from "react";
import { memo } from "react";
import { Draggable } from "@hello-pangea/dnd";
import isEqual from "lodash/isEqual";
// components
import { KanBanProperties } from "./properties";
// ui
import { Tooltip } from "@plane/ui";
// hooks
import useOutsideClickDetector from "hooks/use-outside-click-detector";
// types
import { IIssueDisplayProperties, IIssue } from "types";
import { EIssueActions } from "../types";
import { useRouter } from "next/router";
import { MoreHorizontal } from "lucide-react";
interface IssueBlockProps {
sub_group_id: string;
@ -20,37 +18,28 @@ interface IssueBlockProps {
isDragDisabled: boolean;
showEmptyGroup: boolean;
handleIssues: (sub_group_by: string | null, group_by: string | null, issue: IIssue, action: EIssueActions) => void;
quickActions: (
sub_group_by: string | null,
group_by: string | null,
issue: IIssue,
customActionButton?: React.ReactElement
) => React.ReactNode;
quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode;
displayProperties: IIssueDisplayProperties | null;
isReadOnly: boolean;
}
export const KanBanIssueMemoBlock: React.FC<IssueBlockProps> = (props) => {
const {
sub_group_id,
columnId,
index,
issue,
isDragDisabled,
showEmptyGroup,
handleIssues,
quickActions,
displayProperties,
isReadOnly,
} = props;
// router
interface IssueDetailsBlockProps {
sub_group_id: string;
columnId: string;
issue: IIssue;
showEmptyGroup: boolean;
handleIssues: (sub_group_by: string | null, group_by: string | null, issue: IIssue, action: EIssueActions) => void;
quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode;
displayProperties: IIssueDisplayProperties | null;
isReadOnly: boolean;
}
const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = (props) => {
const { sub_group_id, columnId, issue, showEmptyGroup, handleIssues, quickActions, displayProperties, isReadOnly } =
props;
const router = useRouter();
// states
const [isMenuActive, setIsMenuActive] = useState(false);
const menuActionRef = useRef<HTMLDivElement | null>(null);
const updateIssue = (sub_group_by: string | null, group_by: string | null, issueToUpdate: IIssue) => {
if (issueToUpdate) handleIssues(sub_group_by, group_by, issueToUpdate, EIssueActions.UPDATE);
};
@ -64,24 +53,70 @@ export const KanBanIssueMemoBlock: React.FC<IssueBlockProps> = (props) => {
});
};
return (
<>
{displayProperties && displayProperties?.key && (
<div className="relative">
<div className="text-xs line-clamp-1 text-custom-text-300">
{issue.project_detail.identifier}-{issue.sequence_id}
</div>
<div className="absolute -top-1 right-0 hidden group-hover/kanban-block:block">
{quickActions(
!sub_group_id && sub_group_id === "null" ? null : sub_group_id,
!columnId && columnId === "null" ? null : columnId,
issue
)}
</div>
</div>
)}
<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>
<div>
<KanBanProperties
sub_group_id={sub_group_id}
columnId={columnId}
issue={issue}
handleIssues={updateIssue}
displayProperties={displayProperties}
showEmptyGroup={showEmptyGroup}
isReadOnly={isReadOnly}
/>
</div>
</>
);
};
const validateMemo = (prevProps: IssueDetailsBlockProps, nextProps: IssueDetailsBlockProps) => {
if (prevProps.issue !== nextProps.issue) return false;
if (!isEqual(prevProps.displayProperties, nextProps.displayProperties)) {
return false;
}
return true;
};
const KanbanIssueMemoBlock = memo(KanbanIssueDetailsBlock, validateMemo);
export const KanbanIssueBlock: React.FC<IssueBlockProps> = (props) => {
const {
sub_group_id,
columnId,
index,
issue,
isDragDisabled,
showEmptyGroup,
handleIssues,
quickActions,
displayProperties,
isReadOnly,
} = props;
let draggableId = issue.id;
if (columnId) draggableId = `${draggableId}__${columnId}`;
if (sub_group_id) draggableId = `${draggableId}__${sub_group_id}`;
useOutsideClickDetector(menuActionRef, () => setIsMenuActive(false));
const customActionButton = (
<div
ref={menuActionRef}
className={`w-full cursor-pointer text-custom-sidebar-text-400 rounded p-1 hover:bg-custom-background-80 ${
isMenuActive ? "bg-custom-background-80 text-custom-text-100" : "text-custom-text-200"
}`}
onClick={() => setIsMenuActive(!isMenuActive)}
>
<MoreHorizontal className="h-3.5 w-3.5" />
</div>
);
return (
<>
<Draggable draggableId={draggableId} index={index}>
@ -100,44 +135,16 @@ export const KanBanIssueMemoBlock: React.FC<IssueBlockProps> = (props) => {
isDragDisabled ? "" : "hover:cursor-grab"
} ${snapshot.isDragging ? `border-custom-primary-100` : `border-transparent`}`}
>
{displayProperties && displayProperties?.key && (
<div className="relative">
<div className="text-xs line-clamp-1 text-custom-text-300">
{issue.project_detail.identifier}-{issue.sequence_id}
</div>
<div
className={`absolute -top-1 right-0 hidden group-hover/kanban-block:block ${
isMenuActive ? "!block" : ""
}`}
>
{quickActions(
!sub_group_id && sub_group_id === "null" ? null : sub_group_id,
!columnId && columnId === "null" ? null : columnId,
issue,
customActionButton
)}
</div>
</div>
)}
<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>
<div>
<KanBanProperties
sub_group_id={sub_group_id}
columnId={columnId}
issue={issue}
handleIssues={updateIssue}
displayProperties={displayProperties}
showEmptyGroup={showEmptyGroup}
isReadOnly={isReadOnly}
/>
</div>
<KanbanIssueMemoBlock
sub_group_id={sub_group_id}
columnId={columnId}
issue={issue}
showEmptyGroup={showEmptyGroup}
handleIssues={handleIssues}
quickActions={quickActions}
displayProperties={displayProperties}
isReadOnly={isReadOnly}
/>
</div>
</div>
)}
@ -145,10 +152,3 @@ export const KanBanIssueMemoBlock: React.FC<IssueBlockProps> = (props) => {
</>
);
};
const validateMemo = (prevProps: IssueBlockProps, nextProps: IssueBlockProps) => {
if (prevProps.issue != nextProps.issue) return true;
return false;
};
export const KanbanIssueBlock = memo(KanBanIssueMemoBlock, validateMemo);

View file

@ -22,17 +22,20 @@ export const IssueBlocksList: FC<Props> = (props) => {
return (
<div className="w-full h-full relative divide-y-[0.5px] divide-custom-border-200">
{issueIds && issueIds.length > 0 ? (
issueIds.map((issueId: string) => (
<IssueBlock
key={issues[issueId]?.id}
columnId={columnId}
issue={issues[issueId]}
handleIssues={handleIssues}
quickActions={quickActions}
isReadonly={isReadonly}
displayProperties={displayProperties}
/>
))
issueIds.map(
(issueId: string) =>
issues[issueId] && (
<IssueBlock
key={issues[issueId].id}
columnId={columnId}
issue={issues[issueId]}
handleIssues={handleIssues}
quickActions={quickActions}
isReadonly={isReadonly}
displayProperties={displayProperties}
/>
)
)
) : (
<div className="bg-custom-background-100 text-custom-text-400 text-sm p-3">No issues</div>
)}

View file

@ -0,0 +1,33 @@
import { FC, useState } from "react";
import { Plus } from "lucide-react";
import { Button } from "@plane/ui";
// components
import { CreateUpdateProjectViewModal } from "components/views";
interface ISaveFilterView {
workspaceSlug: string;
projectId: string;
filterParams: any;
}
export const SaveFilterView: FC<ISaveFilterView> = (props) => {
const { workspaceSlug, projectId, filterParams } = props;
const [viewModal, setViewModal] = useState<boolean>(false);
return (
<div>
<CreateUpdateProjectViewModal
workspaceSlug={workspaceSlug}
projectId={projectId}
preLoadedData={{ query_data: { ...filterParams } }}
isOpen={viewModal}
onClose={() => setViewModal(false)}
/>
<Button size="sm" prependIcon={<Plus />} onClick={() => setViewModal(true)}>
Save View
</Button>
</div>
);
};

View file

@ -26,7 +26,7 @@ export const SpreadsheetStateColumn: React.FC<Props> = (props) => {
<>
<IssuePropertyState
projectId={issue.project_detail?.id ?? null}
value={issue.state_detail.id}
value={issue.state}
defaultOptions={issue?.state_detail ? [issue.state_detail] : []}
onChange={(data) => onChange({ state: data.id, state_detail: data })}
className="h-full w-full"

View file

@ -142,7 +142,7 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
</div>
<div className="border-t border-custom-border-100">
<div className="mb-3 z-50 sticky bottom-0 left-0">
<div className="mb-3 z-5 sticky bottom-0 left-0">
{enableQuickCreateIssue && (
<SpreadsheetQuickAddIssueForm formKey="name" quickAddCallback={quickAddCallback} viewId={viewId} />
)}