forked from github/plane
chore: Implemented new kanaban board UX and implemented draggable using react beautiful dnd
This commit is contained in:
parent
731309a932
commit
ef630ef663
52
web/components/issue-layouts/kanban/content.tsx
Normal file
52
web/components/issue-layouts/kanban/content.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
// react beautiful dnd
|
||||
import { Draggable } from "react-beautiful-dnd";
|
||||
|
||||
interface IssueContentProps {
|
||||
columnId: string;
|
||||
issues: any;
|
||||
}
|
||||
|
||||
export const IssueContent = ({ columnId, issues }: IssueContentProps) => {
|
||||
console.log();
|
||||
|
||||
return (
|
||||
<>
|
||||
{issues && issues.length > 0 ? (
|
||||
<>
|
||||
{issues.map((issue: any, index: any) => (
|
||||
<Draggable
|
||||
draggableId={issue.id}
|
||||
index={index}
|
||||
key={`issue-blocks-${columnId}-${issue.id}`}
|
||||
>
|
||||
{(provided: any, snapshot: any) => (
|
||||
<div
|
||||
key={issue.id}
|
||||
className="p-2"
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
<div
|
||||
className={`text-sm rounded p-2 px-3 bg-white shadow space-y-1 border border-transparent`}
|
||||
>
|
||||
<div className="text-xs line-clamp-1 text-gray-500 font-medium">
|
||||
ONE-{issue.sequence_id}-{issue.sort_order}
|
||||
</div>
|
||||
<div className="line-clamp-2 h-[42px]">
|
||||
{issue.name} {issue.name} {issue.name} {issue.name} {issue.name} {issue.name}{" "}
|
||||
{issue.name} {issue.name}
|
||||
</div>
|
||||
<div>Footer</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<div>No issues are available.</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
1
web/components/issue-layouts/kanban/footer.tsx
Normal file
1
web/components/issue-layouts/kanban/footer.tsx
Normal file
@ -0,0 +1 @@
|
||||
export const IssueFooter = () => <div>Footer</div>;
|
1
web/components/issue-layouts/kanban/header.tsx
Normal file
1
web/components/issue-layouts/kanban/header.tsx
Normal file
@ -0,0 +1 @@
|
||||
export const IssueHeader = () => <div>Header</div>;
|
70
web/components/issue-layouts/kanban/index.tsx
Normal file
70
web/components/issue-layouts/kanban/index.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import React from "react";
|
||||
// react beautiful dnd
|
||||
import { DragDropContext, Droppable } from "react-beautiful-dnd";
|
||||
// components
|
||||
import { IssueHeader } from "./header";
|
||||
import { IssueContent } from "./content";
|
||||
import { IssueFooter } from "./footer";
|
||||
// mobx
|
||||
import { observer } from "mobx-react-lite";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { RootStore } from "store/root";
|
||||
|
||||
export const IssueRoot = observer(() => {
|
||||
const store: RootStore = useMobxStore();
|
||||
const { kanban: issueViewStore } = store;
|
||||
|
||||
const onDragEnd = (result: any) => {
|
||||
console.log("result", result);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative w-screen h-screen">
|
||||
{issueViewStore.loader && issueViewStore?.issues === null ? (
|
||||
<div>Loading...</div>
|
||||
) : (
|
||||
<div className="relative w-full h-full p-2">
|
||||
{issueViewStore?.getIssues && Object.keys(issueViewStore?.getIssues).length > 0 ? (
|
||||
<div className="relative overflow-hidden overflow-x-auto w-full h-full flex">
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
{Object.keys(issueViewStore?.getIssues).map((_issueStateKey: any) => (
|
||||
<div
|
||||
key={`${_issueStateKey}`}
|
||||
className="flex-shrink-0 w-[380px] h-full relative flex flex-col"
|
||||
>
|
||||
<div className="flex-shrink-0 w-full p-2 py-1">
|
||||
<IssueHeader />
|
||||
</div>
|
||||
|
||||
<div className="w-full h-full overflow-auto">
|
||||
<Droppable droppableId={`${_issueStateKey}`}>
|
||||
{(provided: any, snapshot: any) => (
|
||||
<>
|
||||
<div {...provided.droppableProps} ref={provided.innerRef}>
|
||||
<IssueContent
|
||||
columnId={_issueStateKey}
|
||||
issues={issueViewStore?.getIssues[_issueStateKey]}
|
||||
/>
|
||||
</div>
|
||||
{provided.placeholder}
|
||||
</>
|
||||
)}
|
||||
</Droppable>
|
||||
</div>
|
||||
|
||||
<div className="flex-shrink-0 w-full p-2 py-1">
|
||||
<IssueFooter />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</DragDropContext>
|
||||
</div>
|
||||
) : (
|
||||
<div>No Issues are available</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
@ -1,11 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
export const KanbanInitLayout = () => {
|
||||
console.log("");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>Hello</div>
|
||||
</div>
|
||||
);
|
||||
};
|
29
web/pages/kanban.tsx
Normal file
29
web/pages/kanban.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import React from "react";
|
||||
// swr
|
||||
import useSWR from "swr";
|
||||
// components
|
||||
import { IssueRoot } from "components/issue-layouts/kanban";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { RootStore } from "store/root";
|
||||
|
||||
const KanbanViewRoot = () => {
|
||||
const workspaceSlug: string = "plane-demo";
|
||||
const projectSlug: string = "5b0e3f6e-c9f1-444d-be22-a8c2706bcf54";
|
||||
|
||||
const store: RootStore = useMobxStore();
|
||||
const { kanban: issueViewStore } = store;
|
||||
|
||||
useSWR(`PROJECT_ISSUES_KANBAN_VIEW`, () => {
|
||||
if (workspaceSlug && projectSlug)
|
||||
issueViewStore.getIssuesAsync(workspaceSlug, projectSlug, "kanban");
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<IssueRoot />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default KanbanViewRoot;
|
@ -17,7 +17,7 @@ const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
||||
const trackEvent =
|
||||
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
||||
|
||||
class ProjectIssuesServices extends APIService {
|
||||
export class ProjectIssuesServices extends APIService {
|
||||
constructor() {
|
||||
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
||||
}
|
||||
|
60
web/store/issue-views/data.ts
Normal file
60
web/store/issue-views/data.ts
Normal file
@ -0,0 +1,60 @@
|
||||
export const filtersPriority: { key: string; title: string }[] = [
|
||||
{ key: "", title: "Urgent" },
|
||||
{ key: "", title: "High" },
|
||||
{ key: "", title: "Medium" },
|
||||
{ key: "", title: "Low" },
|
||||
{ key: "", title: "None" },
|
||||
];
|
||||
|
||||
export const filtersStartDate = [
|
||||
{ key: "", title: "Last Week" },
|
||||
{ key: "", title: "2 weeks from now" },
|
||||
{ key: "", title: "1 month from now" },
|
||||
{ key: "", title: "2 months from now" },
|
||||
{ key: "", title: "Custom" },
|
||||
];
|
||||
|
||||
export const filtersDueDate = [
|
||||
{ key: "", title: "Last Week" },
|
||||
{ key: "", title: "2 weeks from now" },
|
||||
{ key: "", title: "1 month from now" },
|
||||
{ key: "", title: "2 months from now" },
|
||||
{ key: "", title: "Custom" },
|
||||
];
|
||||
|
||||
export const displayPropertyGroupBy = [
|
||||
{ key: "state", title: "States" },
|
||||
{ key: "state_detail.group", title: "State Groups" },
|
||||
{ key: "priority", title: "Priority" },
|
||||
{ key: "labels", title: "Labels" },
|
||||
{ key: "assignees", title: "Assignees" },
|
||||
{ key: "created_by", title: "Created By" },
|
||||
];
|
||||
|
||||
export const displayPropertyOrderBy = [
|
||||
{ key: "sort_order", title: "Manual" },
|
||||
{ key: "created_at", title: "Last Created" },
|
||||
{ key: "updated_at", title: "Last Updated" },
|
||||
{ key: "start_date", title: "Start Date" },
|
||||
{ key: "priority", title: "Priority" },
|
||||
];
|
||||
|
||||
export const displayPropertyIssueType = [
|
||||
{ key: "all", title: "All" },
|
||||
{ key: "active", title: "Active Issues" },
|
||||
{ key: "backlog", title: "Backlog Issues" },
|
||||
];
|
||||
|
||||
export const displayProperties = [
|
||||
{ key: "assignee", title: "Assignee" },
|
||||
{ key: "start_date", title: "Start Date" },
|
||||
{ key: "due_date", title: "Due Date" },
|
||||
{ key: "key", title: "Id" },
|
||||
{ key: "labels", title: "Labels" },
|
||||
{ key: "priority", title: "Priority" },
|
||||
{ key: "state", title: "State" },
|
||||
{ key: "sub_issue_count", title: "Sub Issue Count" },
|
||||
{ key: "attachment_count", title: "Attachment Count" },
|
||||
{ key: "link", title: "Link" },
|
||||
{ key: "estimate", title: "Estimate" },
|
||||
];
|
154
web/store/issue-views/filters.ts
Normal file
154
web/store/issue-views/filters.ts
Normal file
@ -0,0 +1,154 @@
|
||||
import { observable, action, computed, makeObservable, runInAction } from "mobx";
|
||||
// types
|
||||
import { RootStore } from "../root";
|
||||
// services
|
||||
import {} from "services/issues.service";
|
||||
|
||||
export type TIssueViews = "list" | "kanban" | "calendar" | "spreadsheet" | "gantt";
|
||||
|
||||
export interface IIssueFilterStore {
|
||||
loader: boolean;
|
||||
error: any | null;
|
||||
|
||||
// current issue view
|
||||
workspaceId: string | null;
|
||||
projectId: string | null;
|
||||
issueView: string | null;
|
||||
|
||||
// filters
|
||||
priority?: null;
|
||||
state?: null;
|
||||
assignees?: null;
|
||||
createdBy?: null;
|
||||
labels?: null;
|
||||
startDate?: null;
|
||||
dueDate?: null;
|
||||
userSelectedParams?: {
|
||||
assignees: undefined | string;
|
||||
created_by: undefined | string;
|
||||
group_by: undefined | string;
|
||||
labels: undefined | string;
|
||||
order_by: undefined | string;
|
||||
priority: undefined | string;
|
||||
start_date: undefined | string;
|
||||
state: undefined | string;
|
||||
sub_issue: boolean;
|
||||
target_date: undefined | string;
|
||||
type: undefined | string;
|
||||
};
|
||||
|
||||
// display properties
|
||||
displayProperties?: {
|
||||
assignee: boolean;
|
||||
attachment_count: boolean;
|
||||
created_on: boolean;
|
||||
due_date: boolean;
|
||||
estimate: boolean;
|
||||
key: boolean;
|
||||
labels: boolean;
|
||||
link: boolean;
|
||||
priority: boolean;
|
||||
start_date: boolean;
|
||||
state: boolean;
|
||||
sub_issue_count: boolean;
|
||||
updated_on: boolean;
|
||||
};
|
||||
|
||||
// extra's
|
||||
showEmptyGroups?: boolean;
|
||||
|
||||
// actions
|
||||
getProjectIssueFilterProperties: () => any | Promise<any>;
|
||||
getProjectIssueDisplayProperties: () => any | Promise<any>;
|
||||
getProjectMembers: () => any | Promise<any>;
|
||||
getProjectStates: () => any | Promise<any>;
|
||||
getProjectLabels: () => any | Promise<any>;
|
||||
}
|
||||
|
||||
class IssueFilterStore implements IIssueFilterStore {
|
||||
loader: boolean = false;
|
||||
error: any | null = null;
|
||||
|
||||
workspaceId: string | null = null;
|
||||
projectId: string | null = null;
|
||||
issueView: string | null = null;
|
||||
|
||||
// root store
|
||||
rootStore;
|
||||
// service
|
||||
projectPublishService = null;
|
||||
|
||||
constructor(_rootStore: RootStore) {
|
||||
makeObservable(this, {
|
||||
// observable
|
||||
loader: observable,
|
||||
error: observable,
|
||||
|
||||
workspaceId: observable,
|
||||
projectId: observable,
|
||||
|
||||
// action
|
||||
getProjectIssueFilterProperties: action,
|
||||
getProjectIssueDisplayProperties: action,
|
||||
getProjectMembers: action,
|
||||
getProjectStates: action,
|
||||
getProjectLabels: action,
|
||||
// computed
|
||||
});
|
||||
|
||||
this.rootStore = _rootStore;
|
||||
this.projectPublishService = null;
|
||||
}
|
||||
|
||||
// computed functions starts
|
||||
getComputedFilters = (
|
||||
_workspaceId: string,
|
||||
_projectId: string,
|
||||
_view: TIssueViews | null = "kanban"
|
||||
) => {
|
||||
this.workspaceId = _workspaceId;
|
||||
this.projectId = _projectId;
|
||||
this.issueView = _view;
|
||||
|
||||
let filteredRouteParams = {
|
||||
assignees: undefined, // ['user_id', 'user_id']
|
||||
state: undefined, // ['state_id', 'state_id']
|
||||
priority: undefined, // ['low', 'high', 'medium', 'urgent', 'null']
|
||||
type: undefined, // 'active' (started, un_started) || 'backlog' || 'null' (all_the_issues)
|
||||
labels: undefined, // ['label_id', 'label_id']
|
||||
created_by: undefined, // ['user_id', 'user_id']
|
||||
start_date: undefined, // ['yyyy-mm-dd:after/before', 'yyyy-mm-dd:after/before']
|
||||
target_date: undefined, // [yyyy-mm-dd:after, yyyy-mm-dd:before]
|
||||
order_by: "-created_at", // TIssueOrderByOptions
|
||||
group_by: "state", // TIssueGroupByOptions
|
||||
sub_issue: true, // true for all other views except spreadsheet
|
||||
};
|
||||
|
||||
if (_view === "list") filteredRouteParams = { ...filteredRouteParams };
|
||||
if (_view === "kanban") filteredRouteParams = { ...filteredRouteParams };
|
||||
if (_view === "calendar") filteredRouteParams = { ...filteredRouteParams };
|
||||
if (_view === "spreadsheet") filteredRouteParams = { ...filteredRouteParams };
|
||||
if (_view === "gantt") filteredRouteParams = { ...filteredRouteParams };
|
||||
|
||||
return filteredRouteParams;
|
||||
};
|
||||
|
||||
// computed functions ends
|
||||
|
||||
// fetching current user project issue filter and display settings
|
||||
getProjectIssueFilterProperties = () => {};
|
||||
|
||||
// fetching display properties
|
||||
getProjectIssueDisplayProperties = () => {};
|
||||
|
||||
// fetching project members
|
||||
getProjectMembers = () => {};
|
||||
|
||||
// fetching project state <-> groups
|
||||
getProjectStates = () => {};
|
||||
|
||||
// fetching project labels
|
||||
getProjectLabels = () => {};
|
||||
}
|
||||
|
||||
export default IssueFilterStore;
|
92
web/store/issue-views/kanban.ts
Normal file
92
web/store/issue-views/kanban.ts
Normal file
@ -0,0 +1,92 @@
|
||||
import { observable, action, computed, makeObservable, runInAction } from "mobx";
|
||||
// types
|
||||
import { RootStore } from "../root";
|
||||
// services
|
||||
import { ProjectIssuesServices } from "services/issues.service";
|
||||
// types
|
||||
import { TIssueViews } from "./filters";
|
||||
|
||||
export interface IKanbanStore {
|
||||
loader: boolean;
|
||||
error: any | null;
|
||||
issues: { [key: string]: any } | null;
|
||||
|
||||
getIssuesAsync: (
|
||||
workspaceId: string,
|
||||
projectId: string,
|
||||
view: TIssueViews | null
|
||||
) => null | Promise<any>;
|
||||
}
|
||||
|
||||
class KanbanStore implements IKanbanStore {
|
||||
loader: boolean = false;
|
||||
error: any | null = null;
|
||||
|
||||
issues: { [key: string]: any } | null = null;
|
||||
|
||||
// root store
|
||||
rootStore;
|
||||
// service
|
||||
issueService;
|
||||
|
||||
constructor(_rootStore: RootStore) {
|
||||
makeObservable(this, {
|
||||
// observable
|
||||
loader: observable,
|
||||
error: observable,
|
||||
|
||||
issues: observable.ref,
|
||||
// action
|
||||
getIssuesAsync: action,
|
||||
// computed
|
||||
});
|
||||
|
||||
this.rootStore = _rootStore;
|
||||
this.issueService = new ProjectIssuesServices();
|
||||
}
|
||||
|
||||
// computed
|
||||
get getIssues() {
|
||||
if (this.rootStore.issueFilters.projectId && this.issues != null)
|
||||
return this.issues[this.rootStore.issueFilters.projectId];
|
||||
else return null;
|
||||
}
|
||||
|
||||
// fetching issues
|
||||
getIssuesAsync = async (workspaceId: string, projectId: string, view: TIssueViews | null) => {
|
||||
try {
|
||||
this.loader = true;
|
||||
this.error = null;
|
||||
|
||||
const filteredParams = this.rootStore.issueFilters.getComputedFilters(
|
||||
workspaceId,
|
||||
projectId,
|
||||
view
|
||||
);
|
||||
const issuesResponse = await this.issueService.getIssuesWithParams(
|
||||
workspaceId,
|
||||
projectId,
|
||||
filteredParams
|
||||
);
|
||||
|
||||
if (issuesResponse) {
|
||||
runInAction(() => {
|
||||
this.issues = { ...this.issues, [projectId]: { ...issuesResponse } };
|
||||
this.loader = false;
|
||||
this.error = null;
|
||||
});
|
||||
}
|
||||
|
||||
return issuesResponse;
|
||||
} catch (error) {
|
||||
console.warn("error", error);
|
||||
this.loader = false;
|
||||
this.error = null;
|
||||
return error;
|
||||
}
|
||||
};
|
||||
|
||||
// handle issue drag and drop
|
||||
}
|
||||
|
||||
export default KanbanStore;
|
@ -1,82 +0,0 @@
|
||||
import { observable, action, computed, makeObservable, runInAction } from "mobx";
|
||||
// types
|
||||
import { RootStore } from "./root";
|
||||
// services
|
||||
|
||||
export interface IKanbanStore {
|
||||
loader: boolean;
|
||||
error: any | null;
|
||||
|
||||
// current issue view
|
||||
issueView?: "kanban";
|
||||
|
||||
// filters
|
||||
priority?: null;
|
||||
state?: null;
|
||||
assignees?: null;
|
||||
createdBy?: null;
|
||||
labels?: null;
|
||||
startDate?: null;
|
||||
dueDate?: null;
|
||||
userSelectedParams?: {
|
||||
assignees: undefined | string;
|
||||
created_by: undefined | string;
|
||||
group_by: undefined | string;
|
||||
labels: undefined | string;
|
||||
order_by: undefined | string;
|
||||
priority: undefined | string;
|
||||
start_date: undefined | string;
|
||||
state: undefined | string;
|
||||
sub_issue: boolean;
|
||||
target_date: undefined | string;
|
||||
type: undefined | string;
|
||||
};
|
||||
|
||||
// display properties
|
||||
displayProperties?: {
|
||||
assignee: boolean;
|
||||
attachment_count: boolean;
|
||||
created_on: boolean;
|
||||
due_date: boolean;
|
||||
estimate: boolean;
|
||||
key: boolean;
|
||||
labels: boolean;
|
||||
link: boolean;
|
||||
priority: boolean;
|
||||
start_date: boolean;
|
||||
state: boolean;
|
||||
sub_issue_count: boolean;
|
||||
updated_on: boolean;
|
||||
};
|
||||
|
||||
// extra's
|
||||
showEmptyGroups?: boolean;
|
||||
|
||||
issues?: null;
|
||||
}
|
||||
|
||||
class KanbanStore implements IKanbanStore {
|
||||
loader: boolean = false;
|
||||
error: any | null = null;
|
||||
|
||||
// root store
|
||||
rootStore;
|
||||
// service
|
||||
projectPublishService = null;
|
||||
|
||||
constructor(_rootStore: RootStore) {
|
||||
makeObservable(this, {
|
||||
// observable
|
||||
loader: observable,
|
||||
error: observable,
|
||||
|
||||
// action
|
||||
// computed
|
||||
});
|
||||
|
||||
this.rootStore = _rootStore;
|
||||
this.projectPublishService = null;
|
||||
}
|
||||
}
|
||||
|
||||
export default KanbanStore;
|
@ -5,8 +5,10 @@ import UserStore from "./user";
|
||||
import ThemeStore from "./theme";
|
||||
import ProjectStore, { IProjectStore } from "./project";
|
||||
import ProjectPublishStore, { IProjectPublishStore } from "./project-publish";
|
||||
import KanbanStore from "./kanban";
|
||||
import IssuesStore from "./issues";
|
||||
// issues views and filters
|
||||
import IssueFilterStore from "./issue-views/filters";
|
||||
import KanbanStore from "./issue-views/kanban";
|
||||
|
||||
enableStaticRendering(typeof window === "undefined");
|
||||
|
||||
@ -16,6 +18,7 @@ export class RootStore {
|
||||
project: IProjectStore;
|
||||
projectPublish: IProjectPublishStore;
|
||||
issues: IssuesStore;
|
||||
issueFilters: IssueFilterStore;
|
||||
kanban: KanbanStore;
|
||||
|
||||
constructor() {
|
||||
@ -24,6 +27,7 @@ export class RootStore {
|
||||
this.project = new ProjectStore(this);
|
||||
this.projectPublish = new ProjectPublishStore(this);
|
||||
this.issues = new IssuesStore(this);
|
||||
this.issueFilters = new IssueFilterStore(this);
|
||||
this.kanban = new KanbanStore(this);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user