New Directory Setup (#2065)
* chore: moved app & space from apps to root * chore: modified workspace configuration * chore: modified dockerfiles for space and web * chore: modified icons for space * feat: updated files for new svg icons supported by next-images * chore: added /spaces base path for next * chore: added compose config for space * chore: updated husky configuration * chore: updated workflows for new configuration * chore: changed app name to web * fix: resolved build errors with web * chore: reset file tracing root for both projects * chore: added nginx config for deploy * fix: eslint and tsconfig settings for space app * husky setup fixes based on new dir * eslint fixes * prettier formatting --------- Co-authored-by: Henit Chobisa <chobisa.henit@gmail.com>
This commit is contained in:
parent
20e36194b4
commit
1e152c666c
1022 changed files with 1475 additions and 1240 deletions
|
|
@ -1,10 +0,0 @@
|
|||
"use client";
|
||||
|
||||
export const IssueBlockDownVotes = ({ number }: { number: number }) => (
|
||||
<div className="h-6 rounded flex px-1.5 pl-1 py-1 items-center border-[0.5px] border-custom-border-300 text-custom-text-300 text-xs">
|
||||
<span className="material-symbols-rounded text-base !p-0 !m-0 rotate-180 text-custom-text-300">
|
||||
arrow_upward_alt
|
||||
</span>
|
||||
{number}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
"use client";
|
||||
|
||||
// helpers
|
||||
import { renderFullDate } from "constants/helpers";
|
||||
|
||||
export const findHowManyDaysLeft = (date: string | Date) => {
|
||||
const today = new Date();
|
||||
const eventDate = new Date(date);
|
||||
const timeDiff = Math.abs(eventDate.getTime() - today.getTime());
|
||||
|
||||
return Math.ceil(timeDiff / (1000 * 3600 * 24));
|
||||
};
|
||||
|
||||
const dueDateIcon = (
|
||||
date: string,
|
||||
stateGroup: string
|
||||
): {
|
||||
iconName: string;
|
||||
className: string;
|
||||
} => {
|
||||
let iconName = "calendar_today";
|
||||
let className = "";
|
||||
|
||||
if (!date || ["completed", "cancelled"].includes(stateGroup)) {
|
||||
iconName = "calendar_today";
|
||||
className = "";
|
||||
} else {
|
||||
const today = new Date();
|
||||
const dueDate = new Date(date);
|
||||
|
||||
if (dueDate < today) {
|
||||
iconName = "event_busy";
|
||||
className = "text-red-500";
|
||||
} else if (dueDate > today) {
|
||||
iconName = "calendar_today";
|
||||
className = "";
|
||||
} else {
|
||||
iconName = "today";
|
||||
className = "text-red-500";
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
iconName,
|
||||
className,
|
||||
};
|
||||
};
|
||||
|
||||
export const IssueBlockDueDate = ({ due_date, group }: { due_date: string; group: string }) => {
|
||||
const iconDetails = dueDateIcon(due_date, group);
|
||||
|
||||
return (
|
||||
<div className="rounded flex px-2.5 py-1 items-center border-[0.5px] border-custom-border-300 gap-1 text-custom-text-100 text-xs">
|
||||
<span className={`material-symbols-rounded text-sm -my-0.5 ${iconDetails.className}`}>
|
||||
{iconDetails.iconName}
|
||||
</span>
|
||||
{renderFullDate(due_date)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
"use client";
|
||||
|
||||
export const IssueBlockLabels = ({ labels }: any) => (
|
||||
<div className="relative flex items-center flex-wrap gap-1">
|
||||
{labels &&
|
||||
labels.length > 0 &&
|
||||
labels.map((_label: any) => (
|
||||
<div
|
||||
key={_label?.id}
|
||||
className="flex cursor-default items-center flex-shrink-0 rounded-md border border-custom-border-300 px-2.5 py-1 text-xs shadow-sm"
|
||||
>
|
||||
<div className="flex items-center gap-1.5 text-custom-text-200">
|
||||
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: `${_label?.color}` }} />
|
||||
<div className="text-xs">{_label?.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
"use client";
|
||||
|
||||
// types
|
||||
import { TIssuePriorityKey } from "types/issue";
|
||||
// constants
|
||||
import { issuePriorityFilter } from "constants/data";
|
||||
|
||||
export const IssueBlockPriority = ({ priority }: { priority: TIssuePriorityKey | null }) => {
|
||||
const priority_detail = priority != null ? issuePriorityFilter(priority) : null;
|
||||
|
||||
if (priority_detail === null) return <></>;
|
||||
|
||||
return (
|
||||
<div className={`h-6 w-6 rounded grid place-items-center border-[0.5px] ${priority_detail?.className}`}>
|
||||
<span className="material-symbols-rounded text-sm">{priority_detail?.icon}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
// constants
|
||||
import { issueGroupFilter } from "constants/data";
|
||||
|
||||
export const IssueBlockState = ({ state }: any) => {
|
||||
const stateGroup = issueGroupFilter(state.group);
|
||||
|
||||
if (stateGroup === null) return <></>;
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-1 w-full rounded shadow-sm border-[0.5px] border-custom-border-300 duration-300 focus:outline-none px-2.5 py-1 text-xs">
|
||||
<div className="flex items-center w-full gap-1.5 text-custom-text-200">
|
||||
<stateGroup.icon />
|
||||
<div className="text-xs">{state?.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
"use client";
|
||||
|
||||
export const IssueBlockUpVotes = ({ number }: { number: number }) => (
|
||||
<div className="h-6 rounded flex px-1.5 pl-1 py-1 items-center border-[0.5px] border-custom-border-300 text-custom-text-300 text-xs">
|
||||
<span className="material-symbols-rounded text-base !p-0 !m-0 text-custom-text-300">arrow_upward_alt</span>
|
||||
{number}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -1 +0,0 @@
|
|||
export const IssueCalendarView = () => <div> </div>;
|
||||
|
|
@ -1 +0,0 @@
|
|||
export const IssueGanttView = () => <div> </div>;
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
"use client";
|
||||
|
||||
// mobx react lite
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
|
||||
// components
|
||||
import { IssueBlockPriority } from "components/issues/board-views/block-priority";
|
||||
import { IssueBlockState } from "components/issues/board-views/block-state";
|
||||
import { IssueBlockLabels } from "components/issues/board-views/block-labels";
|
||||
import { IssueBlockDueDate } from "components/issues/board-views/block-due-date";
|
||||
// interfaces
|
||||
import { IIssue } from "types/issue";
|
||||
import { RootStore } from "store/root";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
export const IssueListBlock = observer(({ issue }: { issue: IIssue }) => {
|
||||
const { project: projectStore, issueDetails: issueDetailStore }: RootStore = useMobxStore();
|
||||
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspace_slug, project_slug, board } = router.query;
|
||||
|
||||
const handleBlockClick = () => {
|
||||
issueDetailStore.setPeekId(issue.id);
|
||||
router.replace(
|
||||
{
|
||||
pathname: `/${workspace_slug?.toString()}/${project_slug}`,
|
||||
query: {
|
||||
board: board?.toString(),
|
||||
peekId: issue.id,
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
{ shallow: true }
|
||||
);
|
||||
// router.push(`/${workspace_slug?.toString()}/${project_slug}?board=${board?.toString()}&peekId=${issue.id}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="py-3 px-4 h-[118px] flex flex-col gap-1.5 bg-custom-background-100 rounded shadow-custom-shadow-sm border-[0.5px] border-custom-border-200">
|
||||
{/* id */}
|
||||
<div className="text-xs text-custom-text-300 break-words">
|
||||
{projectStore?.project?.identifier}-{issue?.sequence_id}
|
||||
</div>
|
||||
|
||||
{/* name */}
|
||||
<h6 onClick={handleBlockClick} className="text-sm font-medium break-words line-clamp-2 cursor-pointer">
|
||||
{issue.name}
|
||||
</h6>
|
||||
|
||||
<div className="relative flex-grow flex items-end gap-2 w-full overflow-x-scroll hide-horizontal-scrollbar">
|
||||
{/* priority */}
|
||||
{issue?.priority && (
|
||||
<div className="flex-shrink-0">
|
||||
<IssueBlockPriority priority={issue?.priority} />
|
||||
</div>
|
||||
)}
|
||||
{/* state */}
|
||||
{issue?.state_detail && (
|
||||
<div className="flex-shrink-0">
|
||||
<IssueBlockState state={issue?.state_detail} />
|
||||
</div>
|
||||
)}
|
||||
{/* due date */}
|
||||
{issue?.target_date && (
|
||||
<div className="flex-shrink-0">
|
||||
<IssueBlockDueDate due_date={issue?.target_date} group={issue?.state_detail?.group} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
"use client";
|
||||
|
||||
// mobx react lite
|
||||
import { observer } from "mobx-react-lite";
|
||||
// interfaces
|
||||
import { IIssueState } from "types/issue";
|
||||
// constants
|
||||
import { issueGroupFilter } from "constants/data";
|
||||
// mobx hook
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { RootStore } from "store/root";
|
||||
|
||||
export const IssueListHeader = observer(({ state }: { state: IIssueState }) => {
|
||||
const store: RootStore = useMobxStore();
|
||||
|
||||
const stateGroup = issueGroupFilter(state.group);
|
||||
|
||||
if (stateGroup === null) return <></>;
|
||||
|
||||
return (
|
||||
<div className="pb-2 px-2 flex items-center">
|
||||
<div className="w-4 h-4 flex justify-center items-center flex-shrink-0">
|
||||
<stateGroup.icon />
|
||||
</div>
|
||||
<div className="font-semibold text-custom-text-200 capitalize ml-2 mr-3 truncate">{state?.name}</div>
|
||||
<span className="text-custom-text-300 rounded-full flex-shrink-0">
|
||||
{store.issue.getCountOfIssuesByState(state.id)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
"use client";
|
||||
|
||||
// mobx react lite
|
||||
import { observer } from "mobx-react-lite";
|
||||
// components
|
||||
import { IssueListHeader } from "components/issues/board-views/kanban/header";
|
||||
import { IssueListBlock } from "components/issues/board-views/kanban/block";
|
||||
// ui
|
||||
import { Icon } from "components/ui";
|
||||
// interfaces
|
||||
import { IIssueState, IIssue } from "types/issue";
|
||||
// mobx hook
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { RootStore } from "store/root";
|
||||
|
||||
export const IssueKanbanView = observer(() => {
|
||||
const store: RootStore = useMobxStore();
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-full overflow-hidden overflow-x-auto flex gap-3">
|
||||
{store?.issue?.states &&
|
||||
store?.issue?.states.length > 0 &&
|
||||
store?.issue?.states.map((_state: IIssueState) => (
|
||||
<div key={_state.id} className="flex-shrink-0 relative w-[340px] h-full flex flex-col">
|
||||
<div className="flex-shrink-0">
|
||||
<IssueListHeader state={_state} />
|
||||
</div>
|
||||
<div className="w-full h-full overflow-hidden overflow-y-auto hide-vertical-scrollbar">
|
||||
{store.issue.getFilteredIssuesByState(_state.id) &&
|
||||
store.issue.getFilteredIssuesByState(_state.id).length > 0 ? (
|
||||
<div className="space-y-3 pb-2 px-2">
|
||||
{store.issue.getFilteredIssuesByState(_state.id).map((_issue: IIssue) => (
|
||||
<IssueListBlock key={_issue.id} issue={_issue} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex justify-center items-center gap-2 pt-10 text-center text-sm text-custom-text-200 font-medium">
|
||||
<Icon iconName="stack" />
|
||||
No issues in this state
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
import { FC } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// components
|
||||
import { IssueBlockPriority } from "components/issues/board-views/block-priority";
|
||||
import { IssueBlockState } from "components/issues/board-views/block-state";
|
||||
import { IssueBlockLabels } from "components/issues/board-views/block-labels";
|
||||
import { IssueBlockDueDate } from "components/issues/board-views/block-due-date";
|
||||
import { IssueBlockUpVotes } from "components/issues/board-views/block-upvotes";
|
||||
import { IssueBlockDownVotes } from "components/issues/board-views/block-downvotes";
|
||||
// mobx hook
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// interfaces
|
||||
import { IIssue } from "types/issue";
|
||||
// store
|
||||
import { RootStore } from "store/root";
|
||||
import { IssueVotes } from "components/issues/peek-overview";
|
||||
|
||||
export const IssueListBlock: FC<{ issue: IIssue }> = observer((props) => {
|
||||
const { issue } = props;
|
||||
// store
|
||||
const { project: projectStore, issueDetails: issueDetailStore }: RootStore = useMobxStore();
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspace_slug, project_slug, board } = router.query;
|
||||
|
||||
const handleBlockClick = () => {
|
||||
issueDetailStore.setPeekId(issue.id);
|
||||
router.replace(
|
||||
{
|
||||
pathname: `/${workspace_slug?.toString()}/${project_slug}`,
|
||||
query: {
|
||||
board: board?.toString(),
|
||||
peekId: issue.id,
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
{ shallow: true }
|
||||
);
|
||||
// router.push(`/${workspace_slug?.toString()}/${project_slug}?board=${board?.toString()}&peekId=${issue.id}`);
|
||||
};
|
||||
|
||||
const totalUpVotes = issue.votes.filter((v) => v.vote === 1);
|
||||
const totalDownVotes = issue.votes.filter((v) => v.vote === -1);
|
||||
|
||||
return (
|
||||
<div className="flex items-center px-6 py-3.5 relative gap-10 bg-custom-background-100">
|
||||
<div className="relative flex items-center gap-5 w-full flex-grow overflow-hidden">
|
||||
{/* id */}
|
||||
<div className="flex-shrink-0 text-sm text-custom-text-300">
|
||||
{projectStore?.project?.identifier}-{issue?.sequence_id}
|
||||
</div>
|
||||
{/* name */}
|
||||
<div onClick={handleBlockClick} className="font-medium text-sm truncate flex-grow cursor-pointer">
|
||||
{issue.name}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="inline-flex flex-shrink-0 items-center gap-2 text-xs">
|
||||
{/* priority */}
|
||||
{issue?.priority && (
|
||||
<div className="flex-shrink-0">
|
||||
<IssueBlockPriority priority={issue?.priority} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* state */}
|
||||
{issue?.state_detail && (
|
||||
<div className="flex-shrink-0">
|
||||
<IssueBlockState state={issue?.state_detail} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* labels */}
|
||||
{issue?.label_details && issue?.label_details.length > 0 && (
|
||||
<div className="flex-shrink-0">
|
||||
<IssueBlockLabels labels={issue?.label_details} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* due date */}
|
||||
{issue?.target_date && (
|
||||
<div className="flex-shrink-0">
|
||||
<IssueBlockDueDate due_date={issue?.target_date} group={issue?.state_detail?.group} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
"use client";
|
||||
|
||||
// mobx react lite
|
||||
import { observer } from "mobx-react-lite";
|
||||
// interfaces
|
||||
import { IIssueState } from "types/issue";
|
||||
// constants
|
||||
import { issueGroupFilter } from "constants/data";
|
||||
// mobx hook
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { RootStore } from "store/root";
|
||||
|
||||
export const IssueListHeader = observer(({ state }: { state: IIssueState }) => {
|
||||
const store: RootStore = useMobxStore();
|
||||
|
||||
const stateGroup = issueGroupFilter(state.group);
|
||||
|
||||
if (stateGroup === null) return <></>;
|
||||
|
||||
return (
|
||||
<div className="px-6 py-2 flex items-center">
|
||||
<div className="w-4 h-4 flex justify-center items-center">
|
||||
<stateGroup.icon />
|
||||
</div>
|
||||
<div className="font-semibold capitalize ml-2 mr-3">{state?.name}</div>
|
||||
<div className="text-custom-text-200">{store.issue.getCountOfIssuesByState(state.id)}</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
import { useEffect } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// components
|
||||
import { IssueListHeader } from "components/issues/board-views/list/header";
|
||||
import { IssueListBlock } from "components/issues/board-views/list/block";
|
||||
// interfaces
|
||||
import { IIssueState, IIssue } from "types/issue";
|
||||
// mobx hook
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// store
|
||||
import { RootStore } from "store/root";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
export const IssueListView = observer(() => {
|
||||
const { issue: issueStore }: RootStore = useMobxStore();
|
||||
|
||||
return (
|
||||
<>
|
||||
{issueStore?.states &&
|
||||
issueStore?.states.length > 0 &&
|
||||
issueStore?.states.map((_state: IIssueState) => (
|
||||
<div key={_state.id} className="relative w-full">
|
||||
<IssueListHeader state={_state} />
|
||||
{issueStore.getFilteredIssuesByState(_state.id) &&
|
||||
issueStore.getFilteredIssuesByState(_state.id).length > 0 ? (
|
||||
<div className="divide-y divide-custom-border-200">
|
||||
{issueStore.getFilteredIssuesByState(_state.id).map((_issue: IIssue) => (
|
||||
<IssueListBlock key={_issue.id} issue={_issue} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="px-6 py-3.5 text-sm text-custom-text-200 bg-custom-background-100">
|
||||
No Issues are available.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
@ -1 +0,0 @@
|
|||
export const IssueSpreadsheetView = () => <div> </div>;
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
import { useRouter } from "next/router";
|
||||
// mobx react lite
|
||||
import { observer } from "mobx-react-lite";
|
||||
// components
|
||||
import IssueStateFilter from "./state";
|
||||
import IssueLabelFilter from "./label";
|
||||
import IssuePriorityFilter from "./priority";
|
||||
// mobx hook
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { RootStore } from "store/root";
|
||||
|
||||
const IssueFilter = observer(() => {
|
||||
const store: RootStore = useMobxStore();
|
||||
|
||||
const router = useRouter();
|
||||
const { workspace_slug, project_slug } = router.query as { workspace_slug: string; project_slug: string };
|
||||
|
||||
const clearAllFilters = () => {
|
||||
// router.replace(
|
||||
// store.issue.getURLDefinition(workspace_slug, project_slug, {
|
||||
// key: "all",
|
||||
// removeAll: true,
|
||||
// })
|
||||
// );
|
||||
};
|
||||
|
||||
// if (store.issue.getIfFiltersIsEmpty()) return null;
|
||||
|
||||
return (
|
||||
<div className="flex-shrink-0 min-h-[50px] h-auto py-1.5 border-b border-custom-border-200 relative flex items-center shadow-md bg-whiate select-none">
|
||||
<div className="px-5 flex justify-start items-center flex-wrap gap-2 text-sm">
|
||||
{/* state */}
|
||||
{/* {store.issue.checkIfFilterExistsForKey("state") && <IssueStateFilter />} */}
|
||||
{/* labels */}
|
||||
{/* {store.issue.checkIfFilterExistsForKey("label") && <IssueLabelFilter />} */}
|
||||
{/* priority */}
|
||||
{/* {store.issue.checkIfFilterExistsForKey("priority") && <IssuePriorityFilter />} */}
|
||||
{/* clear all filters */}
|
||||
<div
|
||||
className="flex items-center gap-2 border border-custom-border-200 px-2 py-1 cursor-pointer text-xs rounded-full"
|
||||
onClick={clearAllFilters}
|
||||
>
|
||||
<div>Clear all filters</div>
|
||||
<div className="flex-shrink-0 w-3 h-3 cursor-pointer flex justify-center items-center overflow-hidden rounded-sm">
|
||||
<span className="material-symbols-rounded text-[12px]">close</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default IssueFilter;
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
import { useRouter } from "next/router";
|
||||
// mobx react lite
|
||||
import { observer } from "mobx-react-lite";
|
||||
// mobx hook
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// interfaces
|
||||
import { IIssueLabel } from "types/issue";
|
||||
|
||||
export const RenderIssueLabel = observer(({ label }: { label: IIssueLabel }) => {
|
||||
const store = useMobxStore();
|
||||
|
||||
const router = useRouter();
|
||||
const { workspace_slug, project_slug } = router.query as { workspace_slug: string; project_slug: string };
|
||||
|
||||
const removeLabelFromFilter = () => {
|
||||
// router.replace(
|
||||
// store.issue.getURLDefinition(workspace_slug, project_slug, {
|
||||
// key: "label",
|
||||
// value: label?.id,
|
||||
// })
|
||||
// );
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex-shrink-0 relative flex items-center flex-wrap gap-1 px-2 py-0.5 rounded-full select-none"
|
||||
style={{ color: label?.color, backgroundColor: `${label?.color}10` }}
|
||||
>
|
||||
<div
|
||||
className="flex-shrink-0 w-1.5 h-1.5 flex justify-center items-center overflow-hidden rounded-full"
|
||||
style={{ backgroundColor: `${label?.color}` }}
|
||||
/>
|
||||
|
||||
<div className="font-medium whitespace-nowrap text-xs">{label?.name}</div>
|
||||
<div
|
||||
className="flex-shrink-0 w-3 h-3 cursor-pointer flex justify-center items-center overflow-hidden rounded-full"
|
||||
onClick={removeLabelFromFilter}
|
||||
>
|
||||
<span className="material-symbols-rounded text-xs">close</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
import { useRouter } from "next/router";
|
||||
// mobx react lite
|
||||
import { observer } from "mobx-react-lite";
|
||||
// components
|
||||
import { RenderIssueLabel } from "./filter-label-block";
|
||||
// interfaces
|
||||
import { IIssueLabel } from "types/issue";
|
||||
// mobx hook
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { RootStore } from "store/root";
|
||||
|
||||
const IssueLabelFilter = observer(() => {
|
||||
const store: RootStore = useMobxStore();
|
||||
|
||||
const router = useRouter();
|
||||
const { workspace_slug, project_slug } = router.query as { workspace_slug: string; project_slug: string };
|
||||
|
||||
const clearLabelFilters = () => {
|
||||
// router.replace(
|
||||
// store.issue.getURLDefinition(workspace_slug, project_slug, {
|
||||
// key: "label",
|
||||
// removeAll: true,
|
||||
// })
|
||||
// );
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center gap-2 border border-custom-border-300 px-2 py-1 rounded-full text-xs">
|
||||
<div className="flex-shrink-0 text-custom-text-200">Labels</div>
|
||||
<div className="relative flex flex-wrap items-center gap-1">
|
||||
{/* {store?.issue?.labels &&
|
||||
store?.issue?.labels.map(
|
||||
(_label: IIssueLabel, _index: number) =>
|
||||
store.issue.getUserSelectedFilter("label", _label.id) && (
|
||||
<RenderIssueLabel key={_label.id} label={_label} />
|
||||
)
|
||||
)} */}
|
||||
</div>
|
||||
<div
|
||||
className="flex-shrink-0 w-3 h-3 cursor-pointer flex justify-center items-center overflow-hidden rounded-sm"
|
||||
onClick={clearLabelFilters}
|
||||
>
|
||||
<span className="material-symbols-rounded text-[12px]">close</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default IssueLabelFilter;
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
import { useRouter } from "next/router";
|
||||
// mobx react lite
|
||||
import { observer } from "mobx-react-lite";
|
||||
// mobx hook
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// interfaces
|
||||
import { IIssuePriorityFilters } from "types/issue";
|
||||
|
||||
export const RenderIssuePriority = observer(({ priority }: { priority: IIssuePriorityFilters }) => {
|
||||
const store = useMobxStore();
|
||||
|
||||
const router = useRouter();
|
||||
const { workspace_slug, project_slug } = router.query as { workspace_slug: string; project_slug: string };
|
||||
|
||||
const removePriorityFromFilter = () => {
|
||||
// router.replace(
|
||||
// store.issue.getURLDefinition(workspace_slug, project_slug, {
|
||||
// key: "priority",
|
||||
// value: priority?.key,
|
||||
// })
|
||||
// );
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex-shrink-0 relative flex items-center flex-wrap gap-1 px-2 py-0.5 text-xs rounded-full select-none ${
|
||||
priority.className || ``
|
||||
}`}
|
||||
>
|
||||
<div className="flex-shrink-0 flex justify-center items-center overflow-hidden rounded-full">
|
||||
<span className="material-symbols-rounded text-xs">{priority?.icon}</span>
|
||||
</div>
|
||||
<div className="whitespace-nowrap">{priority?.title}</div>
|
||||
<div
|
||||
className="flex-shrink-0 w-3 h-3 cursor-pointer flex justify-center items-center overflow-hidden rounded-full"
|
||||
onClick={removePriorityFromFilter}
|
||||
>
|
||||
<span className="material-symbols-rounded text-xs">close</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
import { useRouter } from "next/router";
|
||||
// mobx react lite
|
||||
import { observer } from "mobx-react-lite";
|
||||
// mobx hook
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { RenderIssuePriority } from "./filter-priority-block";
|
||||
// interfaces
|
||||
import { IIssuePriorityFilters } from "types/issue";
|
||||
// constants
|
||||
import { issuePriorityFilters } from "constants/data";
|
||||
|
||||
const IssuePriorityFilter = observer(() => {
|
||||
const store = useMobxStore();
|
||||
|
||||
const router = useRouter();
|
||||
const { workspace_slug, project_slug } = router.query as { workspace_slug: string; project_slug: string };
|
||||
|
||||
const clearPriorityFilters = () => {
|
||||
// router.replace(
|
||||
// store.issue.getURLDefinition(workspace_slug, project_slug, {
|
||||
// key: "priority",
|
||||
// removeAll: true,
|
||||
// })
|
||||
// );
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center gap-2 border border-custom-border-300 px-2 py-1 rounded-full text-xs">
|
||||
<div className="flex-shrink-0 text-custom-text-200">Priority</div>
|
||||
<div className="relative flex flex-wrap items-center gap-1">
|
||||
{/* {issuePriorityFilters.map(
|
||||
(_priority: IIssuePriorityFilters, _index: number) =>
|
||||
store.issue.getUserSelectedFilter("priority", _priority.key) && (
|
||||
<RenderIssuePriority key={_priority.key} priority={_priority} />
|
||||
)
|
||||
)} */}
|
||||
</div>
|
||||
<div
|
||||
className="flex-shrink-0 w-3 h-3 cursor-pointer flex justify-center items-center overflow-hidden rounded-sm"
|
||||
onClick={() => {
|
||||
clearPriorityFilters();
|
||||
}}
|
||||
>
|
||||
<span className="material-symbols-rounded text-[12px]">close</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default IssuePriorityFilter;
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
import { useRouter } from "next/router";
|
||||
// mobx react lite
|
||||
import { observer } from "mobx-react-lite";
|
||||
// mobx hook
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// interfaces
|
||||
import { IIssueState } from "types/issue";
|
||||
// constants
|
||||
import { issueGroupFilter } from "constants/data";
|
||||
|
||||
export const RenderIssueState = observer(({ state }: { state: IIssueState }) => {
|
||||
const store = useMobxStore();
|
||||
|
||||
const router = useRouter();
|
||||
const { workspace_slug, project_slug } = router.query as { workspace_slug: string; project_slug: string };
|
||||
|
||||
const stateGroup = issueGroupFilter(state.group);
|
||||
|
||||
const removeStateFromFilter = () => {
|
||||
// router.replace(
|
||||
// store.issue.getURLDefinition(workspace_slug, project_slug, {
|
||||
// key: "state",
|
||||
// value: state?.id,
|
||||
// })
|
||||
// );
|
||||
};
|
||||
|
||||
if (stateGroup === null) return <></>;
|
||||
return (
|
||||
<div className={`inline-flex items-center gap-x-1 rounded-full px-2 py-0.5 ${stateGroup.className || ``}`}>
|
||||
<div className="flex-shrink-0 w-3 h-3 flex justify-center items-center overflow-hidden rounded-full">
|
||||
<stateGroup.icon />
|
||||
</div>
|
||||
<div className="text-xs font-medium whitespace-nowrap">{state?.name}</div>
|
||||
<div
|
||||
className="flex-shrink-0 w-3 h-3 cursor-pointer flex justify-center items-center overflow-hidden rounded-full"
|
||||
onClick={removeStateFromFilter}
|
||||
>
|
||||
<span className="material-symbols-rounded text-xs">close</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
import { useRouter } from "next/router";
|
||||
// mobx react lite
|
||||
import { observer } from "mobx-react-lite";
|
||||
// components
|
||||
import { RenderIssueState } from "./filter-state-block";
|
||||
// interfaces
|
||||
import { IIssueState } from "types/issue";
|
||||
// mobx hook
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { RootStore } from "store/root";
|
||||
|
||||
const IssueStateFilter = observer(() => {
|
||||
const store: RootStore = useMobxStore();
|
||||
|
||||
const router = useRouter();
|
||||
const { workspace_slug, project_slug } = router.query as { workspace_slug: string; project_slug: string };
|
||||
|
||||
const clearStateFilters = () => {
|
||||
// router.replace(
|
||||
// store.issue.getURLDefinition(workspace_slug, project_slug, {
|
||||
// key: "state",
|
||||
// removeAll: true,
|
||||
// })
|
||||
// );
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center gap-2 border border-custom-border-300 px-2 py-1 rounded-full text-xs">
|
||||
<div className="flex-shrink-0 text-custom-text-200">State</div>
|
||||
<div className="relative flex flex-wrap items-center gap-1">
|
||||
{/* {store?.issue?.states &&
|
||||
store?.issue?.states.map(
|
||||
(_state: IIssueState, _index: number) =>
|
||||
store.issue.getUserSelectedFilter("state", _state.id) && (
|
||||
<RenderIssueState key={_state.id} state={_state} />
|
||||
)
|
||||
)} */}
|
||||
</div>
|
||||
<div
|
||||
className="flex-shrink-0 w-3 h-3 cursor-pointer flex justify-center items-center overflow-hidden rounded-sm"
|
||||
onClick={clearStateFilters}
|
||||
>
|
||||
<span className="material-symbols-rounded text-[12px]">close</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default IssueStateFilter;
|
||||
|
|
@ -1,117 +0,0 @@
|
|||
import { useEffect } from "react";
|
||||
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// mobx
|
||||
import { observer } from "mobx-react-lite";
|
||||
// components
|
||||
import { NavbarSearch } from "./search";
|
||||
import { NavbarIssueBoardView } from "./issue-board-view";
|
||||
import { NavbarTheme } from "./theme";
|
||||
// ui
|
||||
import { PrimaryButton } from "components/ui";
|
||||
// lib
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// store
|
||||
import { RootStore } from "store/root";
|
||||
|
||||
const renderEmoji = (emoji: string | { name: string; color: string }) => {
|
||||
if (!emoji) return;
|
||||
|
||||
if (typeof emoji === "object")
|
||||
return (
|
||||
<span style={{ color: emoji.color }} className="material-symbols-rounded text-lg">
|
||||
{emoji.name}
|
||||
</span>
|
||||
);
|
||||
else return isNaN(parseInt(emoji)) ? emoji : String.fromCodePoint(parseInt(emoji));
|
||||
};
|
||||
|
||||
const IssueNavbar = observer(() => {
|
||||
const { project: projectStore, user: userStore }: RootStore = useMobxStore();
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspace_slug, project_slug, board } = router.query;
|
||||
|
||||
const user = userStore?.currentUser;
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace_slug && project_slug) {
|
||||
projectStore.fetchProjectSettings(workspace_slug.toString(), project_slug.toString());
|
||||
}
|
||||
}, [projectStore, workspace_slug, project_slug]);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace_slug && projectStore) {
|
||||
if (board) {
|
||||
projectStore.setActiveBoard(board.toString());
|
||||
} else {
|
||||
router.push(`/${workspace_slug}/${project_slug}?board=list`);
|
||||
projectStore.setActiveBoard("list");
|
||||
}
|
||||
}
|
||||
}, [board, router, projectStore, workspace_slug, project_slug]);
|
||||
|
||||
return (
|
||||
<div className="px-5 relative w-full flex items-center gap-4">
|
||||
{/* project detail */}
|
||||
<div className="flex-shrink-0 flex items-center gap-2">
|
||||
<div className="w-4 h-4 flex justify-center items-center">
|
||||
{projectStore?.project && projectStore?.project?.emoji ? (
|
||||
renderEmoji(projectStore?.project?.emoji)
|
||||
) : (
|
||||
<Image src="/plane-logo.webp" alt="plane logo" className="w-[24px] h-[24px]" height="24" width="24" />
|
||||
)}
|
||||
</div>
|
||||
<div className="font-medium text-lg max-w-[300px] line-clamp-1 overflow-hidden">
|
||||
{projectStore?.project?.name || `...`}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* issue search bar */}
|
||||
<div className="w-full">
|
||||
<NavbarSearch />
|
||||
</div>
|
||||
|
||||
{/* issue views */}
|
||||
<div className="flex-shrink-0 relative flex items-center gap-1 transition-all ease-in-out delay-150">
|
||||
<NavbarIssueBoardView />
|
||||
</div>
|
||||
|
||||
{/* theming */}
|
||||
<div className="flex-shrink-0 relative">
|
||||
<NavbarTheme />
|
||||
</div>
|
||||
|
||||
{user ? (
|
||||
<div className="border border-custom-border-200 rounded flex items-center gap-2 p-2">
|
||||
{user.avatar && user.avatar !== "" ? (
|
||||
<div className="h-5 w-5 rounded-full">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img src={user.avatar} alt={user.display_name ?? ""} className="rounded-full" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-custom-background-80 h-5 w-5 rounded-full grid place-items-center text-[10px] capitalize">
|
||||
{(user.display_name ?? "A")[0]}
|
||||
</div>
|
||||
)}
|
||||
<h6 className="text-xs font-medium">{user.display_name}</h6>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-shrink-0">
|
||||
<Link href={`/?next_path=${router.asPath}`}>
|
||||
<a>
|
||||
<PrimaryButton className="flex-shrink-0" outline>
|
||||
Sign in
|
||||
</PrimaryButton>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default IssueNavbar;
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// constants
|
||||
import { issueViews } from "constants/data";
|
||||
// mobx
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { RootStore } from "store/root";
|
||||
|
||||
export const NavbarIssueBoardView = observer(() => {
|
||||
const { project: projectStore, issue: issueStore }: RootStore = useMobxStore();
|
||||
|
||||
const router = useRouter();
|
||||
const { workspace_slug, project_slug } = router.query as { workspace_slug: string; project_slug: string };
|
||||
|
||||
const handleCurrentBoardView = (boardView: string) => {
|
||||
projectStore.setActiveBoard(boardView);
|
||||
router.push(
|
||||
`/${workspace_slug}/${project_slug}?board=${boardView}${
|
||||
issueStore?.filteredLabels && issueStore?.filteredLabels.length > 0
|
||||
? `&labels=${issueStore?.filteredLabels.join(",")}`
|
||||
: ""
|
||||
}${
|
||||
issueStore?.filteredPriorities && issueStore?.filteredPriorities.length > 0
|
||||
? `&priorities=${issueStore?.filteredPriorities.join(",")}`
|
||||
: ""
|
||||
}${
|
||||
issueStore?.filteredStates && issueStore?.filteredStates.length > 0
|
||||
? `&states=${issueStore?.filteredStates.join(",")}`
|
||||
: ""
|
||||
}`
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{projectStore?.viewOptions &&
|
||||
Object.keys(projectStore?.viewOptions).map((viewKey: string) => {
|
||||
if (projectStore?.viewOptions[viewKey]) {
|
||||
return (
|
||||
<div
|
||||
key={viewKey}
|
||||
className={`w-[28px] h-[28px] flex justify-center items-center rounded-sm cursor-pointer ${
|
||||
viewKey === projectStore?.activeBoard
|
||||
? `bg-custom-background-80 text-custom-text-200`
|
||||
: `hover:bg-custom-background-80 text-custom-text-300`
|
||||
}`}
|
||||
onClick={() => handleCurrentBoardView(viewKey)}
|
||||
title={viewKey}
|
||||
>
|
||||
<span
|
||||
className={`material-symbols-rounded text-[18px] ${
|
||||
issueViews[viewKey]?.className ? issueViews[viewKey]?.className : ``
|
||||
}`}
|
||||
>
|
||||
{issueViews[viewKey]?.icon}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// icons
|
||||
import { ChevronDownIcon } from "@heroicons/react/20/solid";
|
||||
// mobx
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { RootStore } from "store/root";
|
||||
// components
|
||||
import { Dropdown } from "components/ui/dropdown";
|
||||
// constants
|
||||
import { issueGroupFilter } from "constants/data";
|
||||
|
||||
const PRIORITIES = ["urgent", "high", "medium", "low"];
|
||||
|
||||
export const NavbarIssueFilter = observer(() => {
|
||||
const store: RootStore = useMobxStore();
|
||||
|
||||
const router = useRouter();
|
||||
const pathName = router.asPath;
|
||||
|
||||
const handleOnSelect = (key: "states" | "labels" | "priorities", value: string) => {
|
||||
// if (key === "states") {
|
||||
// store.issue.userSelectedStates = store.issue.userSelectedStates.includes(value)
|
||||
// ? store.issue.userSelectedStates.filter((s) => s !== value)
|
||||
// : [...store.issue.userSelectedStates, value];
|
||||
// } else if (key === "labels") {
|
||||
// store.issue.userSelectedLabels = store.issue.userSelectedLabels.includes(value)
|
||||
// ? store.issue.userSelectedLabels.filter((l) => l !== value)
|
||||
// : [...store.issue.userSelectedLabels, value];
|
||||
// } else if (key === "priorities") {
|
||||
// store.issue.userSelectedPriorities = store.issue.userSelectedPriorities.includes(value)
|
||||
// ? store.issue.userSelectedPriorities.filter((p) => p !== value)
|
||||
// : [...store.issue.userSelectedPriorities, value];
|
||||
// }
|
||||
// const paramsCommaSeparated = `${`board=${store.issue.currentIssueBoardView || "list"}`}${
|
||||
// store.issue.userSelectedPriorities.length > 0 ? `&priorities=${store.issue.userSelectedPriorities.join(",")}` : ""
|
||||
// }${store.issue.userSelectedStates.length > 0 ? `&states=${store.issue.userSelectedStates.join(",")}` : ""}${
|
||||
// store.issue.userSelectedLabels.length > 0 ? `&labels=${store.issue.userSelectedLabels.join(",")}` : ""
|
||||
// }`;
|
||||
// router.replace(`${pathName}?${paramsCommaSeparated}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
button={
|
||||
<>
|
||||
<span>Filters</span>
|
||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||
</>
|
||||
}
|
||||
items={[
|
||||
{
|
||||
display: "Priority",
|
||||
children: PRIORITIES.map((priority) => ({
|
||||
display: (
|
||||
<span className="capitalize flex items-center gap-x-2">
|
||||
<span className="material-symbols-rounded text-[14px]">
|
||||
{priority === "urgent"
|
||||
? "error"
|
||||
: priority === "high"
|
||||
? "signal_cellular_alt"
|
||||
: priority === "medium"
|
||||
? "signal_cellular_alt_2_bar"
|
||||
: "signal_cellular_alt_1_bar"}
|
||||
</span>
|
||||
{priority}
|
||||
</span>
|
||||
),
|
||||
onClick: () => handleOnSelect("priorities", priority),
|
||||
isSelected: store.issue.filteredPriorities.includes(priority),
|
||||
})),
|
||||
},
|
||||
{
|
||||
display: "State",
|
||||
children: (store.issue.states || []).map((state) => {
|
||||
const stateGroup = issueGroupFilter(state.group);
|
||||
|
||||
return {
|
||||
display: (
|
||||
<span className="capitalize flex items-center gap-x-2">
|
||||
{stateGroup && <stateGroup.icon />}
|
||||
{state.name}
|
||||
</span>
|
||||
),
|
||||
onClick: () => handleOnSelect("states", state.id),
|
||||
isSelected: store.issue.filteredStates.includes(state.id),
|
||||
};
|
||||
}),
|
||||
},
|
||||
{
|
||||
display: "Labels",
|
||||
children: [...(store.issue.labels || [])].map((label) => ({
|
||||
display: (
|
||||
<span className="capitalize flex items-center gap-x-2">
|
||||
<span
|
||||
className="w-3 h-3 rounded-full"
|
||||
style={{
|
||||
backgroundColor: label.color || "#000",
|
||||
}}
|
||||
/>
|
||||
{label.name}
|
||||
</span>
|
||||
),
|
||||
onClick: () => handleOnSelect("labels", label.id),
|
||||
isSelected: store.issue.filteredLabels.includes(label.id),
|
||||
})),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
"use client";
|
||||
|
||||
// mobx react lite
|
||||
import { observer } from "mobx-react-lite";
|
||||
// mobx
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { RootStore } from "store/root";
|
||||
|
||||
export const NavbarIssueView = observer(() => {
|
||||
const store: RootStore = useMobxStore();
|
||||
|
||||
return <div>View</div>;
|
||||
});
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
"use client";
|
||||
|
||||
// mobx react lite
|
||||
import { observer } from "mobx-react-lite";
|
||||
// mobx
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { RootStore } from "store/root";
|
||||
|
||||
export const NavbarSearch = observer(() => {
|
||||
const store: RootStore = useMobxStore();
|
||||
|
||||
return <div> </div>;
|
||||
});
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
// next theme
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
// mobx react lite
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export const NavbarTheme = observer(() => {
|
||||
const [appTheme, setAppTheme] = useState("light");
|
||||
|
||||
const { setTheme, theme } = useTheme();
|
||||
|
||||
const handleTheme = () => {
|
||||
setTheme(theme === "light" ? "dark" : "light");
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!theme) return;
|
||||
|
||||
setAppTheme(theme);
|
||||
}, [theme]);
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleTheme}
|
||||
className="relative w-7 h-7 grid place-items-center bg-custom-background-100 hover:bg-custom-background-80 text-custom-text-100 rounded"
|
||||
>
|
||||
<span className="material-symbols-rounded text-sm">{appTheme === "light" ? "dark_mode" : "light_mode"}</span>
|
||||
</button>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
import React, { useRef } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
// lib
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// ui
|
||||
import { SecondaryButton } from "components/ui";
|
||||
// types
|
||||
import { Comment } from "types/issue";
|
||||
// components
|
||||
import { TipTapEditor } from "components/tiptap";
|
||||
|
||||
const defaultValues: Partial<Comment> = {
|
||||
comment_html: "",
|
||||
};
|
||||
|
||||
type Props = {
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const AddComment: React.FC<Props> = observer((props) => {
|
||||
const { disabled = false } = props;
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
setValue,
|
||||
watch,
|
||||
formState: { isSubmitting },
|
||||
reset,
|
||||
} = useForm<Comment>({ defaultValues });
|
||||
|
||||
const router = useRouter();
|
||||
const { workspace_slug, project_slug } = router.query as { workspace_slug: string; project_slug: string };
|
||||
|
||||
const { user: userStore, issueDetails: issueDetailStore } = useMobxStore();
|
||||
|
||||
const issueId = issueDetailStore.peekId;
|
||||
|
||||
const editorRef = useRef<any>(null);
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const onSubmit = async (formData: Comment) => {
|
||||
if (!workspace_slug || !project_slug || !issueId || isSubmitting || !formData.comment_html) return;
|
||||
|
||||
await issueDetailStore
|
||||
.addIssueComment(workspace_slug, project_slug, issueId, formData)
|
||||
.then(() => {
|
||||
reset(defaultValues);
|
||||
editorRef.current?.clearEditor();
|
||||
})
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Comment could not be posted. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="issue-comments-section">
|
||||
<Controller
|
||||
name="comment_html"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<TipTapEditor
|
||||
workspaceSlug={workspace_slug as string}
|
||||
ref={editorRef}
|
||||
value={
|
||||
!value || value === "" || (typeof value === "object" && Object.keys(value).length === 0)
|
||||
? watch("comment_html")
|
||||
: value
|
||||
}
|
||||
customClassName="p-3 min-h-[50px] shadow-sm"
|
||||
debouncedUpdatesEnabled={false}
|
||||
onChange={(comment_json: Object, comment_html: string) => {
|
||||
onChange(comment_html);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<SecondaryButton
|
||||
onClick={(e) => {
|
||||
userStore.requiredLogin(() => {
|
||||
handleSubmit(onSubmit)(e);
|
||||
});
|
||||
}}
|
||||
type="submit"
|
||||
disabled={isSubmitting || disabled}
|
||||
className="mt-2"
|
||||
>
|
||||
{isSubmitting ? "Adding..." : "Comment"}
|
||||
</SecondaryButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,190 +0,0 @@
|
|||
import React, { useEffect, useState, useRef } from "react";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
// lib
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// icons
|
||||
import { ChatBubbleLeftEllipsisIcon, CheckIcon, XMarkIcon, EllipsisVerticalIcon } from "@heroicons/react/24/outline";
|
||||
// helpers
|
||||
import { timeAgo } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { Comment } from "types/issue";
|
||||
// components
|
||||
import { TipTapEditor } from "components/tiptap";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
comment: Comment;
|
||||
};
|
||||
|
||||
export const CommentCard: React.FC<Props> = observer((props) => {
|
||||
const { comment, workspaceSlug } = props;
|
||||
// store
|
||||
const { user: userStore, issueDetails: issueDetailStore } = useMobxStore();
|
||||
// states
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
const {
|
||||
formState: { isSubmitting },
|
||||
handleSubmit,
|
||||
control,
|
||||
} = useForm<any>({
|
||||
defaultValues: { comment_html: comment.comment_html },
|
||||
});
|
||||
|
||||
const handleDelete = () => {
|
||||
if (!workspaceSlug || !issueDetailStore.peekId) return;
|
||||
issueDetailStore.deleteIssueComment(workspaceSlug, comment.project, issueDetailStore.peekId, comment.id);
|
||||
};
|
||||
|
||||
const handleCommentUpdate = async (formData: Comment) => {
|
||||
if (!workspaceSlug || !issueDetailStore.peekId) return;
|
||||
issueDetailStore.updateIssueComment(workspaceSlug, comment.project, issueDetailStore.peekId, comment.id, formData);
|
||||
setIsEditing(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative flex items-start space-x-3">
|
||||
<div className="relative px-1">
|
||||
{comment.actor_detail.avatar && comment.actor_detail.avatar !== "" ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={comment.actor_detail.avatar}
|
||||
alt={
|
||||
comment.actor_detail.is_bot ? comment.actor_detail.first_name + " Bot" : comment.actor_detail.display_name
|
||||
}
|
||||
height={30}
|
||||
width={30}
|
||||
className="grid h-7 w-7 place-items-center rounded-full border-2 border-custom-border-200"
|
||||
/>
|
||||
) : (
|
||||
<div className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 text-white`}>
|
||||
{comment.actor_detail.is_bot
|
||||
? comment?.actor_detail?.first_name?.charAt(0)
|
||||
: comment?.actor_detail?.display_name?.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<span className="absolute -bottom-0.5 -right-1 rounded-tl bg-custom-background-80 px-0.5 py-px">
|
||||
<ChatBubbleLeftEllipsisIcon className="h-3.5 w-3.5 text-custom-text-200" aria-hidden="true" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div>
|
||||
<div className="text-xs">
|
||||
{comment.actor_detail.is_bot ? comment.actor_detail.first_name + " Bot" : comment.actor_detail.display_name}
|
||||
</div>
|
||||
<p className="mt-0.5 text-xs text-custom-text-200">
|
||||
<>Commented {timeAgo(comment.created_at)}</>
|
||||
</p>
|
||||
</div>
|
||||
<div className="issue-comments-section p-0">
|
||||
<form
|
||||
onSubmit={handleSubmit(handleCommentUpdate)}
|
||||
className={`flex-col gap-2 ${isEditing ? "flex" : "hidden"}`}
|
||||
>
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="comment_html"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TipTapEditor
|
||||
workspaceSlug={workspaceSlug as string}
|
||||
value={value}
|
||||
debouncedUpdatesEnabled={false}
|
||||
customClassName="min-h-[50px] p-3 shadow-sm"
|
||||
onChange={(comment_json: Object, comment_html: string) => {
|
||||
onChange(comment_html);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-1 self-end">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="group rounded border border-green-500 bg-green-500/20 p-2 shadow-md duration-300 hover:bg-green-500"
|
||||
>
|
||||
<CheckIcon className="h-3 w-3 text-green-500 duration-300 group-hover:text-white" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="group rounded border border-red-500 bg-red-500/20 p-2 shadow-md duration-300 hover:bg-red-500"
|
||||
onClick={() => setIsEditing(false)}
|
||||
>
|
||||
<XMarkIcon className="h-3 w-3 text-red-500 duration-300 group-hover:text-white" />
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div className={`${isEditing ? "hidden" : ""}`}>
|
||||
<TipTapEditor
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
value={comment.comment_html}
|
||||
editable={false}
|
||||
customClassName="text-xs border border-custom-border-200 bg-custom-background-100"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{userStore?.currentUser?.id === comment?.actor_detail?.id && (
|
||||
<Menu as="div" className="relative w-min text-left">
|
||||
<Menu.Button
|
||||
type="button"
|
||||
onClick={() => {}}
|
||||
className="relative grid place-items-center rounded p-1 text-custom-text-200 hover:text-custom-text-100 outline-none cursor-pointer hover:bg-custom-background-80"
|
||||
>
|
||||
<EllipsisVerticalIcon className="h-5 w-5 text-custom-text-200 duration-300" />
|
||||
</Menu.Button>
|
||||
|
||||
<Transition
|
||||
as={React.Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute z-10 overflow-y-scroll whitespace-nowrap rounded-md max-h-36 border right-0 origin-top-right mt-1 overflow-auto min-w-[8rem] border-custom-border-300 p-1 text-xs shadow-lg focus:outline-none bg-custom-background-90">
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<div className="py-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setIsEditing(true);
|
||||
}}
|
||||
className={`w-full select-none truncate rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80 ${
|
||||
active ? "bg-custom-background-80" : ""
|
||||
}`}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<div className="py-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDelete}
|
||||
className={`w-full select-none truncate rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80 ${
|
||||
active ? "bg-custom-background-80" : ""
|
||||
}`}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
import { useEffect } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// lib
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import {
|
||||
PeekOverviewHeader,
|
||||
PeekOverviewIssueActivity,
|
||||
PeekOverviewIssueDetails,
|
||||
PeekOverviewIssueProperties,
|
||||
} from "components/issues/peek-overview";
|
||||
// types
|
||||
import { Loader } from "components/ui/loader";
|
||||
import { IIssue } from "types/issue";
|
||||
|
||||
type Props = {
|
||||
handleClose: () => void;
|
||||
issueDetails: IIssue | undefined;
|
||||
};
|
||||
|
||||
export const FullScreenPeekView: React.FC<Props> = observer((props) => {
|
||||
const { handleClose, issueDetails } = props;
|
||||
|
||||
return (
|
||||
<div className="h-full w-full grid grid-cols-10 divide-x divide-custom-border-200 overflow-hidden">
|
||||
<div className="h-full w-full flex flex-col col-span-7 overflow-hidden">
|
||||
<div className="w-full p-5">
|
||||
<PeekOverviewHeader handleClose={handleClose} issueDetails={issueDetails} />
|
||||
</div>
|
||||
{issueDetails ? (
|
||||
<div className="h-full w-full px-6 overflow-y-auto">
|
||||
{/* issue title and description */}
|
||||
<div className="w-full">
|
||||
<PeekOverviewIssueDetails issueDetails={issueDetails} />
|
||||
</div>
|
||||
{/* divider */}
|
||||
<div className="h-[1] w-full border-t border-custom-border-200 my-5" />
|
||||
{/* issue activity/comments */}
|
||||
<div className="w-full pb-5">
|
||||
<PeekOverviewIssueActivity issueDetails={issueDetails} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Loader className="px-6">
|
||||
<Loader.Item height="30px" />
|
||||
<div className="space-y-2 mt-3">
|
||||
<Loader.Item height="20px" width="70%" />
|
||||
<Loader.Item height="20px" width="60%" />
|
||||
<Loader.Item height="20px" width="60%" />
|
||||
</div>
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-span-3 h-full w-full overflow-y-auto">
|
||||
{/* issue properties */}
|
||||
<div className="w-full px-6 py-5">
|
||||
{issueDetails ? (
|
||||
<PeekOverviewIssueProperties issueDetails={issueDetails} />
|
||||
) : (
|
||||
<Loader className="mt-11 space-y-4">
|
||||
<Loader.Item height="30px" />
|
||||
<Loader.Item height="30px" />
|
||||
<Loader.Item height="30px" />
|
||||
<Loader.Item height="30px" />
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,145 +0,0 @@
|
|||
import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// headless ui
|
||||
import { Listbox, Transition } from "@headlessui/react";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// ui
|
||||
import { Icon } from "components/ui";
|
||||
// icons
|
||||
import { East } from "@mui/icons-material";
|
||||
// helpers
|
||||
import { copyTextToClipboard } from "helpers/string.helper";
|
||||
// store
|
||||
import { IPeekMode } from "store/issue_details";
|
||||
import { RootStore } from "store/root";
|
||||
// lib
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// types
|
||||
import { IIssue } from "types/issue";
|
||||
|
||||
type Props = {
|
||||
handleClose: () => void;
|
||||
issueDetails: IIssue | undefined;
|
||||
};
|
||||
|
||||
const peekModes: {
|
||||
key: IPeekMode;
|
||||
icon: string;
|
||||
label: string;
|
||||
}[] = [
|
||||
{ key: "side", icon: "side_navigation", label: "Side Peek" },
|
||||
{
|
||||
key: "modal",
|
||||
icon: "dialogs",
|
||||
label: "Modal Peek",
|
||||
},
|
||||
{
|
||||
key: "full",
|
||||
icon: "nearby",
|
||||
label: "Full Screen Peek",
|
||||
},
|
||||
];
|
||||
|
||||
export const PeekOverviewHeader: React.FC<Props> = (props) => {
|
||||
const { handleClose, issueDetails } = props;
|
||||
|
||||
const { issueDetails: issueDetailStore }: RootStore = useMobxStore();
|
||||
|
||||
const router = useRouter();
|
||||
const { workspace_slug } = router.query;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const handleCopyLink = () => {
|
||||
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||
|
||||
copyTextToClipboard(`${originURL}/${workspace_slug}/projects/${issueDetails?.project}/`).then(() => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Link copied!",
|
||||
message: "Issue link copied to clipboard",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-4">
|
||||
{issueDetailStore.peekMode === "side" && (
|
||||
<button type="button" onClick={handleClose}>
|
||||
<East
|
||||
sx={{
|
||||
fontSize: "14px",
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
<Listbox
|
||||
as="div"
|
||||
value={issueDetailStore.peekMode}
|
||||
onChange={(val) => issueDetailStore.setPeekMode(val)}
|
||||
className="relative flex-shrink-0 text-left"
|
||||
>
|
||||
<Listbox.Button
|
||||
className={`grid place-items-center ${issueDetailStore.peekMode === "full" ? "rotate-45" : ""}`}
|
||||
>
|
||||
<Icon iconName={peekModes.find((m) => m.key === issueDetailStore.peekMode)?.icon ?? ""} />
|
||||
</Listbox.Button>
|
||||
|
||||
<Transition
|
||||
as={React.Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Listbox.Options className="absolute z-10 border border-custom-border-300 mt-1 overflow-y-auto rounded-md bg-custom-background-90 text-xs shadow-lg focus:outline-none left-0 origin-top-left min-w-[8rem] whitespace-nowrap">
|
||||
<div className="space-y-1 p-2">
|
||||
{peekModes.map((mode) => (
|
||||
<Listbox.Option
|
||||
key={mode.key}
|
||||
value={mode.key}
|
||||
className={({ active, selected }) =>
|
||||
`cursor-pointer select-none truncate rounded px-1 py-1.5 ${
|
||||
active || selected ? "bg-custom-background-80" : ""
|
||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||
}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Icon
|
||||
iconName={mode.icon}
|
||||
className={`!text-base flex-shrink-0 -my-1 ${mode.key === "full" ? "rotate-45" : ""}`}
|
||||
/>
|
||||
{mode.label}
|
||||
</div>
|
||||
</div>
|
||||
{selected && <Icon iconName="done" />}
|
||||
</div>
|
||||
)}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</div>
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</Listbox>
|
||||
</div>
|
||||
{(issueDetailStore.peekMode === "side" || issueDetailStore.peekMode === "modal") && (
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
<button type="button" onClick={handleCopyLink} className="-rotate-45 focus:outline-none" tabIndex={1}>
|
||||
<Icon iconName="link" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
export * from "./full-screen-peek-view";
|
||||
export * from "./header";
|
||||
export * from "./issue-activity";
|
||||
export * from "./issue-details";
|
||||
export * from "./issue-properties";
|
||||
export * from "./layout";
|
||||
export * from "./side-peek-view";
|
||||
export * from "./issue-reaction";
|
||||
export * from "./issue-vote-reactions";
|
||||
export * from "./issue-emoji-reactions";
|
||||
export * from "./comment-detail-card";
|
||||
export * from "./add-comment";
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
import React from "react";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// mobx
|
||||
import { observer } from "mobx-react-lite";
|
||||
// lib
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { CommentCard, AddComment } from "components/issues/peek-overview";
|
||||
// ui
|
||||
import { Icon, PrimaryButton } from "components/ui";
|
||||
// types
|
||||
import { IIssue } from "types/issue";
|
||||
|
||||
type Props = {
|
||||
issueDetails: IIssue;
|
||||
};
|
||||
|
||||
export const PeekOverviewIssueActivity: React.FC<Props> = observer((props) => {
|
||||
const router = useRouter();
|
||||
const { workspace_slug } = router.query;
|
||||
|
||||
const { issueDetails: issueDetailStore, project: projectStore, user: userStore } = useMobxStore();
|
||||
|
||||
const comments = issueDetailStore.details[issueDetailStore.peekId || ""]?.comments || [];
|
||||
|
||||
const user = userStore?.currentUser;
|
||||
|
||||
return (
|
||||
<div className="pb-10">
|
||||
<h4 className="font-medium">Activity</h4>
|
||||
{workspace_slug && (
|
||||
<div className="mt-4">
|
||||
<div className="space-y-4">
|
||||
{comments.map((comment: any) => (
|
||||
<CommentCard key={comment.id} comment={comment} workspaceSlug={workspace_slug?.toString()} />
|
||||
))}
|
||||
</div>
|
||||
{user ? (
|
||||
<>
|
||||
{projectStore.deploySettings?.comments && (
|
||||
<div className="mt-4">
|
||||
<AddComment disabled={!userStore.currentUser} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="bg-custom-background-80 px-2 py-2.5 flex items-center justify-between gap-2 border border-custom-border-300 rounded mt-4">
|
||||
<p className="flex gap-2 text-sm text-custom-text-200 break-words overflow-hidden">
|
||||
<Icon iconName="lock" className="!text-sm" />
|
||||
Sign in to add your comment
|
||||
</p>
|
||||
<Link href={`/?next_path=${router.asPath}`}>
|
||||
<a>
|
||||
<PrimaryButton className="flex-shrink-0 !px-7">Sign in</PrimaryButton>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
import { IssueReactions } from "components/issues/peek-overview";
|
||||
import { TipTapEditor } from "components/tiptap";
|
||||
import { useRouter } from "next/router";
|
||||
// types
|
||||
import { IIssue } from "types/issue";
|
||||
|
||||
type Props = {
|
||||
issueDetails: IIssue;
|
||||
};
|
||||
|
||||
export const PeekOverviewIssueDetails: React.FC<Props> = ({ issueDetails }) => {
|
||||
const router = useRouter();
|
||||
const { workspace_slug } = router.query;
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h6 className="font-medium text-custom-text-200">
|
||||
{issueDetails.project_detail.identifier}-{issueDetails.sequence_id}
|
||||
</h6>
|
||||
<h4 className="break-words text-2xl font-semibold">{issueDetails.name}</h4>
|
||||
{issueDetails.description_html !== "" && issueDetails.description_html !== "<p></p>" && (
|
||||
<TipTapEditor
|
||||
workspaceSlug={workspace_slug as string}
|
||||
value={
|
||||
!issueDetails.description_html ||
|
||||
issueDetails.description_html === "" ||
|
||||
(typeof issueDetails.description_html === "object" &&
|
||||
Object.keys(issueDetails.description_html).length === 0)
|
||||
? "<p></p>"
|
||||
: issueDetails.description_html
|
||||
}
|
||||
customClassName="p-3 min-h-[50px] shadow-sm"
|
||||
debouncedUpdatesEnabled={false}
|
||||
editable={false}
|
||||
/>
|
||||
)}
|
||||
<IssueReactions />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// lib
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// helpers
|
||||
import { groupReactions, renderEmoji } from "helpers/emoji.helper";
|
||||
// components
|
||||
import { ReactionSelector, Tooltip } from "components/ui";
|
||||
|
||||
export const IssueEmojiReactions: React.FC = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspace_slug, project_slug } = router.query;
|
||||
// store
|
||||
const { user: userStore, issueDetails: issueDetailsStore } = useMobxStore();
|
||||
|
||||
const user = userStore?.currentUser;
|
||||
const issueId = issueDetailsStore.peekId;
|
||||
const reactions = issueId ? issueDetailsStore.details[issueId]?.reactions || [] : [];
|
||||
const groupedReactions = groupReactions(reactions, "reaction");
|
||||
|
||||
const handleReactionSelectClick = (reactionHex: string) => {
|
||||
if (!workspace_slug || !project_slug || !issueId) return;
|
||||
const userReaction = reactions?.find((r) => r.actor_detail.id === user?.id && r.reaction === reactionHex);
|
||||
if (userReaction) return;
|
||||
issueDetailsStore.addIssueReaction(workspace_slug.toString(), project_slug.toString(), issueId, reactionHex);
|
||||
};
|
||||
|
||||
const handleReactionClick = (reactionHex: string) => {
|
||||
if (!workspace_slug || !project_slug || !issueId) return;
|
||||
issueDetailsStore.removeIssueReaction(workspace_slug.toString(), project_slug.toString(), issueId, reactionHex);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (user) return;
|
||||
userStore.fetchCurrentUser();
|
||||
}, [user, userStore]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ReactionSelector
|
||||
onSelect={(value) => {
|
||||
userStore.requiredLogin(() => {
|
||||
handleReactionSelectClick(value);
|
||||
});
|
||||
}}
|
||||
size="md"
|
||||
/>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{Object.keys(groupedReactions || {}).map((reaction) => {
|
||||
const reactions = groupedReactions?.[reaction] ?? [];
|
||||
const REACTIONS_LIMIT = 1000;
|
||||
|
||||
if (reactions.length > 0)
|
||||
return (
|
||||
<Tooltip
|
||||
key={reaction}
|
||||
tooltipContent={
|
||||
<div>
|
||||
{reactions
|
||||
.map((r) => r.actor_detail.display_name)
|
||||
.splice(0, REACTIONS_LIMIT)
|
||||
.join(", ")}
|
||||
{reactions.length > REACTIONS_LIMIT && " and " + (reactions.length - REACTIONS_LIMIT) + " more"}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
userStore.requiredLogin(() => {
|
||||
handleReactionClick(reaction);
|
||||
});
|
||||
}}
|
||||
className={`flex items-center gap-1 text-custom-text-100 text-sm h-full px-2 py-1 rounded-md ${
|
||||
reactions?.some((r) => r.actor_detail.id === user?.id && r.reaction === reaction)
|
||||
? "bg-custom-primary-100/10"
|
||||
: "bg-custom-background-80"
|
||||
}`}
|
||||
>
|
||||
<span>{renderEmoji(reaction)}</span>
|
||||
<span
|
||||
className={
|
||||
reactions?.some((r) => r.actor_detail.id === user?.id && r.reaction === reaction)
|
||||
? "text-custom-primary-100"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{groupedReactions?.[reaction].length}{" "}
|
||||
</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,155 +0,0 @@
|
|||
// headless ui
|
||||
import { Disclosure } from "@headlessui/react";
|
||||
// import { getStateGroupIcon } from "components/icons";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// icons
|
||||
import { Icon } from "components/ui";
|
||||
import { copyTextToClipboard, addSpaceIfCamelCase } from "helpers/string.helper";
|
||||
// types
|
||||
import { IIssue } from "types/issue";
|
||||
// constants
|
||||
import { issueGroupFilter, issuePriorityFilter } from "constants/data";
|
||||
import { useEffect } from "react";
|
||||
import { renderDateFormat } from "constants/helpers";
|
||||
import { IPeekMode } from "store/issue_details";
|
||||
import { useRouter } from "next/router";
|
||||
import { RootStore } from "store/root";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
|
||||
type Props = {
|
||||
issueDetails: IIssue;
|
||||
mode?: IPeekMode;
|
||||
};
|
||||
|
||||
const validDate = (date: any, state: string): string => {
|
||||
if (date === null || ["backlog", "unstarted", "cancelled"].includes(state))
|
||||
return `bg-gray-500/10 text-gray-500 border-gray-500/50`;
|
||||
else {
|
||||
const today = new Date();
|
||||
const dueDate = new Date(date);
|
||||
|
||||
if (dueDate < today) return `bg-red-500/10 text-red-500 border-red-500/50`;
|
||||
else return `bg-green-500/10 text-green-500 border-green-500/50`;
|
||||
}
|
||||
};
|
||||
|
||||
export const PeekOverviewIssueProperties: React.FC<Props> = ({ issueDetails, mode }) => {
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { issueDetails: issueDetailStore }: RootStore = useMobxStore();
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const startDate = issueDetails.start_date;
|
||||
const targetDate = issueDetails.target_date;
|
||||
|
||||
const minDate = startDate ? new Date(startDate) : null;
|
||||
minDate?.setDate(minDate.getDate());
|
||||
|
||||
const maxDate = targetDate ? new Date(targetDate) : null;
|
||||
maxDate?.setDate(maxDate.getDate());
|
||||
|
||||
const state = issueDetails.state_detail;
|
||||
const stateGroup = issueGroupFilter(state.group);
|
||||
|
||||
const priority = issueDetails.priority ? issuePriorityFilter(issueDetails.priority) : null;
|
||||
|
||||
const handleCopyLink = () => {
|
||||
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||
|
||||
copyTextToClipboard(
|
||||
`${originURL}/${workspaceSlug}/projects/${issueDetails.project}/issues/${issueDetails.id}`
|
||||
).then(() => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Link copied!",
|
||||
message: "Issue link copied to clipboard",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={mode === "full" ? "divide-y divide-custom-border-200" : ""}>
|
||||
{mode === "full" && (
|
||||
<div className="flex justify-between gap-2 pb-3">
|
||||
<h6 className="flex items-center gap-2 font-medium">
|
||||
{/* {getStateGroupIcon(issue.state_detail.group, "16", "16", issue.state_detail.color)} */}
|
||||
{issueDetails.project_detail.identifier}-{issueDetails.sequence_id}
|
||||
</h6>
|
||||
<div className="flex items-center gap-2">
|
||||
<button type="button" onClick={handleCopyLink} className="-rotate-45">
|
||||
<Icon iconName="link" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={`space-y-4 ${mode === "full" ? "pt-3" : ""}`}>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<div className="flex-shrink-0 w-1/4 flex items-center gap-2 font-medium">
|
||||
<Icon iconName="radio_button_checked" className="!text-base flex-shrink-0" />
|
||||
<span className="flex-grow truncate">State</span>
|
||||
</div>
|
||||
<div className="w-3/4">
|
||||
{stateGroup && (
|
||||
<div className="inline-flex bg-custom-background-80 text-sm rounded px-2.5 py-0.5">
|
||||
<div className="flex items-center gap-1.5 text-left text-custom-text-100">
|
||||
<stateGroup.icon />
|
||||
{addSpaceIfCamelCase(state?.name ?? "")}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<div className="flex-shrink-0 w-1/4 flex items-center gap-2 font-medium">
|
||||
<Icon iconName="signal_cellular_alt" className="!text-base flex-shrink-0" />
|
||||
<span className="flex-grow truncate">Priority</span>
|
||||
</div>
|
||||
<div className="w-3/4">
|
||||
<div
|
||||
className={`inline-flex items-center gap-1.5 text-left text-sm capitalize rounded px-2.5 py-0.5 ${
|
||||
priority?.key === "urgent"
|
||||
? "border-red-500/20 bg-red-500/20 text-red-500"
|
||||
: priority?.key === "high"
|
||||
? "border-orange-500/20 bg-orange-500/20 text-orange-500"
|
||||
: priority?.key === "medium"
|
||||
? "border-yellow-500/20 bg-yellow-500/20 text-yellow-500"
|
||||
: priority?.key === "low"
|
||||
? "border-green-500/20 bg-green-500/20 text-green-500"
|
||||
: "bg-custom-background-80 border-custom-border-200"
|
||||
}`}
|
||||
>
|
||||
{priority && (
|
||||
<span className="grid place-items-center -my-1">
|
||||
<Icon iconName={priority?.icon!} />
|
||||
</span>
|
||||
)}
|
||||
<span>{priority?.title ?? "None"}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<div className="flex-shrink-0 w-1/4 flex items-center gap-2 font-medium">
|
||||
<Icon iconName="calendar_today" className="!text-base flex-shrink-0" />
|
||||
<span className="flex-grow truncate">Due date</span>
|
||||
</div>
|
||||
<div>
|
||||
{issueDetails.target_date ? (
|
||||
<div
|
||||
className={`h-[24px] rounded-md flex px-2.5 py-1 items-center border border-custom-border-100 gap-1 text-custom-text-100 text-xs font-medium
|
||||
${validDate(issueDetails.target_date, state)}`}
|
||||
>
|
||||
{renderDateFormat(issueDetails.target_date)}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-custom-text-200">Empty</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
import { IssueEmojiReactions, IssueVotes } from "components/issues/peek-overview";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
|
||||
export const IssueReactions: React.FC = () => {
|
||||
const { project: projectStore } = useMobxStore();
|
||||
|
||||
return (
|
||||
<div className="flex gap-3 items-center mt-4">
|
||||
{projectStore?.deploySettings?.votes && (
|
||||
<>
|
||||
<div className="flex gap-2 items-center">
|
||||
<IssueVotes />
|
||||
</div>
|
||||
<div className="w-0.5 h-8 bg-custom-background-200" />
|
||||
</>
|
||||
)}
|
||||
{projectStore?.deploySettings?.reactions && (
|
||||
<div className="flex gap-2 items-center">
|
||||
<IssueEmojiReactions />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,129 +0,0 @@
|
|||
import { useState, useEffect } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// mobx
|
||||
import { observer } from "mobx-react-lite";
|
||||
// lib
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { Tooltip } from "components/ui";
|
||||
|
||||
export const IssueVotes: React.FC = observer(() => {
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const { workspace_slug, project_slug } = router.query;
|
||||
|
||||
const { user: userStore, issueDetails: issueDetailsStore } = useMobxStore();
|
||||
|
||||
const user = userStore?.currentUser;
|
||||
const issueId = issueDetailsStore.peekId;
|
||||
|
||||
const votes = issueId ? issueDetailsStore.details[issueId]?.votes : [];
|
||||
|
||||
const allUpVotes = votes?.filter((vote) => vote.vote === 1);
|
||||
const allDownVotes = votes?.filter((vote) => vote.vote === -1);
|
||||
|
||||
const isUpVotedByUser = allUpVotes?.some((vote) => vote.actor === user?.id);
|
||||
const isDownVotedByUser = allDownVotes?.some((vote) => vote.actor === user?.id);
|
||||
|
||||
const handleVote = async (e: any, voteValue: 1 | -1) => {
|
||||
if (!workspace_slug || !project_slug || !issueId) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
|
||||
const actionPerformed = votes?.find((vote) => vote.actor === user?.id && vote.vote === voteValue);
|
||||
|
||||
if (actionPerformed)
|
||||
await issueDetailsStore.removeIssueVote(workspace_slug.toString(), project_slug.toString(), issueId);
|
||||
else
|
||||
await issueDetailsStore.addIssueVote(workspace_slug.toString(), project_slug.toString(), issueId, {
|
||||
vote: voteValue,
|
||||
});
|
||||
|
||||
setIsSubmitting(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (user) return;
|
||||
|
||||
userStore.fetchCurrentUser();
|
||||
}, [user, userStore]);
|
||||
|
||||
const VOTES_LIMIT = 1000;
|
||||
|
||||
return (
|
||||
<div className="flex gap-2 items-center">
|
||||
{/* upvote button 👇 */}
|
||||
<Tooltip
|
||||
tooltipContent={
|
||||
<div>
|
||||
{allUpVotes.length > 0 ? (
|
||||
<>
|
||||
{allUpVotes
|
||||
.map((r) => r.actor_detail.display_name)
|
||||
.splice(0, VOTES_LIMIT)
|
||||
.join(", ")}
|
||||
{allUpVotes.length > VOTES_LIMIT && " and " + (allUpVotes.length - VOTES_LIMIT) + " more"}
|
||||
</>
|
||||
) : (
|
||||
"No upvotes yet"
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
disabled={isSubmitting}
|
||||
onClick={(e) => {
|
||||
userStore.requiredLogin(() => {
|
||||
handleVote(e, 1);
|
||||
});
|
||||
}}
|
||||
className={`flex items-center justify-center overflow-hidden px-2 gap-x-1 border rounded focus:outline-none ${
|
||||
isUpVotedByUser ? "border-custom-primary-200 text-custom-primary-200" : "border-custom-border-300"
|
||||
}`}
|
||||
>
|
||||
<span className="material-symbols-rounded text-base !p-0 !m-0">arrow_upward_alt</span>
|
||||
<span className="text-sm font-normal transition-opacity ease-in-out">{allUpVotes.length}</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
{/* downvote button 👇 */}
|
||||
<Tooltip
|
||||
tooltipContent={
|
||||
<div>
|
||||
{allDownVotes.length > 0 ? (
|
||||
<>
|
||||
{allDownVotes
|
||||
.map((r) => r.actor_detail.display_name)
|
||||
.splice(0, VOTES_LIMIT)
|
||||
.join(", ")}
|
||||
{allDownVotes.length > VOTES_LIMIT && " and " + (allDownVotes.length - VOTES_LIMIT) + " more"}
|
||||
</>
|
||||
) : (
|
||||
"No downvotes yet"
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
disabled={isSubmitting}
|
||||
onClick={(e) => {
|
||||
userStore.requiredLogin(() => {
|
||||
handleVote(e, -1);
|
||||
});
|
||||
}}
|
||||
className={`flex items-center justify-center overflow-hidden px-2 gap-x-1 border rounded focus:outline-none ${
|
||||
isDownVotedByUser ? "border-red-600 text-red-600" : "border-custom-border-300"
|
||||
}`}
|
||||
>
|
||||
<span className="material-symbols-rounded text-base !p-0 !m-0">arrow_downward_alt</span>
|
||||
<span className="text-sm font-normal transition-opacity ease-in-out">{allDownVotes.length}</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,129 +0,0 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// mobx
|
||||
import { observer } from "mobx-react-lite";
|
||||
// headless ui
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// components
|
||||
import { FullScreenPeekView, SidePeekView } from "components/issues/peek-overview";
|
||||
// lib
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
|
||||
type Props = {};
|
||||
|
||||
export const IssuePeekOverview: React.FC<Props> = observer((props) => {
|
||||
const [isSidePeekOpen, setIsSidePeekOpen] = useState(false);
|
||||
const [isModalPeekOpen, setIsModalPeekOpen] = useState(false);
|
||||
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspace_slug, project_slug, peekId, board } = router.query;
|
||||
// store
|
||||
const { issueDetails: issueDetailStore, issue: issueStore } = useMobxStore();
|
||||
|
||||
const issueDetails = issueDetailStore.peekId && peekId ? issueDetailStore.details[peekId.toString()] : undefined;
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace_slug && project_slug && peekId && issueStore.issues && issueStore.issues.length > 0) {
|
||||
if (!issueDetails) {
|
||||
issueDetailStore.fetchIssueDetails(workspace_slug.toString(), project_slug.toString(), peekId.toString());
|
||||
}
|
||||
}
|
||||
}, [workspace_slug, project_slug, issueDetailStore, issueDetails, peekId, issueStore.issues]);
|
||||
|
||||
const handleClose = () => {
|
||||
issueDetailStore.setPeekId(null);
|
||||
router.replace(
|
||||
{
|
||||
pathname: `/${workspace_slug?.toString()}/${project_slug}`,
|
||||
query: {
|
||||
board,
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
{ shallow: true }
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (peekId) {
|
||||
if (issueDetailStore.peekMode === "side") {
|
||||
setIsSidePeekOpen(true);
|
||||
setIsModalPeekOpen(false);
|
||||
} else {
|
||||
setIsModalPeekOpen(true);
|
||||
setIsSidePeekOpen(false);
|
||||
}
|
||||
} else {
|
||||
setIsSidePeekOpen(false);
|
||||
setIsModalPeekOpen(false);
|
||||
}
|
||||
}, [peekId, issueDetailStore.peekMode]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Transition.Root appear show={isSidePeekOpen} as={React.Fragment}>
|
||||
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
||||
<div className="fixed inset-0 z-20 h-full w-full overflow-y-auto">
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="transition-transform duration-300"
|
||||
enterFrom="translate-x-full"
|
||||
enterTo="translate-x-0"
|
||||
leave="transition-transform duration-200"
|
||||
leaveFrom="translate-x-0"
|
||||
leaveTo="translate-x-full"
|
||||
>
|
||||
<Dialog.Panel className="fixed z-20 bg-custom-background-100 top-0 right-0 h-full w-1/2 shadow-custom-shadow-sm">
|
||||
<SidePeekView handleClose={handleClose} issueDetails={issueDetails} />
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
<Transition.Root appear show={isModalPeekOpen} as={React.Fragment}>
|
||||
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
|
||||
</Transition.Child>
|
||||
<div className="fixed inset-0 z-20 h-full w-full overflow-y-auto">
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Dialog.Panel>
|
||||
<div
|
||||
className={`fixed z-20 bg-custom-background-100 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded-lg shadow-custom-shadow-xl transition-all duration-300 ${
|
||||
issueDetailStore.peekMode === "modal" ? "h-[70%] w-3/5" : "h-[95%] w-[95%]"
|
||||
}`}
|
||||
>
|
||||
{issueDetailStore.peekMode === "modal" && (
|
||||
<SidePeekView handleClose={handleClose} issueDetails={issueDetails} />
|
||||
)}
|
||||
{issueDetailStore.peekMode === "full" && (
|
||||
<FullScreenPeekView handleClose={handleClose} issueDetails={issueDetails} />
|
||||
)}
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
import { observer } from "mobx-react-lite";
|
||||
// components
|
||||
import {
|
||||
PeekOverviewHeader,
|
||||
PeekOverviewIssueActivity,
|
||||
PeekOverviewIssueDetails,
|
||||
PeekOverviewIssueProperties,
|
||||
} from "components/issues/peek-overview";
|
||||
|
||||
import { Loader } from "components/ui/loader";
|
||||
import { IIssue } from "types/issue";
|
||||
|
||||
type Props = {
|
||||
handleClose: () => void;
|
||||
issueDetails: IIssue | undefined;
|
||||
};
|
||||
|
||||
export const SidePeekView: React.FC<Props> = observer((props) => {
|
||||
const { handleClose, issueDetails } = props;
|
||||
|
||||
return (
|
||||
<div className="h-full w-full flex flex-col overflow-hidden">
|
||||
<div className="w-full p-5">
|
||||
<PeekOverviewHeader handleClose={handleClose} issueDetails={issueDetails} />
|
||||
</div>
|
||||
{issueDetails ? (
|
||||
<div className="h-full w-full px-6 overflow-y-auto">
|
||||
{/* issue title and description */}
|
||||
<div className="w-full">
|
||||
<PeekOverviewIssueDetails issueDetails={issueDetails} />
|
||||
</div>
|
||||
{/* issue properties */}
|
||||
<div className="w-full mt-6">
|
||||
<PeekOverviewIssueProperties issueDetails={issueDetails} />
|
||||
</div>
|
||||
{/* divider */}
|
||||
<div className="h-[1] w-full border-t border-custom-border-200 my-5" />
|
||||
{/* issue activity/comments */}
|
||||
<div className="w-full pb-5">
|
||||
<PeekOverviewIssueActivity issueDetails={issueDetails} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Loader className="px-6">
|
||||
<Loader.Item height="30px" />
|
||||
<div className="space-y-2 mt-3">
|
||||
<Loader.Item height="20px" width="70%" />
|
||||
<Loader.Item height="20px" width="60%" />
|
||||
<Loader.Item height="20px" width="60%" />
|
||||
</div>
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue