forked from github/plane
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:
parent
bffba6b9dc
commit
a79dbdadb5
@ -6,7 +6,7 @@ import { useMobxStore } from "lib/mobx/store-provider";
|
|||||||
// components
|
// components
|
||||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
|
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
|
||||||
// ui
|
// ui
|
||||||
import { Breadcrumbs, CustomMenu, PhotoFilterIcon } from "@plane/ui";
|
import { Breadcrumbs, Button, CustomMenu, PhotoFilterIcon } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { truncateText } from "helpers/string.helper";
|
import { truncateText } from "helpers/string.helper";
|
||||||
import { renderEmoji } from "helpers/emoji.helper";
|
import { renderEmoji } from "helpers/emoji.helper";
|
||||||
@ -15,6 +15,8 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption
|
|||||||
// constants
|
// constants
|
||||||
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
|
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
|
||||||
import { EFilterType } from "store/issues/types";
|
import { EFilterType } from "store/issues/types";
|
||||||
|
import { EProjectStore } from "store/command-palette.store";
|
||||||
|
import { Plus } from "lucide-react";
|
||||||
|
|
||||||
export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -25,7 +27,6 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const {
|
const {
|
||||||
issueFilter: issueFilterStore,
|
|
||||||
projectViewFilters: projectViewFiltersStore,
|
projectViewFilters: projectViewFiltersStore,
|
||||||
project: { currentProjectDetails },
|
project: { currentProjectDetails },
|
||||||
projectLabel: { projectLabels },
|
projectLabel: { projectLabels },
|
||||||
@ -33,6 +34,8 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
|||||||
projectState: projectStateStore,
|
projectState: projectStateStore,
|
||||||
projectViews: projectViewsStore,
|
projectViews: projectViewsStore,
|
||||||
viewIssuesFilter: { issueFilters, updateFilters },
|
viewIssuesFilter: { issueFilters, updateFilters },
|
||||||
|
commandPalette: commandPaletteStore,
|
||||||
|
trackEvent: { setTrackElement },
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
|
|
||||||
const storedFilters = viewId ? projectViewFiltersStore.storedFilters[viewId.toString()] : undefined;
|
const storedFilters = viewId ? projectViewFiltersStore.storedFilters[viewId.toString()] : undefined;
|
||||||
@ -170,6 +173,16 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
|||||||
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
||||||
/>
|
/>
|
||||||
</FiltersDropdown>
|
</FiltersDropdown>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setTrackElement("PROJECT_VIEW_PAGE_HEADER");
|
||||||
|
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.PROJECT_VIEW);
|
||||||
|
}}
|
||||||
|
size="sm"
|
||||||
|
prependIcon={<Plus />}
|
||||||
|
>
|
||||||
|
Add Issue
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -43,7 +43,6 @@ interface IssuesModalProps {
|
|||||||
|
|
||||||
// services
|
// services
|
||||||
const issueService = new IssueService();
|
const issueService = new IssueService();
|
||||||
const issueDraftService = new IssueDraftService();
|
|
||||||
const moduleService = new ModuleService();
|
const moduleService = new ModuleService();
|
||||||
|
|
||||||
export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer((props) => {
|
export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer((props) => {
|
||||||
@ -65,7 +64,7 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||||
|
|
||||||
const { project: projectStore, user: userStore } = useMobxStore();
|
const { project: projectStore, user: userStore, projectDraftIssues: draftIssueStore } = useMobxStore();
|
||||||
|
|
||||||
const user = userStore.currentUser;
|
const user = userStore.currentUser;
|
||||||
const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined;
|
const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined;
|
||||||
@ -167,9 +166,10 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
|
|||||||
const createDraftIssue = async (payload: Partial<IIssue>) => {
|
const createDraftIssue = async (payload: Partial<IIssue>) => {
|
||||||
if (!workspaceSlug || !activeProject || !user) return;
|
if (!workspaceSlug || !activeProject || !user) return;
|
||||||
|
|
||||||
await issueDraftService
|
await draftIssueStore
|
||||||
.createDraftIssue(workspaceSlug as string, activeProject ?? "", payload)
|
.createIssue(workspaceSlug as string, activeProject ?? "", payload)
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
|
await draftIssueStore.fetchIssues(workspaceSlug as string, activeProject ?? "", "mutation");
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Success!",
|
title: "Success!",
|
||||||
@ -192,8 +192,8 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
|
|||||||
const updateDraftIssue = async (payload: Partial<IIssue>) => {
|
const updateDraftIssue = async (payload: Partial<IIssue>) => {
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
|
|
||||||
await issueDraftService
|
await draftIssueStore
|
||||||
.updateDraftIssue(workspaceSlug as string, activeProject ?? "", data?.id ?? "", payload)
|
.updateIssue(workspaceSlug as string, activeProject ?? "", data?.id ?? "", payload)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (isUpdatingSingleIssue) {
|
if (isUpdatingSingleIssue) {
|
||||||
mutate<IIssue>(PROJECT_ISSUES_DETAILS, (prevData) => ({ ...prevData, ...res }), false);
|
mutate<IIssue>(PROJECT_ISSUES_DETAILS, (prevData) => ({ ...prevData, ...res }), false);
|
||||||
|
@ -4,14 +4,14 @@ import { observer } from "mobx-react-lite";
|
|||||||
// mobx store
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// components
|
// components
|
||||||
import { AppliedFiltersList } from "components/issues";
|
import { AppliedFiltersList, SaveFilterView } from "components/issues";
|
||||||
// types
|
// types
|
||||||
import { IIssueFilterOptions } from "types";
|
import { IIssueFilterOptions } from "types";
|
||||||
import { EFilterType } from "store/issues/types";
|
import { EFilterType } from "store/issues/types";
|
||||||
|
|
||||||
export const ArchivedIssueAppliedFiltersRoot: React.FC = observer(() => {
|
export const ArchivedIssueAppliedFiltersRoot: React.FC = observer(() => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string };
|
||||||
|
|
||||||
const {
|
const {
|
||||||
projectArchivedIssuesFilter: { issueFilters, updateFilters },
|
projectArchivedIssuesFilter: { issueFilters, updateFilters },
|
||||||
@ -69,7 +69,7 @@ export const ArchivedIssueAppliedFiltersRoot: React.FC = observer(() => {
|
|||||||
if (Object.keys(appliedFilters).length === 0) return null;
|
if (Object.keys(appliedFilters).length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4">
|
<div className="p-4 flex items-center justify-between">
|
||||||
<AppliedFiltersList
|
<AppliedFiltersList
|
||||||
appliedFilters={appliedFilters}
|
appliedFilters={appliedFilters}
|
||||||
handleClearAllFilters={handleClearAllFilters}
|
handleClearAllFilters={handleClearAllFilters}
|
||||||
@ -78,6 +78,8 @@ export const ArchivedIssueAppliedFiltersRoot: React.FC = observer(() => {
|
|||||||
members={projectMembers?.map((m) => m.member)}
|
members={projectMembers?.map((m) => m.member)}
|
||||||
states={projectStateStore.states?.[projectId?.toString() ?? ""]}
|
states={projectStateStore.states?.[projectId?.toString() ?? ""]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<SaveFilterView workspaceSlug={workspaceSlug} projectId={projectId} filterParams={appliedFilters} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
// mobx store
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// components
|
// components
|
||||||
import { AppliedFiltersList } from "components/issues";
|
import { AppliedFiltersList, SaveFilterView } from "components/issues";
|
||||||
// types
|
// types
|
||||||
import { IIssueFilterOptions } from "types";
|
import { IIssueFilterOptions } from "types";
|
||||||
import { EFilterType } from "store/issues/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;
|
if (Object.keys(appliedFilters).length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4">
|
<div className="p-4 flex items-center justify-between">
|
||||||
<AppliedFiltersList
|
<AppliedFiltersList
|
||||||
appliedFilters={appliedFilters}
|
appliedFilters={appliedFilters}
|
||||||
handleClearAllFilters={handleClearAllFilters}
|
handleClearAllFilters={handleClearAllFilters}
|
||||||
@ -84,6 +85,8 @@ export const CycleAppliedFiltersRoot: React.FC = observer(() => {
|
|||||||
members={projectMembers?.map((m) => m.member)}
|
members={projectMembers?.map((m) => m.member)}
|
||||||
states={projectStateStore.states?.[cycleId ?? ""]}
|
states={projectStateStore.states?.[cycleId ?? ""]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<SaveFilterView workspaceSlug={workspaceSlug} projectId={projectId} filterParams={appliedFilters} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -4,14 +4,14 @@ import { observer } from "mobx-react-lite";
|
|||||||
// mobx store
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// components
|
// components
|
||||||
import { AppliedFiltersList } from "components/issues";
|
import { AppliedFiltersList, SaveFilterView } from "components/issues";
|
||||||
// types
|
// types
|
||||||
import { IIssueFilterOptions } from "types";
|
import { IIssueFilterOptions } from "types";
|
||||||
import { EFilterType } from "store/issues/types";
|
import { EFilterType } from "store/issues/types";
|
||||||
|
|
||||||
export const DraftIssueAppliedFiltersRoot: React.FC = observer(() => {
|
export const DraftIssueAppliedFiltersRoot: React.FC = observer(() => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string };
|
||||||
|
|
||||||
const {
|
const {
|
||||||
projectDraftIssuesFilter: { issueFilters, updateFilters },
|
projectDraftIssuesFilter: { issueFilters, updateFilters },
|
||||||
@ -64,7 +64,7 @@ export const DraftIssueAppliedFiltersRoot: React.FC = observer(() => {
|
|||||||
if (Object.keys(appliedFilters).length === 0) return null;
|
if (Object.keys(appliedFilters).length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4">
|
<div className="p-4 flex items-center justify-between">
|
||||||
<AppliedFiltersList
|
<AppliedFiltersList
|
||||||
appliedFilters={appliedFilters}
|
appliedFilters={appliedFilters}
|
||||||
handleClearAllFilters={handleClearAllFilters}
|
handleClearAllFilters={handleClearAllFilters}
|
||||||
@ -73,6 +73,8 @@ export const DraftIssueAppliedFiltersRoot: React.FC = observer(() => {
|
|||||||
members={projectMembers?.map((m) => m.member)}
|
members={projectMembers?.map((m) => m.member)}
|
||||||
states={projectStateStore.states?.[projectId?.toString() ?? ""]}
|
states={projectStateStore.states?.[projectId?.toString() ?? ""]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<SaveFilterView workspaceSlug={workspaceSlug} projectId={projectId} filterParams={appliedFilters} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -4,7 +4,7 @@ import { observer } from "mobx-react-lite";
|
|||||||
// mobx store
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// components
|
// components
|
||||||
import { AppliedFiltersList } from "components/issues";
|
import { AppliedFiltersList, SaveFilterView } from "components/issues";
|
||||||
// types
|
// types
|
||||||
import { IIssueFilterOptions } from "types";
|
import { IIssueFilterOptions } from "types";
|
||||||
import { EFilterType } from "store/issues/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;
|
if (Object.keys(appliedFilters).length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4">
|
<div className="p-4 flex items-center justify-between">
|
||||||
<AppliedFiltersList
|
<AppliedFiltersList
|
||||||
appliedFilters={appliedFilters}
|
appliedFilters={appliedFilters}
|
||||||
handleClearAllFilters={handleClearAllFilters}
|
handleClearAllFilters={handleClearAllFilters}
|
||||||
@ -85,6 +85,8 @@ export const ModuleAppliedFiltersRoot: React.FC = observer(() => {
|
|||||||
members={projectMembers?.map((m) => m.member)}
|
members={projectMembers?.map((m) => m.member)}
|
||||||
states={projectStateStore.states?.[moduleId ?? ""]}
|
states={projectStateStore.states?.[moduleId ?? ""]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<SaveFilterView workspaceSlug={workspaceSlug} projectId={projectId} filterParams={appliedFilters} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -4,14 +4,18 @@ import { observer } from "mobx-react-lite";
|
|||||||
// mobx store
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// components
|
// components
|
||||||
import { AppliedFiltersList } from "components/issues";
|
import { AppliedFiltersList, SaveFilterView } from "components/issues";
|
||||||
|
|
||||||
// types
|
// types
|
||||||
import { IIssueFilterOptions } from "types";
|
import { IIssueFilterOptions } from "types";
|
||||||
import { EFilterType } from "store/issues/types";
|
import { EFilterType } from "store/issues/types";
|
||||||
|
|
||||||
export const ProjectAppliedFiltersRoot: React.FC = observer(() => {
|
export const ProjectAppliedFiltersRoot: React.FC = observer(() => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query as {
|
||||||
|
workspaceSlug: string;
|
||||||
|
projectId: string;
|
||||||
|
};
|
||||||
|
|
||||||
const {
|
const {
|
||||||
projectLabel: { projectLabels },
|
projectLabel: { projectLabels },
|
||||||
@ -60,7 +64,7 @@ export const ProjectAppliedFiltersRoot: React.FC = observer(() => {
|
|||||||
if (Object.keys(appliedFilters).length === 0) return null;
|
if (Object.keys(appliedFilters).length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4">
|
<div className="p-4 flex items-center justify-between">
|
||||||
<AppliedFiltersList
|
<AppliedFiltersList
|
||||||
appliedFilters={appliedFilters}
|
appliedFilters={appliedFilters}
|
||||||
handleClearAllFilters={handleClearAllFilters}
|
handleClearAllFilters={handleClearAllFilters}
|
||||||
@ -69,6 +73,8 @@ export const ProjectAppliedFiltersRoot: React.FC = observer(() => {
|
|||||||
members={projectMembers?.map((m) => m.member)}
|
members={projectMembers?.map((m) => m.member)}
|
||||||
states={projectStateStore.states?.[projectId?.toString() ?? ""]}
|
states={projectStateStore.states?.[projectId?.toString() ?? ""]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<SaveFilterView workspaceSlug={workspaceSlug} projectId={projectId} filterParams={appliedFilters} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -31,7 +31,6 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => {
|
|||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
|
|
||||||
const viewDetails = viewId ? projectViewsStore.viewDetails[viewId.toString()] : undefined;
|
const viewDetails = viewId ? projectViewsStore.viewDetails[viewId.toString()] : undefined;
|
||||||
const storedFilters = viewId ? projectViewFiltersStore.storedFilters[viewId.toString()] : undefined;
|
|
||||||
|
|
||||||
const userFilters = issueFilters?.filters;
|
const userFilters = issueFilters?.filters;
|
||||||
// filters whose value not null or empty array
|
// 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(), {
|
projectViewsStore.updateView(workspaceSlug.toString(), projectId.toString(), viewId.toString(), {
|
||||||
query_data: {
|
query_data: {
|
||||||
...viewDetails.query_data,
|
...viewDetails.query_data,
|
||||||
...(storedFilters ?? {}),
|
...(appliedFilters ?? {}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -104,13 +103,16 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => {
|
|||||||
members={projectMembers?.map((m) => m.member)}
|
members={projectMembers?.map((m) => m.member)}
|
||||||
states={projectStateStore.states?.[projectId?.toString() ?? ""]}
|
states={projectStateStore.states?.[projectId?.toString() ?? ""]}
|
||||||
/>
|
/>
|
||||||
{storedFilters && viewDetails && areFiltersDifferent(storedFilters, viewDetails.query_data ?? {}) && (
|
|
||||||
<div className="flex items-center justify-center flex-shrink-0">
|
{appliedFilters &&
|
||||||
<Button variant="primary" size="sm" onClick={handleUpdateView}>
|
viewDetails?.query_data &&
|
||||||
Update view
|
areFiltersDifferent(appliedFilters, viewDetails?.query_data ?? {}) && (
|
||||||
</Button>
|
<div className="flex items-center justify-center flex-shrink-0">
|
||||||
</div>
|
<Button variant="primary" size="sm" onClick={handleUpdateView}>
|
||||||
)}
|
Update view
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -15,3 +15,6 @@ export * from "./spreadsheet";
|
|||||||
|
|
||||||
// properties
|
// properties
|
||||||
export * from "./properties";
|
export * from "./properties";
|
||||||
|
|
||||||
|
// save view
|
||||||
|
export * from "./save-filter-view";
|
||||||
|
@ -1,16 +1,14 @@
|
|||||||
import { memo, useRef, useState } from "react";
|
import { memo } from "react";
|
||||||
import { Draggable } from "@hello-pangea/dnd";
|
import { Draggable } from "@hello-pangea/dnd";
|
||||||
|
import isEqual from "lodash/isEqual";
|
||||||
// components
|
// components
|
||||||
import { KanBanProperties } from "./properties";
|
import { KanBanProperties } from "./properties";
|
||||||
// ui
|
// ui
|
||||||
import { Tooltip } from "@plane/ui";
|
import { Tooltip } from "@plane/ui";
|
||||||
// hooks
|
|
||||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
|
||||||
// types
|
// types
|
||||||
import { IIssueDisplayProperties, IIssue } from "types";
|
import { IIssueDisplayProperties, IIssue } from "types";
|
||||||
import { EIssueActions } from "../types";
|
import { EIssueActions } from "../types";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { MoreHorizontal } from "lucide-react";
|
|
||||||
|
|
||||||
interface IssueBlockProps {
|
interface IssueBlockProps {
|
||||||
sub_group_id: string;
|
sub_group_id: string;
|
||||||
@ -20,37 +18,28 @@ interface IssueBlockProps {
|
|||||||
isDragDisabled: boolean;
|
isDragDisabled: boolean;
|
||||||
showEmptyGroup: boolean;
|
showEmptyGroup: boolean;
|
||||||
handleIssues: (sub_group_by: string | null, group_by: string | null, issue: IIssue, action: EIssueActions) => void;
|
handleIssues: (sub_group_by: string | null, group_by: string | null, issue: IIssue, action: EIssueActions) => void;
|
||||||
quickActions: (
|
quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode;
|
||||||
sub_group_by: string | null,
|
|
||||||
group_by: string | null,
|
|
||||||
issue: IIssue,
|
|
||||||
customActionButton?: React.ReactElement
|
|
||||||
) => React.ReactNode;
|
|
||||||
displayProperties: IIssueDisplayProperties | null;
|
displayProperties: IIssueDisplayProperties | null;
|
||||||
isReadOnly: boolean;
|
isReadOnly: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const KanBanIssueMemoBlock: React.FC<IssueBlockProps> = (props) => {
|
interface IssueDetailsBlockProps {
|
||||||
const {
|
sub_group_id: string;
|
||||||
sub_group_id,
|
columnId: string;
|
||||||
columnId,
|
issue: IIssue;
|
||||||
index,
|
showEmptyGroup: boolean;
|
||||||
issue,
|
handleIssues: (sub_group_by: string | null, group_by: string | null, issue: IIssue, action: EIssueActions) => void;
|
||||||
isDragDisabled,
|
quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode;
|
||||||
showEmptyGroup,
|
displayProperties: IIssueDisplayProperties | null;
|
||||||
handleIssues,
|
isReadOnly: boolean;
|
||||||
quickActions,
|
}
|
||||||
displayProperties,
|
|
||||||
isReadOnly,
|
const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = (props) => {
|
||||||
} = props;
|
const { sub_group_id, columnId, issue, showEmptyGroup, handleIssues, quickActions, displayProperties, isReadOnly } =
|
||||||
// router
|
props;
|
||||||
|
|
||||||
const router = useRouter();
|
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) => {
|
const updateIssue = (sub_group_by: string | null, group_by: string | null, issueToUpdate: IIssue) => {
|
||||||
if (issueToUpdate) handleIssues(sub_group_by, group_by, issueToUpdate, EIssueActions.UPDATE);
|
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;
|
let draggableId = issue.id;
|
||||||
if (columnId) draggableId = `${draggableId}__${columnId}`;
|
if (columnId) draggableId = `${draggableId}__${columnId}`;
|
||||||
if (sub_group_id) draggableId = `${draggableId}__${sub_group_id}`;
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Draggable draggableId={draggableId} index={index}>
|
<Draggable draggableId={draggableId} index={index}>
|
||||||
@ -100,44 +135,16 @@ export const KanBanIssueMemoBlock: React.FC<IssueBlockProps> = (props) => {
|
|||||||
isDragDisabled ? "" : "hover:cursor-grab"
|
isDragDisabled ? "" : "hover:cursor-grab"
|
||||||
} ${snapshot.isDragging ? `border-custom-primary-100` : `border-transparent`}`}
|
} ${snapshot.isDragging ? `border-custom-primary-100` : `border-transparent`}`}
|
||||||
>
|
>
|
||||||
{displayProperties && displayProperties?.key && (
|
<KanbanIssueMemoBlock
|
||||||
<div className="relative">
|
sub_group_id={sub_group_id}
|
||||||
<div className="text-xs line-clamp-1 text-custom-text-300">
|
columnId={columnId}
|
||||||
{issue.project_detail.identifier}-{issue.sequence_id}
|
issue={issue}
|
||||||
</div>
|
showEmptyGroup={showEmptyGroup}
|
||||||
<div
|
handleIssues={handleIssues}
|
||||||
className={`absolute -top-1 right-0 hidden group-hover/kanban-block:block ${
|
quickActions={quickActions}
|
||||||
isMenuActive ? "!block" : ""
|
displayProperties={displayProperties}
|
||||||
}`}
|
isReadOnly={isReadOnly}
|
||||||
>
|
/>
|
||||||
{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>
|
|
||||||
</div>
|
</div>
|
||||||
</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);
|
|
||||||
|
@ -22,17 +22,20 @@ export const IssueBlocksList: FC<Props> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<div className="w-full h-full relative divide-y-[0.5px] divide-custom-border-200">
|
<div className="w-full h-full relative divide-y-[0.5px] divide-custom-border-200">
|
||||||
{issueIds && issueIds.length > 0 ? (
|
{issueIds && issueIds.length > 0 ? (
|
||||||
issueIds.map((issueId: string) => (
|
issueIds.map(
|
||||||
<IssueBlock
|
(issueId: string) =>
|
||||||
key={issues[issueId]?.id}
|
issues[issueId] && (
|
||||||
columnId={columnId}
|
<IssueBlock
|
||||||
issue={issues[issueId]}
|
key={issues[issueId].id}
|
||||||
handleIssues={handleIssues}
|
columnId={columnId}
|
||||||
quickActions={quickActions}
|
issue={issues[issueId]}
|
||||||
isReadonly={isReadonly}
|
handleIssues={handleIssues}
|
||||||
displayProperties={displayProperties}
|
quickActions={quickActions}
|
||||||
/>
|
isReadonly={isReadonly}
|
||||||
))
|
displayProperties={displayProperties}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<div className="bg-custom-background-100 text-custom-text-400 text-sm p-3">No issues</div>
|
<div className="bg-custom-background-100 text-custom-text-400 text-sm p-3">No issues</div>
|
||||||
)}
|
)}
|
||||||
|
33
web/components/issues/issue-layouts/save-filter-view.tsx
Normal file
33
web/components/issues/issue-layouts/save-filter-view.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
@ -26,7 +26,7 @@ export const SpreadsheetStateColumn: React.FC<Props> = (props) => {
|
|||||||
<>
|
<>
|
||||||
<IssuePropertyState
|
<IssuePropertyState
|
||||||
projectId={issue.project_detail?.id ?? null}
|
projectId={issue.project_detail?.id ?? null}
|
||||||
value={issue.state_detail.id}
|
value={issue.state}
|
||||||
defaultOptions={issue?.state_detail ? [issue.state_detail] : []}
|
defaultOptions={issue?.state_detail ? [issue.state_detail] : []}
|
||||||
onChange={(data) => onChange({ state: data.id, state_detail: data })}
|
onChange={(data) => onChange({ state: data.id, state_detail: data })}
|
||||||
className="h-full w-full"
|
className="h-full w-full"
|
||||||
|
@ -142,7 +142,7 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="border-t border-custom-border-100">
|
<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 && (
|
{enableQuickCreateIssue && (
|
||||||
<SpreadsheetQuickAddIssueForm formKey="name" quickAddCallback={quickAddCallback} viewId={viewId} />
|
<SpreadsheetQuickAddIssueForm formKey="name" quickAddCallback={quickAddCallback} viewId={viewId} />
|
||||||
)}
|
)}
|
||||||
|
@ -78,8 +78,8 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = (props) =
|
|||||||
[issue, issueUpdate]
|
[issue, issueUpdate]
|
||||||
);
|
);
|
||||||
|
|
||||||
const [localTitleValue, setLocalTitleValue] = useState("");
|
const [localTitleValue, setLocalTitleValue] = useState(issue.name);
|
||||||
const [localIssueDescription, setLocalIssueDescription] = useState("");
|
const [localIssueDescription, setLocalIssueDescription] = useState(issue.description_html);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (issue.id) {
|
if (issue.id) {
|
||||||
@ -88,10 +88,6 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = (props) =
|
|||||||
}
|
}
|
||||||
}, [issue.id]);
|
}, [issue.id]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setLocalTitleValue(issue.name);
|
|
||||||
}, [issue.name]);
|
|
||||||
|
|
||||||
const debouncedFormSave = debounce(async () => {
|
const debouncedFormSave = debounce(async () => {
|
||||||
handleSubmit(handleDescriptionFormSubmit)().finally(() => setIsSubmitting("submitted"));
|
handleSubmit(handleDescriptionFormSubmit)().finally(() => setIsSubmitting("submitted"));
|
||||||
}, 1500);
|
}, 1500);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { FC, Fragment, ReactNode } from "react";
|
import { FC, Fragment, ReactNode, useEffect } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
@ -40,17 +40,17 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
useSWR(
|
const fetchIssueDetail = async () => {
|
||||||
workspaceSlug && projectId && issueId && peekIssueId && issueId === peekIssueId
|
if (workspaceSlug && projectId && peekIssueId) {
|
||||||
? `ISSUE_DETAILS_${workspaceSlug}_${projectId}_${peekIssueId}`
|
if (isArchived)
|
||||||
: null,
|
await archivedIssueDetailStore.fetchPeekIssueDetails(workspaceSlug, projectId, peekIssueId as string);
|
||||||
async () => {
|
else await issueDetailStore.fetchPeekIssueDetails(workspaceSlug, projectId, peekIssueId as string);
|
||||||
if (workspaceSlug && projectId && issueId && issueId === peekIssueId) {
|
|
||||||
if (isArchived) await archivedIssueDetailStore.fetchPeekIssueDetails(workspaceSlug, projectId, peekIssueId);
|
|
||||||
else await issueDetailStore.fetchPeekIssueDetails(workspaceSlug, projectId, peekIssueId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchIssueDetail();
|
||||||
|
}, [workspaceSlug, projectId, peekIssueId]);
|
||||||
|
|
||||||
const handleCopyText = (e: React.MouseEvent<HTMLButtonElement>) => {
|
const handleCopyText = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
Loading…
Reference in New Issue
Block a user