forked from github/plane
Merge pull request #2195 from makeplane/fix/list-sorting
Implementing list view
This commit is contained in:
commit
cc9ebc58bc
@ -198,6 +198,8 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
const isNotAllowed =
|
||||
userAuth.isGuest || userAuth.isViewer || disableUserActions || isArchivedIssues;
|
||||
|
||||
console.log("properties", properties);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContextMenu
|
||||
|
41
web/components/issue-layouts/list/group-header.tsx
Normal file
41
web/components/issue-layouts/list/group-header.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import { FC } from "react";
|
||||
// lib
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { RootStore } from "store/root";
|
||||
|
||||
export interface IIssueListGroupHeader {
|
||||
groupId: string;
|
||||
groupBy: string;
|
||||
}
|
||||
|
||||
export const IssueListGroupHeader: FC<IIssueListGroupHeader> = (props) => {
|
||||
const { groupId, groupBy } = props;
|
||||
|
||||
const { issueView: issueViewStore, issueFilters: issueFilterStore }: RootStore = useMobxStore();
|
||||
|
||||
return (
|
||||
<div>
|
||||
{groupBy === "state" && <>{issueFilterStore.getProjectStateById(groupId)?.name}</>}
|
||||
{groupBy === "state_detail.group" && <>{groupId}</>}
|
||||
{groupBy === "priority" && <>{groupId}</>}
|
||||
{groupBy === "project" && (
|
||||
<>{issueFilterStore.workspaceProjects?.find((p) => (p.id = groupId))}</>
|
||||
)}
|
||||
{groupBy === "labels" && (
|
||||
<>{issueFilterStore.projectLabels?.find((p) => p.id === groupId)?.name || " None"}</>
|
||||
)}
|
||||
{groupBy === "assignees" && (
|
||||
<>
|
||||
{issueFilterStore.projectMembers?.find((p) => p?.member?.id === groupId)?.member
|
||||
?.display_name || " None"}
|
||||
</>
|
||||
)}
|
||||
{groupBy === "created_by" && (
|
||||
<>
|
||||
{issueFilterStore.projectMembers?.find((p) => p?.member?.id === groupId)?.member
|
||||
?.display_name || " None"}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
4
web/components/issue-layouts/list/index.ts
Normal file
4
web/components/issue-layouts/list/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from "./root";
|
||||
export * from "./list";
|
||||
export * from "./item";
|
||||
export * from "./group-header";
|
@ -1,6 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
export const IssueListViewRoot = () => {
|
||||
console.log();
|
||||
return <div>IssueListViewRoot</div>;
|
||||
};
|
234
web/components/issue-layouts/list/item.tsx
Normal file
234
web/components/issue-layouts/list/item.tsx
Normal file
@ -0,0 +1,234 @@
|
||||
import React, { FC, useState } from "react";
|
||||
import { Tooltip, CustomMenu, ContextMenu } from "components/ui";
|
||||
// lib
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { IIssue } from "types";
|
||||
import useUserAuth from "hooks/use-user-auth";
|
||||
// icons
|
||||
import {
|
||||
ClipboardDocumentCheckIcon,
|
||||
LinkIcon,
|
||||
PencilIcon,
|
||||
TrashIcon,
|
||||
XMarkIcon,
|
||||
ArrowTopRightOnSquareIcon,
|
||||
PaperClipIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
// components
|
||||
import { LayerDiagonalIcon } from "components/icons";
|
||||
import {
|
||||
ViewAssigneeSelect,
|
||||
ViewDueDateSelect,
|
||||
ViewEstimateSelect,
|
||||
ViewIssueLabel,
|
||||
ViewPrioritySelect,
|
||||
ViewStartDateSelect,
|
||||
ViewStateSelect,
|
||||
} from "components/issues";
|
||||
|
||||
export interface IIssueListItem {
|
||||
issue: IIssue;
|
||||
}
|
||||
|
||||
export const IssueListItem: FC<IIssueListItem> = (props) => {
|
||||
const { issue } = props;
|
||||
// store
|
||||
const { user: userStore, issueFilters: issueFilterStore } = useMobxStore();
|
||||
const displayProperties = issueFilterStore.userFilters?.display_properties;
|
||||
console.log("userStore", userStore);
|
||||
// context menu
|
||||
const [contextMenu, setContextMenu] = useState(false);
|
||||
const [contextMenuPosition, setContextMenuPosition] = useState<React.MouseEvent | null>(null);
|
||||
const { user: userAuth } = useUserAuth();
|
||||
|
||||
// const isNotAllowed =
|
||||
// userAuth?.isGuest || userAuth?.isViewer || disableUserActions || isArchivedIssues;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<>
|
||||
<ContextMenu
|
||||
clickEvent={contextMenuPosition}
|
||||
title="Quick actions"
|
||||
isOpen={contextMenu}
|
||||
setIsOpen={setContextMenu}
|
||||
>
|
||||
{/* {!isNotAllowed && (
|
||||
<>
|
||||
<ContextMenu.Item Icon={PencilIcon} onClick={editIssue}>
|
||||
Edit issue
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Item Icon={ClipboardDocumentCheckIcon} onClick={makeIssueCopy}>
|
||||
Make a copy...
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Item Icon={TrashIcon} onClick={() => handleDeleteIssue(issue)}>
|
||||
Delete issue
|
||||
</ContextMenu.Item>
|
||||
</>
|
||||
)}
|
||||
<ContextMenu.Item Icon={LinkIcon} onClick={handleCopyText}>
|
||||
Copy issue link
|
||||
</ContextMenu.Item>
|
||||
<a href={issuePath} target="_blank" rel="noreferrer noopener">
|
||||
<ContextMenu.Item Icon={ArrowTopRightOnSquareIcon}>
|
||||
Open issue in new tab
|
||||
</ContextMenu.Item>
|
||||
</a> */}
|
||||
</ContextMenu>
|
||||
<div
|
||||
className="flex items-center justify-between px-4 py-2.5 gap-10 border-b border-custom-border-200 bg-custom-background-100 last:border-b-0"
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
setContextMenu(true);
|
||||
setContextMenuPosition(e);
|
||||
}}
|
||||
>
|
||||
<div className="flex-grow cursor-pointer min-w-[200px] whitespace-nowrap overflow-hidden overflow-ellipsis">
|
||||
<div className="group relative flex items-center gap-2">
|
||||
{/* {properties.key && (
|
||||
<Tooltip
|
||||
tooltipHeading="Issue ID"
|
||||
tooltipContent={`${issue.project_detail?.identifier}-${issue.sequence_id}`}
|
||||
>
|
||||
<span className="flex-shrink-0 text-xs text-custom-text-200">
|
||||
{issue.project_detail?.identifier}-{issue.sequence_id}
|
||||
</span>
|
||||
</Tooltip>
|
||||
)} */}
|
||||
<Tooltip position="top-left" tooltipHeading="Title" tooltipContent={issue.name}>
|
||||
<button
|
||||
type="button"
|
||||
className="truncate text-[0.825rem] text-custom-text-100"
|
||||
onClick={() => {
|
||||
// if (!isDraftIssues) openPeekOverview(issue);
|
||||
// if (handleDraftIssueSelect) handleDraftIssueSelect(issue);
|
||||
}}
|
||||
>
|
||||
{issue.name}
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`flex flex-shrink-0 items-center gap-2 text-xs `}>
|
||||
{displayProperties?.priority && (
|
||||
<ViewPrioritySelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="right"
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{displayProperties?.state && (
|
||||
<ViewStateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="right"
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{displayProperties?.start_date && issue.start_date && (
|
||||
<ViewStartDateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{displayProperties?.due_date && issue.target_date && (
|
||||
<ViewDueDateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{displayProperties?.labels && (
|
||||
<ViewIssueLabel labelDetails={issue.label_details} maxRender={3} />
|
||||
)}
|
||||
{displayProperties?.assignee && (
|
||||
<ViewAssigneeSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="right"
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{displayProperties?.estimate && issue.estimate_point !== null && (
|
||||
<ViewEstimateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="right"
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{displayProperties?.sub_issue_count && issue.sub_issues_count > 0 && (
|
||||
<div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2.5 py-1 text-xs shadow-sm">
|
||||
<Tooltip tooltipHeading="Sub-issue" tooltipContent={`${issue.sub_issues_count}`}>
|
||||
<div className="flex items-center gap-1 text-custom-text-200">
|
||||
<LayerDiagonalIcon className="h-3.5 w-3.5" />
|
||||
{issue.sub_issues_count}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
{displayProperties?.link && issue.link_count > 0 && (
|
||||
<div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2.5 py-1 text-xs shadow-sm">
|
||||
<Tooltip tooltipHeading="Links" tooltipContent={`${issue.link_count}`}>
|
||||
<div className="flex items-center gap-1 text-custom-text-200">
|
||||
<LinkIcon className="h-3.5 w-3.5" />
|
||||
{issue.link_count}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
{displayProperties?.attachment_count && issue.attachment_count > 0 && (
|
||||
<div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2.5 py-1 text-xs shadow-sm">
|
||||
<Tooltip tooltipHeading="Attachments" tooltipContent={`${issue.attachment_count}`}>
|
||||
<div className="flex items-center gap-1 text-custom-text-200">
|
||||
<PaperClipIcon className="h-3.5 w-3.5 -rotate-45" />
|
||||
{issue.attachment_count}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
{/* {type && !isNotAllowed && (
|
||||
<CustomMenu width="auto" ellipsis>
|
||||
<CustomMenu.MenuItem onClick={editIssue}>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<PencilIcon className="h-4 w-4" />
|
||||
<span>Edit issue</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
{type !== "issue" && removeIssue && (
|
||||
<CustomMenu.MenuItem onClick={removeIssue}>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<XMarkIcon className="h-4 w-4" />
|
||||
<span>Remove from {type}</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
<CustomMenu.MenuItem onClick={() => handleDeleteIssue(issue)}>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
<span>Delete issue</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={handleCopyText}>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<LinkIcon className="h-4 w-4" />
|
||||
<span>Copy issue link</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
)} */}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
18
web/components/issue-layouts/list/list.tsx
Normal file
18
web/components/issue-layouts/list/list.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import React, { FC } from "react";
|
||||
import { IIssue } from "types";
|
||||
import { IssueListItem } from "./item";
|
||||
|
||||
export interface IIssueListView {
|
||||
issues: IIssue[];
|
||||
}
|
||||
|
||||
export const IssueListView: FC<IIssueListView> = (props) => {
|
||||
const { issues = [] } = props;
|
||||
return (
|
||||
<div>
|
||||
{issues.map((issue) => (
|
||||
<IssueListItem issue={issue} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
44
web/components/issue-layouts/list/root.tsx
Normal file
44
web/components/issue-layouts/list/root.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import React from "react";
|
||||
import { Disclosure } from "@headlessui/react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { RootStore } from "store/root";
|
||||
// components
|
||||
import { IssueListView } from "./list";
|
||||
import { IssueListGroupHeader } from "./group-header";
|
||||
|
||||
export const IssueListViewRoot = observer(() => {
|
||||
const { issueView: issueViewStore, issueFilters: issueFilterStore }: RootStore = useMobxStore();
|
||||
console.log("issueViewStore", issueViewStore);
|
||||
console.log("userFilters", issueFilterStore.userFilters);
|
||||
console.log("issueFilterStore", issueFilterStore);
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-full">
|
||||
{issueViewStore.loader || issueViewStore?.getIssues === null ? (
|
||||
<div>Loading...</div>
|
||||
) : (
|
||||
<>
|
||||
{Object.keys(issueViewStore?.getIssues).map((groupId) => (
|
||||
<Disclosure key={groupId}>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Disclosure.Button className="flex w-full justify-between rounded-lg bg-purple-100 px-4 py-2 text-left text-sm font-medium text-purple-900 hover:bg-purple-200 focus:outline-none focus-visible:ring focus-visible:ring-purple-500 focus-visible:ring-opacity-75">
|
||||
<IssueListGroupHeader
|
||||
groupId={groupId}
|
||||
groupBy={issueFilterStore.userFilters?.display_filters["group_by"] || ""}
|
||||
/>
|
||||
</Disclosure.Button>
|
||||
<Disclosure.Panel className="px-4 pt-4 pb-2">
|
||||
<IssueListView issues={issueViewStore?.getIssues?.[groupId]}></IssueListView>
|
||||
</Disclosure.Panel>
|
||||
</>
|
||||
)}
|
||||
</Disclosure>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
@ -7,7 +7,7 @@ import { DisplayFiltersSelection } from "./display-filters";
|
||||
|
||||
import { FilterPreview } from "./filters-preview";
|
||||
|
||||
import { IssueListViewRoot } from "./list";
|
||||
import { IssueListViewRoot } from "./list/root";
|
||||
import { IssueKanBanViewRoot } from "./kanban";
|
||||
import { IssueCalendarViewRoot } from "./calendar";
|
||||
import { IssueSpreadsheetViewRoot } from "./spreadsheet";
|
||||
@ -19,8 +19,7 @@ import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { RootStore } from "store/root";
|
||||
|
||||
export const IssuesRoot = observer(() => {
|
||||
const store: RootStore = useMobxStore();
|
||||
const { issueFilters: issueFilterStore } = store;
|
||||
const { issueFilters: issueFilterStore }: RootStore = useMobxStore();
|
||||
|
||||
return (
|
||||
<div className="w-full h-full relative flex flex-col overflow-hidden">
|
||||
|
@ -21,6 +21,7 @@ import {
|
||||
displayProperties,
|
||||
extraProperties,
|
||||
} from "./issue_data";
|
||||
import { IIssueState } from "./Issues";
|
||||
|
||||
export type TIssueViews = "my_issues" | "issues" | "modules" | "views" | "cycles";
|
||||
export type TIssueLayouts = "list" | "kanban" | "calendar" | "spreadsheet" | "gantt_chart";
|
||||
@ -259,6 +260,9 @@ class IssueFilterStore implements IIssueFilterStore {
|
||||
userFilters: computed,
|
||||
|
||||
// actions
|
||||
|
||||
getProjectStateById: action,
|
||||
|
||||
getComputedFilters: action,
|
||||
|
||||
handleUserFilter: action,
|
||||
@ -321,6 +325,21 @@ class IssueFilterStore implements IIssueFilterStore {
|
||||
return this.issueRenderFilters?.workspace_properties?.[this.workspaceId]?.project_properties?.[this.projectId]
|
||||
?.states;
|
||||
}
|
||||
|
||||
getProjectStateById = (stateId: string) => {
|
||||
if (!this.workspaceId || !this.projectId) return null;
|
||||
const states =
|
||||
this.issueRenderFilters?.workspace_properties?.[this.workspaceId]?.project_properties?.[this.projectId]?.states;
|
||||
|
||||
let stateInfo: any = null;
|
||||
Object.keys(states).forEach((stateGroupName) => {
|
||||
if (states[stateGroupName].find((state: any) => state.id === stateId)) {
|
||||
stateInfo = states[stateGroupName].find((state: any) => state.id === stateId);
|
||||
}
|
||||
});
|
||||
return stateInfo;
|
||||
};
|
||||
|
||||
get projectLabels() {
|
||||
if (!this.workspaceId || !this.projectId) return null;
|
||||
return this.issueRenderFilters?.workspace_properties?.[this.workspaceId]?.project_properties?.[this.projectId]
|
||||
|
@ -40,7 +40,7 @@ class ThemeStore {
|
||||
|
||||
setTheme = async (_theme: { theme: ICurrentUserSettings }) => {
|
||||
try {
|
||||
const currentTheme: string = _theme.theme.theme.toString();
|
||||
const currentTheme: string = _theme?.theme?.theme?.toString();
|
||||
|
||||
// updating the local storage theme value
|
||||
localStorage.setItem("theme", currentTheme);
|
||||
|
Loading…
Reference in New Issue
Block a user