mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
feat: cycles and modules toggle in settings, refactor: folder structure (#247)
* feat: link option in remirror * fix: removed link import from remirror toolbar * refactor: constants folder * refactor: layouts folder structure * fix: issue view context * feat: cycles and modules toggle in settings
This commit is contained in:
parent
4e27e93739
commit
76cc634a46
@ -39,48 +39,38 @@ export const AllBoards: React.FC<Props> = ({
|
|||||||
<div className="h-[calc(100vh-157px)] lg:h-[calc(100vh-115px)] w-full">
|
<div className="h-[calc(100vh-157px)] lg:h-[calc(100vh-115px)] w-full">
|
||||||
<DragDropContext onDragEnd={handleOnDragEnd}>
|
<DragDropContext onDragEnd={handleOnDragEnd}>
|
||||||
<div className="h-full w-full overflow-hidden">
|
<div className="h-full w-full overflow-hidden">
|
||||||
<StrictModeDroppable droppableId="state" type="state" direction="horizontal">
|
<div className="h-full w-full">
|
||||||
{(provided) => (
|
<div className="flex h-full gap-x-4 overflow-x-auto overflow-y-hidden">
|
||||||
<div
|
{Object.keys(groupedByIssues).map((singleGroup, index) => {
|
||||||
className="h-full w-full"
|
const stateId =
|
||||||
{...provided.droppableProps}
|
selectedGroup === "state_detail.name"
|
||||||
ref={provided.innerRef}
|
? states?.find((s) => s.name === singleGroup)?.id ?? null
|
||||||
>
|
: null;
|
||||||
<div className="flex h-full gap-x-4 overflow-x-auto overflow-y-hidden">
|
|
||||||
{Object.keys(groupedByIssues).map((singleGroup, index) => {
|
|
||||||
const stateId =
|
|
||||||
selectedGroup === "state_detail.name"
|
|
||||||
? states?.find((s) => s.name === singleGroup)?.id ?? null
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const bgColor =
|
const bgColor =
|
||||||
selectedGroup === "state_detail.name"
|
selectedGroup === "state_detail.name"
|
||||||
? states?.find((s) => s.name === singleGroup)?.color
|
? states?.find((s) => s.name === singleGroup)?.color
|
||||||
: "#000000";
|
: "#000000";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SingleBoard
|
<SingleBoard
|
||||||
key={index}
|
key={index}
|
||||||
index={index}
|
type={type}
|
||||||
type={type}
|
bgColor={bgColor}
|
||||||
bgColor={bgColor}
|
groupTitle={singleGroup}
|
||||||
groupTitle={singleGroup}
|
groupedByIssues={groupedByIssues}
|
||||||
groupedByIssues={groupedByIssues}
|
selectedGroup={selectedGroup}
|
||||||
selectedGroup={selectedGroup}
|
members={members}
|
||||||
members={members}
|
addIssueToState={() => addIssueToState(singleGroup, stateId)}
|
||||||
addIssueToState={() => addIssueToState(singleGroup, stateId)}
|
handleDeleteIssue={handleDeleteIssue}
|
||||||
handleDeleteIssue={handleDeleteIssue}
|
openIssuesListModal={openIssuesListModal ?? null}
|
||||||
openIssuesListModal={openIssuesListModal ?? null}
|
orderBy={orderBy}
|
||||||
orderBy={orderBy}
|
userAuth={userAuth}
|
||||||
userAuth={userAuth}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
})}
|
||||||
})}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{provided.placeholder}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</StrictModeDroppable>
|
|
||||||
</div>
|
</div>
|
||||||
</DragDropContext>
|
</DragDropContext>
|
||||||
</div>
|
</div>
|
||||||
|
@ -12,15 +12,13 @@ import {
|
|||||||
// helpers
|
// helpers
|
||||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import { IIssue, NestedKeyOf } from "types";
|
import { IIssue } from "types";
|
||||||
type Props = {
|
type Props = {
|
||||||
provided: DraggableProvided;
|
|
||||||
isCollapsed: boolean;
|
isCollapsed: boolean;
|
||||||
setIsCollapsed: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsCollapsed: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
groupedByIssues: {
|
groupedByIssues: {
|
||||||
[key: string]: IIssue[];
|
[key: string]: IIssue[];
|
||||||
};
|
};
|
||||||
selectedGroup: NestedKeyOf<IIssue> | null;
|
|
||||||
groupTitle: string;
|
groupTitle: string;
|
||||||
createdBy: string | null;
|
createdBy: string | null;
|
||||||
bgColor?: string;
|
bgColor?: string;
|
||||||
@ -30,9 +28,7 @@ type Props = {
|
|||||||
export const BoardHeader: React.FC<Props> = ({
|
export const BoardHeader: React.FC<Props> = ({
|
||||||
isCollapsed,
|
isCollapsed,
|
||||||
setIsCollapsed,
|
setIsCollapsed,
|
||||||
provided,
|
|
||||||
groupedByIssues,
|
groupedByIssues,
|
||||||
selectedGroup,
|
|
||||||
groupTitle,
|
groupTitle,
|
||||||
createdBy,
|
createdBy,
|
||||||
bgColor,
|
bgColor,
|
||||||
@ -44,16 +40,6 @@ export const BoardHeader: React.FC<Props> = ({
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className={`flex items-center ${!isCollapsed ? "flex-col gap-2" : "gap-1"}`}>
|
<div className={`flex items-center ${!isCollapsed ? "flex-col gap-2" : "gap-1"}`}>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
{...provided.dragHandleProps}
|
|
||||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-200 ${
|
|
||||||
!isCollapsed ? "" : "rotate-90"
|
|
||||||
} ${selectedGroup !== "state_detail.name" ? "hidden" : ""}`}
|
|
||||||
>
|
|
||||||
<EllipsisHorizontalIcon className="h-4 w-4 text-gray-600" />
|
|
||||||
<EllipsisHorizontalIcon className="mt-[-0.7rem] h-4 w-4 text-gray-600" />
|
|
||||||
</button>
|
|
||||||
<div
|
<div
|
||||||
className={`flex cursor-pointer items-center gap-x-1 rounded-md bg-slate-900 px-2 ${
|
className={`flex cursor-pointer items-center gap-x-1 rounded-md bg-slate-900 px-2 ${
|
||||||
!isCollapsed ? "mb-2 flex-col gap-y-2 py-2" : ""
|
!isCollapsed ? "mb-2 flex-col gap-y-2 py-2" : ""
|
||||||
|
@ -17,7 +17,6 @@ import { PlusIcon } from "@heroicons/react/24/outline";
|
|||||||
import { IIssue, IProjectMember, NestedKeyOf, UserAuth } from "types";
|
import { IIssue, IProjectMember, NestedKeyOf, UserAuth } from "types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
index: number;
|
|
||||||
type?: "issue" | "cycle" | "module";
|
type?: "issue" | "cycle" | "module";
|
||||||
bgColor?: string;
|
bgColor?: string;
|
||||||
groupTitle: string;
|
groupTitle: string;
|
||||||
@ -34,7 +33,6 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const SingleBoard: React.FC<Props> = ({
|
export const SingleBoard: React.FC<Props> = ({
|
||||||
index,
|
|
||||||
type,
|
type,
|
||||||
bgColor,
|
bgColor,
|
||||||
groupTitle,
|
groupTitle,
|
||||||
@ -70,95 +68,79 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
: (bgColor = "#ff0000");
|
: (bgColor = "#ff0000");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Draggable draggableId={groupTitle} index={index}>
|
<div className={`h-full flex-shrink-0 rounded ${!isCollapsed ? "" : "w-80 border bg-gray-50"}`}>
|
||||||
{(provided, snapshot) => (
|
<div className={`${!isCollapsed ? "" : "flex h-full flex-col space-y-3 overflow-y-auto"}`}>
|
||||||
<div
|
<BoardHeader
|
||||||
className={`h-full flex-shrink-0 rounded ${
|
addIssueToState={addIssueToState}
|
||||||
snapshot && snapshot.isDragging ? "border-theme shadow-lg" : ""
|
bgColor={bgColor}
|
||||||
} ${!isCollapsed ? "" : "w-80 border bg-gray-50"}`}
|
createdBy={createdBy}
|
||||||
ref={provided?.innerRef}
|
groupTitle={groupTitle}
|
||||||
{...provided?.draggableProps}
|
groupedByIssues={groupedByIssues}
|
||||||
>
|
isCollapsed={isCollapsed}
|
||||||
<div
|
setIsCollapsed={setIsCollapsed}
|
||||||
className={`${!isCollapsed ? "" : "flex h-full flex-col space-y-3 overflow-y-auto"}`}
|
/>
|
||||||
>
|
<StrictModeDroppable key={groupTitle} droppableId={groupTitle}>
|
||||||
<BoardHeader
|
{(provided, snapshot) => (
|
||||||
provided={provided}
|
<div
|
||||||
addIssueToState={addIssueToState}
|
className={`relative mt-3 h-full space-y-3 px-3 pb-3 ${
|
||||||
bgColor={bgColor}
|
snapshot.isDraggingOver ? "bg-indigo-50 bg-opacity-50" : ""
|
||||||
createdBy={createdBy}
|
} ${!isCollapsed ? "hidden" : "block"}`}
|
||||||
groupTitle={groupTitle}
|
ref={provided.innerRef}
|
||||||
groupedByIssues={groupedByIssues}
|
{...provided.droppableProps}
|
||||||
isCollapsed={isCollapsed}
|
>
|
||||||
setIsCollapsed={setIsCollapsed}
|
{groupedByIssues[groupTitle].map((issue, index: number) => (
|
||||||
selectedGroup={selectedGroup}
|
<SingleBoardIssue
|
||||||
/>
|
key={index}
|
||||||
<StrictModeDroppable key={groupTitle} droppableId={groupTitle}>
|
index={index}
|
||||||
{(provided, snapshot) => (
|
type={type}
|
||||||
<div
|
issue={issue}
|
||||||
className={`relative mt-3 h-full space-y-3 overflow-y-auto px-3 pb-3 ${
|
selectedGroup={selectedGroup}
|
||||||
snapshot.isDraggingOver ? "bg-indigo-50 bg-opacity-50" : ""
|
properties={properties}
|
||||||
} ${!isCollapsed ? "hidden" : "block"}`}
|
handleDeleteIssue={handleDeleteIssue}
|
||||||
ref={provided.innerRef}
|
orderBy={orderBy}
|
||||||
{...provided.droppableProps}
|
userAuth={userAuth}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: orderBy === "manual" ? "inline" : "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{provided.placeholder}
|
||||||
|
</span>
|
||||||
|
{type === "issue" ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex items-center rounded p-2 text-xs font-medium outline-none duration-300 hover:bg-gray-100"
|
||||||
|
onClick={addIssueToState}
|
||||||
>
|
>
|
||||||
{groupedByIssues[groupTitle].map((issue, index: number) => (
|
<PlusIcon className="mr-1 h-3 w-3" />
|
||||||
<SingleBoardIssue
|
Create
|
||||||
key={index}
|
</button>
|
||||||
index={index}
|
) : (
|
||||||
type={type}
|
<CustomMenu
|
||||||
issue={issue}
|
label={
|
||||||
properties={properties}
|
<span className="flex items-center gap-1">
|
||||||
members={members}
|
<PlusIcon className="h-3 w-3" />
|
||||||
handleDeleteIssue={handleDeleteIssue}
|
Add issue
|
||||||
orderBy={orderBy}
|
</span>
|
||||||
userAuth={userAuth}
|
}
|
||||||
/>
|
className="mt-1"
|
||||||
))}
|
optionsPosition="left"
|
||||||
<span
|
noBorder
|
||||||
style={{
|
>
|
||||||
display: orderBy === "manual" ? "inline" : "none",
|
<CustomMenu.MenuItem onClick={addIssueToState}>Create new</CustomMenu.MenuItem>
|
||||||
}}
|
{openIssuesListModal && (
|
||||||
>
|
<CustomMenu.MenuItem onClick={openIssuesListModal}>
|
||||||
{provided.placeholder}
|
Add an existing issue
|
||||||
</span>
|
</CustomMenu.MenuItem>
|
||||||
{type === "issue" ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="flex items-center rounded p-2 text-xs font-medium outline-none duration-300 hover:bg-gray-100"
|
|
||||||
onClick={addIssueToState}
|
|
||||||
>
|
|
||||||
<PlusIcon className="mr-1 h-3 w-3" />
|
|
||||||
Create
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<CustomMenu
|
|
||||||
label={
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<PlusIcon className="h-3 w-3" />
|
|
||||||
Add issue
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
className="mt-1"
|
|
||||||
optionsPosition="left"
|
|
||||||
noBorder
|
|
||||||
>
|
|
||||||
<CustomMenu.MenuItem onClick={addIssueToState}>
|
|
||||||
Create new
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
{openIssuesListModal && (
|
|
||||||
<CustomMenu.MenuItem onClick={openIssuesListModal}>
|
|
||||||
Add an existing issue
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
)}
|
|
||||||
</CustomMenu>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</CustomMenu>
|
||||||
)}
|
)}
|
||||||
</StrictModeDroppable>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</StrictModeDroppable>
|
||||||
)}
|
</div>
|
||||||
</Draggable>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -16,14 +16,17 @@ import {
|
|||||||
import { TrashIcon } from "@heroicons/react/24/outline";
|
import { TrashIcon } from "@heroicons/react/24/outline";
|
||||||
// services
|
// services
|
||||||
import issuesService from "services/issues.service";
|
import issuesService from "services/issues.service";
|
||||||
import stateService from "services/state.service";
|
|
||||||
// components
|
// components
|
||||||
import { AssigneeSelect, DueDateSelect, PrioritySelect, StateSelect } from "components/core/select";
|
import {
|
||||||
|
ViewAssigneeSelect,
|
||||||
|
ViewDueDateSelect,
|
||||||
|
ViewPrioritySelect,
|
||||||
|
ViewStateSelect,
|
||||||
|
} from "components/issues/view-select";
|
||||||
// types
|
// types
|
||||||
import {
|
import {
|
||||||
CycleIssueResponse,
|
CycleIssueResponse,
|
||||||
IIssue,
|
IIssue,
|
||||||
IProjectMember,
|
|
||||||
IssueResponse,
|
IssueResponse,
|
||||||
ModuleIssueResponse,
|
ModuleIssueResponse,
|
||||||
NestedKeyOf,
|
NestedKeyOf,
|
||||||
@ -31,14 +34,14 @@ import {
|
|||||||
UserAuth,
|
UserAuth,
|
||||||
} from "types";
|
} from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { STATE_LIST, CYCLE_ISSUES, MODULE_ISSUES, PROJECT_ISSUES_LIST } from "constants/fetch-keys";
|
import { CYCLE_ISSUES, MODULE_ISSUES, PROJECT_ISSUES_LIST } from "constants/fetch-keys";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
index: number;
|
index: number;
|
||||||
type?: string;
|
type?: string;
|
||||||
issue: IIssue;
|
issue: IIssue;
|
||||||
|
selectedGroup: NestedKeyOf<IIssue> | null;
|
||||||
properties: Properties;
|
properties: Properties;
|
||||||
members: IProjectMember[] | undefined;
|
|
||||||
handleDeleteIssue: (issue: IIssue) => void;
|
handleDeleteIssue: (issue: IIssue) => void;
|
||||||
orderBy: NestedKeyOf<IIssue> | "manual" | null;
|
orderBy: NestedKeyOf<IIssue> | "manual" | null;
|
||||||
userAuth: UserAuth;
|
userAuth: UserAuth;
|
||||||
@ -48,8 +51,8 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
index,
|
index,
|
||||||
type,
|
type,
|
||||||
issue,
|
issue,
|
||||||
|
selectedGroup,
|
||||||
properties,
|
properties,
|
||||||
members,
|
|
||||||
handleDeleteIssue,
|
handleDeleteIssue,
|
||||||
orderBy,
|
orderBy,
|
||||||
userAuth,
|
userAuth,
|
||||||
@ -57,13 +60,6 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||||
|
|
||||||
const { data: states } = useSWR(
|
|
||||||
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
|
|
||||||
workspaceSlug && projectId
|
|
||||||
? () => stateService.getStates(workspaceSlug as string, projectId as string)
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
const partialUpdateIssue = useCallback(
|
const partialUpdateIssue = useCallback(
|
||||||
(formData: Partial<IIssue>) => {
|
(formData: Partial<IIssue>) => {
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
@ -125,16 +121,8 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
issuesService
|
issuesService
|
||||||
.patchIssue(workspaceSlug as string, projectId as string, issue.id, formData)
|
.patchIssue(workspaceSlug as string, projectId as string, issue.id, formData)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
mutate(
|
if (cycleId) mutate(CYCLE_ISSUES(cycleId as string));
|
||||||
cycleId
|
if (moduleId) mutate(MODULE_ISSUES(moduleId as string));
|
||||||
? CYCLE_ISSUES(cycleId as string)
|
|
||||||
: CYCLE_ISSUES(issue?.issue_cycle?.cycle ?? "")
|
|
||||||
);
|
|
||||||
mutate(
|
|
||||||
moduleId
|
|
||||||
? MODULE_ISSUES(moduleId as string)
|
|
||||||
: MODULE_ISSUES(issue?.issue_module?.module ?? "")
|
|
||||||
);
|
|
||||||
|
|
||||||
mutate(PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string));
|
mutate(PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string));
|
||||||
})
|
})
|
||||||
@ -164,7 +152,12 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Draggable key={issue.id} draggableId={issue.id} index={index}>
|
<Draggable
|
||||||
|
key={issue.id}
|
||||||
|
draggableId={issue.id}
|
||||||
|
index={index}
|
||||||
|
isDragDisabled={selectedGroup === "created_by"}
|
||||||
|
>
|
||||||
{(provided, snapshot) => (
|
{(provided, snapshot) => (
|
||||||
<div
|
<div
|
||||||
className={`rounded border bg-white shadow-sm ${
|
className={`rounded border bg-white shadow-sm ${
|
||||||
@ -204,22 +197,22 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
</Link>
|
</Link>
|
||||||
<div className="flex flex-wrap items-center gap-x-1 gap-y-2 text-xs">
|
<div className="flex flex-wrap items-center gap-x-1 gap-y-2 text-xs">
|
||||||
{properties.priority && (
|
{properties.priority && (
|
||||||
<PrioritySelect
|
<ViewPrioritySelect
|
||||||
issue={issue}
|
issue={issue}
|
||||||
partialUpdateIssue={partialUpdateIssue}
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
isNotAllowed={isNotAllowed}
|
isNotAllowed={isNotAllowed}
|
||||||
|
position="left"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{properties.state && (
|
{properties.state && (
|
||||||
<StateSelect
|
<ViewStateSelect
|
||||||
issue={issue}
|
issue={issue}
|
||||||
states={states}
|
|
||||||
partialUpdateIssue={partialUpdateIssue}
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
isNotAllowed={isNotAllowed}
|
isNotAllowed={isNotAllowed}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{properties.due_date && (
|
{properties.due_date && (
|
||||||
<DueDateSelect
|
<ViewDueDateSelect
|
||||||
issue={issue}
|
issue={issue}
|
||||||
partialUpdateIssue={partialUpdateIssue}
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
isNotAllowed={isNotAllowed}
|
isNotAllowed={isNotAllowed}
|
||||||
@ -232,9 +225,8 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{properties.assignee && (
|
{properties.assignee && (
|
||||||
<AssigneeSelect
|
<ViewAssigneeSelect
|
||||||
issue={issue}
|
issue={issue}
|
||||||
members={members}
|
|
||||||
partialUpdateIssue={partialUpdateIssue}
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
isNotAllowed={isNotAllowed}
|
isNotAllowed={isNotAllowed}
|
||||||
/>
|
/>
|
||||||
|
@ -17,7 +17,7 @@ import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
|||||||
// types
|
// types
|
||||||
import { IIssue, Properties } from "types";
|
import { IIssue, Properties } from "types";
|
||||||
// common
|
// common
|
||||||
import { filterIssueOptions, groupByOptions, orderByOptions } from "constants/";
|
import { GROUP_BY_OPTIONS, ORDER_BY_OPTIONS, FILTER_ISSUE_OPTIONS } from "constants/issue";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issues?: IIssue[];
|
issues?: IIssue[];
|
||||||
@ -99,12 +99,12 @@ export const IssuesFilterView: React.FC<Props> = ({ issues }) => {
|
|||||||
<h4 className="text-sm text-gray-600">Group by</h4>
|
<h4 className="text-sm text-gray-600">Group by</h4>
|
||||||
<CustomMenu
|
<CustomMenu
|
||||||
label={
|
label={
|
||||||
groupByOptions.find((option) => option.key === groupByProperty)
|
GROUP_BY_OPTIONS.find((option) => option.key === groupByProperty)
|
||||||
?.name ?? "Select"
|
?.name ?? "Select"
|
||||||
}
|
}
|
||||||
width="lg"
|
width="lg"
|
||||||
>
|
>
|
||||||
{groupByOptions.map((option) =>
|
{GROUP_BY_OPTIONS.map((option) =>
|
||||||
issueView === "kanban" && option.key === null ? null : (
|
issueView === "kanban" && option.key === null ? null : (
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
key={option.key}
|
key={option.key}
|
||||||
@ -120,12 +120,12 @@ export const IssuesFilterView: React.FC<Props> = ({ issues }) => {
|
|||||||
<h4 className="text-sm text-gray-600">Order by</h4>
|
<h4 className="text-sm text-gray-600">Order by</h4>
|
||||||
<CustomMenu
|
<CustomMenu
|
||||||
label={
|
label={
|
||||||
orderByOptions.find((option) => option.key === orderBy)?.name ??
|
ORDER_BY_OPTIONS.find((option) => option.key === orderBy)?.name ??
|
||||||
"Select"
|
"Select"
|
||||||
}
|
}
|
||||||
width="lg"
|
width="lg"
|
||||||
>
|
>
|
||||||
{orderByOptions.map((option) =>
|
{ORDER_BY_OPTIONS.map((option) =>
|
||||||
groupByProperty === "priority" &&
|
groupByProperty === "priority" &&
|
||||||
option.key === "priority" ? null : (
|
option.key === "priority" ? null : (
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
@ -142,12 +142,12 @@ export const IssuesFilterView: React.FC<Props> = ({ issues }) => {
|
|||||||
<h4 className="text-sm text-gray-600">Issue type</h4>
|
<h4 className="text-sm text-gray-600">Issue type</h4>
|
||||||
<CustomMenu
|
<CustomMenu
|
||||||
label={
|
label={
|
||||||
filterIssueOptions.find((option) => option.key === filterIssue)
|
FILTER_ISSUE_OPTIONS.find((option) => option.key === filterIssue)
|
||||||
?.name ?? "Select"
|
?.name ?? "Select"
|
||||||
}
|
}
|
||||||
width="lg"
|
width="lg"
|
||||||
>
|
>
|
||||||
{filterIssueOptions.map((option) => (
|
{FILTER_ISSUE_OPTIONS.map((option) => (
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
key={option.key}
|
key={option.key}
|
||||||
onClick={() => setFilterIssue(option.key)}
|
onClick={() => setFilterIssue(option.key)}
|
||||||
|
@ -68,7 +68,7 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
|
|
||||||
const { issueView, groupedByIssues, groupByProperty: selectedGroup } = useIssueView(issues);
|
const { issueView, groupedByIssues, groupByProperty: selectedGroup } = useIssueView(issues);
|
||||||
|
|
||||||
const { data: states, mutate: mutateState } = useSWR<IState[]>(
|
const { data: states } = useSWR<IState[]>(
|
||||||
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
|
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
|
||||||
workspaceSlug
|
workspaceSlug
|
||||||
? () => stateService.getStates(workspaceSlug as string, projectId as string)
|
? () => stateService.getStates(workspaceSlug as string, projectId as string)
|
||||||
@ -86,252 +86,192 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
(result: DropResult) => {
|
(result: DropResult) => {
|
||||||
if (!result.destination || !workspaceSlug || !projectId) return;
|
if (!result.destination || !workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
const { source, destination, type } = result;
|
const { source, destination } = result;
|
||||||
|
|
||||||
if (type === "state") {
|
const draggedItem = groupedByIssues[source.droppableId][source.index];
|
||||||
const newStates = Array.from(states ?? []);
|
|
||||||
const [reorderedState] = newStates.splice(source.index, 1);
|
|
||||||
newStates.splice(destination.index, 0, reorderedState);
|
|
||||||
const prevSequenceNumber = newStates[destination.index - 1]?.sequence;
|
|
||||||
const nextSequenceNumber = newStates[destination.index + 1]?.sequence;
|
|
||||||
|
|
||||||
const sequenceNumber =
|
if (source.droppableId !== destination.droppableId) {
|
||||||
prevSequenceNumber && nextSequenceNumber
|
const sourceGroup = source.droppableId; // source group id
|
||||||
? (prevSequenceNumber + nextSequenceNumber) / 2
|
const destinationGroup = destination.droppableId; // destination group id
|
||||||
: nextSequenceNumber
|
|
||||||
? nextSequenceNumber - 15000 / 2
|
|
||||||
: prevSequenceNumber
|
|
||||||
? prevSequenceNumber + 15000 / 2
|
|
||||||
: 15000;
|
|
||||||
|
|
||||||
newStates[destination.index].sequence = sequenceNumber;
|
if (!sourceGroup || !destinationGroup) return;
|
||||||
|
|
||||||
mutateState(newStates, false);
|
if (selectedGroup === "priority") {
|
||||||
|
// update the removed item for mutation
|
||||||
|
draggedItem.priority = destinationGroup;
|
||||||
|
|
||||||
stateService
|
if (cycleId)
|
||||||
.patchState(
|
mutate<CycleIssueResponse[]>(
|
||||||
workspaceSlug as string,
|
CYCLE_ISSUES(cycleId as string),
|
||||||
projectId as string,
|
|
||||||
newStates[destination.index].id,
|
|
||||||
{
|
|
||||||
sequence: sequenceNumber,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.then((response) => {
|
|
||||||
console.log(response);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.error(err);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const draggedItem = groupedByIssues[source.droppableId][source.index];
|
|
||||||
if (source.droppableId !== destination.droppableId) {
|
|
||||||
const sourceGroup = source.droppableId; // source group id
|
|
||||||
const destinationGroup = destination.droppableId; // destination group id
|
|
||||||
|
|
||||||
if (!sourceGroup || !destinationGroup) return;
|
|
||||||
|
|
||||||
if (selectedGroup === "priority") {
|
|
||||||
// update the removed item for mutation
|
|
||||||
draggedItem.priority = destinationGroup;
|
|
||||||
|
|
||||||
if (cycleId)
|
|
||||||
mutate<CycleIssueResponse[]>(
|
|
||||||
CYCLE_ISSUES(cycleId as string),
|
|
||||||
(prevData) => {
|
|
||||||
if (!prevData) return prevData;
|
|
||||||
const updatedIssues = prevData.map((issue) => {
|
|
||||||
if (issue.issue_detail.id === draggedItem.id) {
|
|
||||||
return {
|
|
||||||
...issue,
|
|
||||||
issue_detail: {
|
|
||||||
...draggedItem,
|
|
||||||
priority: destinationGroup,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return issue;
|
|
||||||
});
|
|
||||||
return [...updatedIssues];
|
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
if (moduleId)
|
|
||||||
mutate<ModuleIssueResponse[]>(
|
|
||||||
MODULE_ISSUES(moduleId as string),
|
|
||||||
(prevData) => {
|
|
||||||
if (!prevData) return prevData;
|
|
||||||
const updatedIssues = prevData.map((issue) => {
|
|
||||||
if (issue.issue_detail.id === draggedItem.id) {
|
|
||||||
return {
|
|
||||||
...issue,
|
|
||||||
issue_detail: {
|
|
||||||
...draggedItem,
|
|
||||||
priority: destinationGroup,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return issue;
|
|
||||||
});
|
|
||||||
return [...updatedIssues];
|
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
mutate<IssueResponse>(
|
|
||||||
PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string),
|
|
||||||
(prevData) => {
|
(prevData) => {
|
||||||
if (!prevData) return prevData;
|
if (!prevData) return prevData;
|
||||||
|
const updatedIssues = prevData.map((issue) => {
|
||||||
const updatedIssues = prevData.results.map((issue) => {
|
if (issue.issue_detail.id === draggedItem.id) {
|
||||||
if (issue.id === draggedItem.id)
|
|
||||||
return {
|
return {
|
||||||
...draggedItem,
|
...issue,
|
||||||
priority: destinationGroup,
|
issue_detail: {
|
||||||
|
...draggedItem,
|
||||||
|
priority: destinationGroup,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
}
|
||||||
return issue;
|
return issue;
|
||||||
});
|
});
|
||||||
|
return [...updatedIssues];
|
||||||
return {
|
|
||||||
...prevData,
|
|
||||||
results: updatedIssues,
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
// patch request
|
if (moduleId)
|
||||||
issuesService
|
mutate<ModuleIssueResponse[]>(
|
||||||
.patchIssue(workspaceSlug as string, projectId as string, draggedItem.id, {
|
MODULE_ISSUES(moduleId as string),
|
||||||
priority: destinationGroup,
|
|
||||||
})
|
|
||||||
.then((res) => {
|
|
||||||
mutate(
|
|
||||||
cycleId
|
|
||||||
? CYCLE_ISSUES(cycleId as string)
|
|
||||||
: CYCLE_ISSUES(draggedItem.issue_cycle?.cycle ?? "")
|
|
||||||
);
|
|
||||||
mutate(
|
|
||||||
moduleId
|
|
||||||
? MODULE_ISSUES(moduleId as string)
|
|
||||||
: MODULE_ISSUES(draggedItem.issue_module?.module ?? "")
|
|
||||||
);
|
|
||||||
|
|
||||||
mutate(PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string));
|
|
||||||
});
|
|
||||||
} else if (selectedGroup === "state_detail.name") {
|
|
||||||
const destinationState = states?.find((s) => s.name === destinationGroup);
|
|
||||||
const destinationStateId = destinationState?.id;
|
|
||||||
|
|
||||||
// update the removed item for mutation
|
|
||||||
if (!destinationStateId || !destinationState) return;
|
|
||||||
draggedItem.state = destinationStateId;
|
|
||||||
draggedItem.state_detail = destinationState;
|
|
||||||
|
|
||||||
if (cycleId)
|
|
||||||
mutate<CycleIssueResponse[]>(
|
|
||||||
CYCLE_ISSUES(cycleId as string),
|
|
||||||
(prevData) => {
|
|
||||||
if (!prevData) return prevData;
|
|
||||||
const updatedIssues = prevData.map((issue) => {
|
|
||||||
if (issue.issue_detail.id === draggedItem.id) {
|
|
||||||
return {
|
|
||||||
...issue,
|
|
||||||
issue_detail: {
|
|
||||||
...draggedItem,
|
|
||||||
state_detail: destinationState,
|
|
||||||
state: destinationStateId,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return issue;
|
|
||||||
});
|
|
||||||
return [...updatedIssues];
|
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
if (moduleId)
|
|
||||||
mutate<ModuleIssueResponse[]>(
|
|
||||||
MODULE_ISSUES(moduleId as string),
|
|
||||||
(prevData) => {
|
|
||||||
if (!prevData) return prevData;
|
|
||||||
const updatedIssues = prevData.map((issue) => {
|
|
||||||
if (issue.issue_detail.id === draggedItem.id) {
|
|
||||||
return {
|
|
||||||
...issue,
|
|
||||||
issue_detail: {
|
|
||||||
...draggedItem,
|
|
||||||
state_detail: destinationState,
|
|
||||||
state: destinationStateId,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return issue;
|
|
||||||
});
|
|
||||||
return [...updatedIssues];
|
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
mutate<IssueResponse>(
|
|
||||||
PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string),
|
|
||||||
(prevData) => {
|
(prevData) => {
|
||||||
if (!prevData) return prevData;
|
if (!prevData) return prevData;
|
||||||
|
const updatedIssues = prevData.map((issue) => {
|
||||||
const updatedIssues = prevData.results.map((issue) => {
|
if (issue.issue_detail.id === draggedItem.id) {
|
||||||
if (issue.id === draggedItem.id)
|
|
||||||
return {
|
return {
|
||||||
...draggedItem,
|
...issue,
|
||||||
state_detail: destinationState,
|
issue_detail: {
|
||||||
state: destinationStateId,
|
...draggedItem,
|
||||||
|
priority: destinationGroup,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
}
|
||||||
return issue;
|
return issue;
|
||||||
});
|
});
|
||||||
|
return [...updatedIssues];
|
||||||
return {
|
|
||||||
...prevData,
|
|
||||||
results: updatedIssues,
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
// patch request
|
mutate<IssueResponse>(
|
||||||
issuesService
|
PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string),
|
||||||
.patchIssue(workspaceSlug as string, projectId as string, draggedItem.id, {
|
(prevData) => {
|
||||||
state: destinationStateId,
|
if (!prevData) return prevData;
|
||||||
})
|
|
||||||
.then((res) => {
|
const updatedIssues = prevData.results.map((issue) => {
|
||||||
mutate(
|
if (issue.id === draggedItem.id)
|
||||||
cycleId
|
return {
|
||||||
? CYCLE_ISSUES(cycleId as string)
|
...draggedItem,
|
||||||
: CYCLE_ISSUES(draggedItem.issue_cycle?.cycle ?? "")
|
priority: destinationGroup,
|
||||||
);
|
};
|
||||||
mutate(
|
|
||||||
moduleId
|
return issue;
|
||||||
? MODULE_ISSUES(moduleId as string)
|
|
||||||
: MODULE_ISSUES(draggedItem.issue_module?.module ?? "")
|
|
||||||
);
|
|
||||||
mutate(PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string));
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
return {
|
||||||
|
...prevData,
|
||||||
|
results: updatedIssues,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
// patch request
|
||||||
|
issuesService
|
||||||
|
.patchIssue(workspaceSlug as string, projectId as string, draggedItem.id, {
|
||||||
|
priority: destinationGroup,
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
if (cycleId) mutate(CYCLE_ISSUES(cycleId as string));
|
||||||
|
if (moduleId) mutate(MODULE_ISSUES(moduleId as string));
|
||||||
|
|
||||||
|
mutate(PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string));
|
||||||
|
});
|
||||||
|
} else if (selectedGroup === "state_detail.name") {
|
||||||
|
const destinationState = states?.find((s) => s.name === destinationGroup);
|
||||||
|
const destinationStateId = destinationState?.id;
|
||||||
|
|
||||||
|
// update the removed item for mutation
|
||||||
|
if (!destinationStateId || !destinationState) return;
|
||||||
|
draggedItem.state = destinationStateId;
|
||||||
|
draggedItem.state_detail = destinationState;
|
||||||
|
|
||||||
|
if (cycleId)
|
||||||
|
mutate<CycleIssueResponse[]>(
|
||||||
|
CYCLE_ISSUES(cycleId as string),
|
||||||
|
(prevData) => {
|
||||||
|
if (!prevData) return prevData;
|
||||||
|
const updatedIssues = prevData.map((issue) => {
|
||||||
|
if (issue.issue_detail.id === draggedItem.id) {
|
||||||
|
return {
|
||||||
|
...issue,
|
||||||
|
issue_detail: {
|
||||||
|
...draggedItem,
|
||||||
|
state_detail: destinationState,
|
||||||
|
state: destinationStateId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return issue;
|
||||||
|
});
|
||||||
|
return [...updatedIssues];
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
if (moduleId)
|
||||||
|
mutate<ModuleIssueResponse[]>(
|
||||||
|
MODULE_ISSUES(moduleId as string),
|
||||||
|
(prevData) => {
|
||||||
|
if (!prevData) return prevData;
|
||||||
|
const updatedIssues = prevData.map((issue) => {
|
||||||
|
if (issue.issue_detail.id === draggedItem.id) {
|
||||||
|
return {
|
||||||
|
...issue,
|
||||||
|
issue_detail: {
|
||||||
|
...draggedItem,
|
||||||
|
state_detail: destinationState,
|
||||||
|
state: destinationStateId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return issue;
|
||||||
|
});
|
||||||
|
return [...updatedIssues];
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
mutate<IssueResponse>(
|
||||||
|
PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string),
|
||||||
|
(prevData) => {
|
||||||
|
if (!prevData) return prevData;
|
||||||
|
|
||||||
|
const updatedIssues = prevData.results.map((issue) => {
|
||||||
|
if (issue.id === draggedItem.id)
|
||||||
|
return {
|
||||||
|
...draggedItem,
|
||||||
|
state_detail: destinationState,
|
||||||
|
state: destinationStateId,
|
||||||
|
};
|
||||||
|
|
||||||
|
return issue;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prevData,
|
||||||
|
results: updatedIssues,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
// patch request
|
||||||
|
issuesService
|
||||||
|
.patchIssue(workspaceSlug as string, projectId as string, draggedItem.id, {
|
||||||
|
state: destinationStateId,
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
if (cycleId) mutate(CYCLE_ISSUES(cycleId as string));
|
||||||
|
if (moduleId) mutate(MODULE_ISSUES(moduleId as string));
|
||||||
|
|
||||||
|
mutate(PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[workspaceSlug, cycleId, moduleId, groupedByIssues, projectId, selectedGroup, states]
|
||||||
workspaceSlug,
|
|
||||||
cycleId,
|
|
||||||
moduleId,
|
|
||||||
mutateState,
|
|
||||||
groupedByIssues,
|
|
||||||
projectId,
|
|
||||||
selectedGroup,
|
|
||||||
states,
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const addIssueToState = (groupTitle: string, stateId: string | null) => {
|
const addIssueToState = (groupTitle: string, stateId: string | null) => {
|
||||||
|
@ -3,20 +3,23 @@ import React, { useCallback } from "react";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
import useSWR, { mutate } from "swr";
|
import { mutate } from "swr";
|
||||||
|
|
||||||
// services
|
// services
|
||||||
import issuesService from "services/issues.service";
|
import issuesService from "services/issues.service";
|
||||||
import stateService from "services/state.service";
|
|
||||||
// components
|
// components
|
||||||
import { AssigneeSelect, DueDateSelect, PrioritySelect, StateSelect } from "components/core/select";
|
import {
|
||||||
|
ViewAssigneeSelect,
|
||||||
|
ViewDueDateSelect,
|
||||||
|
ViewPrioritySelect,
|
||||||
|
ViewStateSelect,
|
||||||
|
} from "components/issues/view-select";
|
||||||
// ui
|
// ui
|
||||||
import { CustomMenu } from "components/ui";
|
import { CustomMenu } from "components/ui";
|
||||||
// types
|
// types
|
||||||
import {
|
import {
|
||||||
CycleIssueResponse,
|
CycleIssueResponse,
|
||||||
IIssue,
|
IIssue,
|
||||||
IProjectMember,
|
|
||||||
IssueResponse,
|
IssueResponse,
|
||||||
ModuleIssueResponse,
|
ModuleIssueResponse,
|
||||||
Properties,
|
Properties,
|
||||||
@ -29,7 +32,6 @@ type Props = {
|
|||||||
type?: string;
|
type?: string;
|
||||||
issue: IIssue;
|
issue: IIssue;
|
||||||
properties: Properties;
|
properties: Properties;
|
||||||
members: IProjectMember[] | undefined;
|
|
||||||
editIssue: () => void;
|
editIssue: () => void;
|
||||||
removeIssue?: (() => void) | null;
|
removeIssue?: (() => void) | null;
|
||||||
handleDeleteIssue: (issue: IIssue) => void;
|
handleDeleteIssue: (issue: IIssue) => void;
|
||||||
@ -40,7 +42,6 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
type,
|
type,
|
||||||
issue,
|
issue,
|
||||||
properties,
|
properties,
|
||||||
members,
|
|
||||||
editIssue,
|
editIssue,
|
||||||
removeIssue,
|
removeIssue,
|
||||||
handleDeleteIssue,
|
handleDeleteIssue,
|
||||||
@ -49,13 +50,6 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||||
|
|
||||||
const { data: states } = useSWR(
|
|
||||||
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
|
|
||||||
workspaceSlug && projectId
|
|
||||||
? () => stateService.getStates(workspaceSlug as string, projectId as string)
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
const partialUpdateIssue = useCallback(
|
const partialUpdateIssue = useCallback(
|
||||||
(formData: Partial<IIssue>) => {
|
(formData: Partial<IIssue>) => {
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
@ -117,16 +111,8 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
issuesService
|
issuesService
|
||||||
.patchIssue(workspaceSlug as string, projectId as string, issue.id, formData)
|
.patchIssue(workspaceSlug as string, projectId as string, issue.id, formData)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
mutate(
|
if (cycleId) mutate(CYCLE_ISSUES(cycleId as string));
|
||||||
cycleId
|
if (moduleId) mutate(MODULE_ISSUES(moduleId as string));
|
||||||
? CYCLE_ISSUES(cycleId as string)
|
|
||||||
: CYCLE_ISSUES(issue?.issue_cycle?.cycle ?? "")
|
|
||||||
);
|
|
||||||
mutate(
|
|
||||||
moduleId
|
|
||||||
? MODULE_ISSUES(moduleId as string)
|
|
||||||
: MODULE_ISSUES(issue?.issue_module?.module ?? "")
|
|
||||||
);
|
|
||||||
|
|
||||||
mutate(PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string));
|
mutate(PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string));
|
||||||
})
|
})
|
||||||
@ -161,22 +147,21 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-shrink-0 flex-wrap items-center gap-x-1 gap-y-2 text-xs">
|
<div className="flex flex-shrink-0 flex-wrap items-center gap-x-1 gap-y-2 text-xs">
|
||||||
{properties.priority && (
|
{properties.priority && (
|
||||||
<PrioritySelect
|
<ViewPrioritySelect
|
||||||
issue={issue}
|
issue={issue}
|
||||||
partialUpdateIssue={partialUpdateIssue}
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
isNotAllowed={isNotAllowed}
|
isNotAllowed={isNotAllowed}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{properties.state && (
|
{properties.state && (
|
||||||
<StateSelect
|
<ViewStateSelect
|
||||||
issue={issue}
|
issue={issue}
|
||||||
states={states}
|
|
||||||
partialUpdateIssue={partialUpdateIssue}
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
isNotAllowed={isNotAllowed}
|
isNotAllowed={isNotAllowed}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{properties.due_date && (
|
{properties.due_date && (
|
||||||
<DueDateSelect
|
<ViewDueDateSelect
|
||||||
issue={issue}
|
issue={issue}
|
||||||
partialUpdateIssue={partialUpdateIssue}
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
isNotAllowed={isNotAllowed}
|
isNotAllowed={isNotAllowed}
|
||||||
@ -188,9 +173,8 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{properties.assignee && (
|
{properties.assignee && (
|
||||||
<AssigneeSelect
|
<ViewAssigneeSelect
|
||||||
issue={issue}
|
issue={issue}
|
||||||
members={members}
|
|
||||||
partialUpdateIssue={partialUpdateIssue}
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
isNotAllowed={isNotAllowed}
|
isNotAllowed={isNotAllowed}
|
||||||
/>
|
/>
|
||||||
|
@ -101,7 +101,6 @@ export const SingleList: React.FC<Props> = ({
|
|||||||
type={type}
|
type={type}
|
||||||
issue={issue}
|
issue={issue}
|
||||||
properties={properties}
|
properties={properties}
|
||||||
members={members}
|
|
||||||
editIssue={() => handleEditIssue(issue)}
|
editIssue={() => handleEditIssue(issue)}
|
||||||
handleDeleteIssue={handleDeleteIssue}
|
handleDeleteIssue={handleDeleteIssue}
|
||||||
removeIssue={() => {
|
removeIssue={() => {
|
||||||
|
@ -1,48 +0,0 @@
|
|||||||
// ui
|
|
||||||
import { CustomDatePicker } from "components/ui";
|
|
||||||
// helpers
|
|
||||||
import { findHowManyDaysLeft, renderShortNumericDateFormat } from "helpers/date-time.helper";
|
|
||||||
// types
|
|
||||||
import { IIssue } from "types";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
issue: IIssue;
|
|
||||||
partialUpdateIssue: (formData: Partial<IIssue>) => void;
|
|
||||||
isNotAllowed: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DueDateSelect: React.FC<Props> = ({ issue, partialUpdateIssue, isNotAllowed }) => (
|
|
||||||
<div
|
|
||||||
className={`group relative ${
|
|
||||||
issue.target_date === null
|
|
||||||
? ""
|
|
||||||
: issue.target_date < new Date().toISOString()
|
|
||||||
? "text-red-600"
|
|
||||||
: findHowManyDaysLeft(issue.target_date) <= 3 && "text-orange-400"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<CustomDatePicker
|
|
||||||
placeholder="N/A"
|
|
||||||
value={issue?.target_date}
|
|
||||||
onChange={(val) =>
|
|
||||||
partialUpdateIssue({
|
|
||||||
target_date: val,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
className={issue?.target_date ? "w-[6.5rem]" : "w-[3rem] text-center"}
|
|
||||||
/>
|
|
||||||
<div className="absolute bottom-full right-0 z-10 mb-2 hidden whitespace-nowrap rounded-md bg-white p-2 shadow-md group-hover:block">
|
|
||||||
<h5 className="mb-1 font-medium text-gray-900">Due date</h5>
|
|
||||||
<div>{renderShortNumericDateFormat(issue.target_date ?? "")}</div>
|
|
||||||
<div>
|
|
||||||
{issue.target_date
|
|
||||||
? issue.target_date < new Date().toISOString()
|
|
||||||
? `Due date has passed by ${findHowManyDaysLeft(issue.target_date)} days`
|
|
||||||
: findHowManyDaysLeft(issue.target_date) <= 3
|
|
||||||
? `Due date is in ${findHowManyDaysLeft(issue.target_date)} days`
|
|
||||||
: "Due date"
|
|
||||||
: "N/A"}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
@ -1,97 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
// ui
|
|
||||||
import { Listbox, Transition } from "@headlessui/react";
|
|
||||||
// types
|
|
||||||
import { IIssue, IState } from "types";
|
|
||||||
// constants
|
|
||||||
import { getPriorityIcon } from "constants/global";
|
|
||||||
import { PRIORITIES } from "constants/";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
issue: IIssue;
|
|
||||||
partialUpdateIssue: (formData: Partial<IIssue>) => void;
|
|
||||||
isNotAllowed: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PrioritySelect: React.FC<Props> = ({ issue, partialUpdateIssue, isNotAllowed }) => (
|
|
||||||
<Listbox
|
|
||||||
as="div"
|
|
||||||
value={issue.priority}
|
|
||||||
onChange={(data: string) => {
|
|
||||||
partialUpdateIssue({ priority: data });
|
|
||||||
}}
|
|
||||||
className="group relative flex-shrink-0"
|
|
||||||
disabled={isNotAllowed}
|
|
||||||
>
|
|
||||||
{({ open }) => (
|
|
||||||
<>
|
|
||||||
<div>
|
|
||||||
<Listbox.Button
|
|
||||||
className={`flex ${
|
|
||||||
isNotAllowed ? "cursor-not-allowed" : "cursor-pointer"
|
|
||||||
} items-center gap-x-2 rounded px-2 py-0.5 capitalize shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 ${
|
|
||||||
issue.priority === "urgent"
|
|
||||||
? "bg-red-100 text-red-600"
|
|
||||||
: issue.priority === "high"
|
|
||||||
? "bg-orange-100 text-orange-500"
|
|
||||||
: issue.priority === "medium"
|
|
||||||
? "bg-yellow-100 text-yellow-500"
|
|
||||||
: issue.priority === "low"
|
|
||||||
? "bg-green-100 text-green-500"
|
|
||||||
: "bg-gray-100"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{getPriorityIcon(
|
|
||||||
issue.priority && issue.priority !== "" ? issue.priority ?? "" : "None",
|
|
||||||
"text-sm"
|
|
||||||
)}
|
|
||||||
</Listbox.Button>
|
|
||||||
|
|
||||||
<Transition
|
|
||||||
show={open}
|
|
||||||
as={React.Fragment}
|
|
||||||
leave="transition ease-in duration-100"
|
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0"
|
|
||||||
>
|
|
||||||
<Listbox.Options className="absolute right-0 z-10 mt-1 max-h-48 w-36 overflow-auto rounded-md bg-white py-1 text-xs shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
|
||||||
{PRIORITIES?.map((priority) => (
|
|
||||||
<Listbox.Option
|
|
||||||
key={priority}
|
|
||||||
className={({ active }) =>
|
|
||||||
`flex cursor-pointer select-none items-center gap-x-2 px-3 py-2 capitalize ${
|
|
||||||
active ? "bg-indigo-50" : "bg-white"
|
|
||||||
}`
|
|
||||||
}
|
|
||||||
value={priority}
|
|
||||||
>
|
|
||||||
{getPriorityIcon(priority, "text-sm")}
|
|
||||||
{priority ?? "None"}
|
|
||||||
</Listbox.Option>
|
|
||||||
))}
|
|
||||||
</Listbox.Options>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
<div className="absolute bottom-full right-0 z-10 mb-2 hidden whitespace-nowrap rounded-md bg-white p-2 shadow-md group-hover:block">
|
|
||||||
<h5 className="mb-1 font-medium text-gray-900">Priority</h5>
|
|
||||||
<div
|
|
||||||
className={`capitalize ${
|
|
||||||
issue.priority === "urgent"
|
|
||||||
? "text-red-600"
|
|
||||||
: issue.priority === "high"
|
|
||||||
? "text-orange-500"
|
|
||||||
: issue.priority === "medium"
|
|
||||||
? "text-yellow-500"
|
|
||||||
: issue.priority === "low"
|
|
||||||
? "text-green-500"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{issue.priority ?? "None"}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Listbox>
|
|
||||||
);
|
|
@ -1,55 +0,0 @@
|
|||||||
// ui
|
|
||||||
import { CustomSelect } from "components/ui";
|
|
||||||
// helpers
|
|
||||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
|
||||||
// types
|
|
||||||
import { IIssue, IState } from "types";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
issue: IIssue;
|
|
||||||
states: IState[] | undefined;
|
|
||||||
partialUpdateIssue: (formData: Partial<IIssue>) => void;
|
|
||||||
isNotAllowed: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const StateSelect: React.FC<Props> = ({
|
|
||||||
issue,
|
|
||||||
states,
|
|
||||||
partialUpdateIssue,
|
|
||||||
isNotAllowed,
|
|
||||||
}) => (
|
|
||||||
<CustomSelect
|
|
||||||
label={
|
|
||||||
<>
|
|
||||||
<span
|
|
||||||
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
|
||||||
style={{
|
|
||||||
backgroundColor: states?.find((s) => s.id === issue.state)?.color,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{addSpaceIfCamelCase(states?.find((s) => s.id === issue.state)?.name ?? "")}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
value={issue.state}
|
|
||||||
onChange={(data: string) => {
|
|
||||||
partialUpdateIssue({ state: data });
|
|
||||||
}}
|
|
||||||
maxHeight="md"
|
|
||||||
noChevron
|
|
||||||
disabled={isNotAllowed}
|
|
||||||
>
|
|
||||||
{states?.map((state) => (
|
|
||||||
<CustomSelect.Option key={state.id} value={state.id}>
|
|
||||||
<>
|
|
||||||
<span
|
|
||||||
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
|
||||||
style={{
|
|
||||||
backgroundColor: state.color,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{addSpaceIfCamelCase(state.name)}
|
|
||||||
</>
|
|
||||||
</CustomSelect.Option>
|
|
||||||
))}
|
|
||||||
</CustomSelect>
|
|
||||||
);
|
|
@ -7,7 +7,7 @@ import { Props } from "./types";
|
|||||||
import emojis from "./emojis.json";
|
import emojis from "./emojis.json";
|
||||||
// helpers
|
// helpers
|
||||||
import { getRecentEmojis, saveRecentEmoji } from "./helpers";
|
import { getRecentEmojis, saveRecentEmoji } from "./helpers";
|
||||||
import { getRandomEmoji } from "helpers/functions.helper";
|
import { getRandomEmoji } from "helpers/common.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
CalendarDaysIcon,
|
CalendarDaysIcon,
|
||||||
ChartBarIcon,
|
ChartBarIcon,
|
||||||
ChatBubbleBottomCenterTextIcon,
|
ChatBubbleBottomCenterTextIcon,
|
||||||
|
RectangleGroupIcon,
|
||||||
Squares2X2Icon,
|
Squares2X2Icon,
|
||||||
UserIcon,
|
UserIcon,
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
@ -18,7 +19,7 @@ import { CommentCard } from "components/issues/comment";
|
|||||||
// ui
|
// ui
|
||||||
import { Loader } from "components/ui";
|
import { Loader } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { BlockedIcon, BlockerIcon, TagIcon, UserGroupIcon } from "components/icons";
|
import { BlockedIcon, BlockerIcon, CyclesIcon, TagIcon, UserGroupIcon } from "components/icons";
|
||||||
// helpers
|
// helpers
|
||||||
import { renderShortNumericDateFormat, timeAgo } from "helpers/date-time.helper";
|
import { renderShortNumericDateFormat, timeAgo } from "helpers/date-time.helper";
|
||||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||||
@ -47,9 +48,17 @@ const activityDetails: {
|
|||||||
message: "marked this issue is blocking",
|
message: "marked this issue is blocking",
|
||||||
icon: <BlockerIcon height="16" width="16" />,
|
icon: <BlockerIcon height="16" width="16" />,
|
||||||
},
|
},
|
||||||
|
cycles: {
|
||||||
|
message: "set the cycle to",
|
||||||
|
icon: <CyclesIcon height="16" width="16" />,
|
||||||
|
},
|
||||||
labels: {
|
labels: {
|
||||||
icon: <TagIcon height="16" width="16" />,
|
icon: <TagIcon height="16" width="16" />,
|
||||||
},
|
},
|
||||||
|
modules: {
|
||||||
|
message: "set the module to",
|
||||||
|
icon: <RectangleGroupIcon className="h-4 w-4" />,
|
||||||
|
},
|
||||||
state: {
|
state: {
|
||||||
message: "set the state to",
|
message: "set the state to",
|
||||||
icon: <Squares2X2Icon className="h-4 w-4" />,
|
icon: <Squares2X2Icon className="h-4 w-4" />,
|
||||||
@ -76,10 +85,12 @@ const activityDetails: {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssueActivitySection: React.FC<{
|
type Props = {
|
||||||
issueActivities: IIssueActivity[];
|
issueActivities: IIssueActivity[];
|
||||||
mutate: KeyedMutator<IIssueActivity[]>;
|
mutate: KeyedMutator<IIssueActivity[]>;
|
||||||
}> = ({ issueActivities, mutate }) => {
|
};
|
||||||
|
|
||||||
|
export const IssueActivitySection: React.FC<Props> = ({ issueActivities, mutate }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { workspaceSlug, projectId, issueId } = router.query;
|
const { workspaceSlug, projectId, issueId } = router.query;
|
||||||
@ -183,7 +194,9 @@ export const IssueActivitySection: React.FC<{
|
|||||||
?.message}{" "}
|
?.message}{" "}
|
||||||
</span>
|
</span>
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{activity.verb === "created" ? (
|
{activity.verb === "created" &&
|
||||||
|
activity.field !== "cycles" &&
|
||||||
|
activity.field !== "modules" ? (
|
||||||
<span className="text-gray-600">created this issue.</span>
|
<span className="text-gray-600">created this issue.</span>
|
||||||
) : activity.field === "description" ? null : activity.field === "state" ? (
|
) : activity.field === "description" ? null : activity.field === "state" ? (
|
||||||
activity.new_value ? (
|
activity.new_value ? (
|
||||||
|
@ -10,7 +10,7 @@ import issuesServices from "services/issues.service";
|
|||||||
// ui
|
// ui
|
||||||
import { Loader } from "components/ui";
|
import { Loader } from "components/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { debounce } from "helpers/functions.helper";
|
import { debounce } from "helpers/common.helper";
|
||||||
// types
|
// types
|
||||||
import type { IIssueActivity, IIssueComment } from "types";
|
import type { IIssueActivity, IIssueComment } from "types";
|
||||||
import type { KeyedMutator } from "swr";
|
import type { KeyedMutator } from "swr";
|
||||||
|
@ -3,20 +3,23 @@ import React, { useCallback } from "react";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
import useSWR, { mutate } from "swr";
|
import { mutate } from "swr";
|
||||||
|
|
||||||
// services
|
// services
|
||||||
import stateService from "services/state.service";
|
|
||||||
import issuesService from "services/issues.service";
|
import issuesService from "services/issues.service";
|
||||||
// components
|
// components
|
||||||
import { DueDateSelect, PrioritySelect, StateSelect } from "components/core/select";
|
import {
|
||||||
|
ViewDueDateSelect,
|
||||||
|
ViewPrioritySelect,
|
||||||
|
ViewStateSelect,
|
||||||
|
} from "components/issues/view-select";
|
||||||
// ui
|
// ui
|
||||||
import { AssigneesList } from "components/ui/avatar";
|
import { AssigneesList } from "components/ui/avatar";
|
||||||
import { CustomMenu } from "components/ui";
|
import { CustomMenu } from "components/ui";
|
||||||
// types
|
// types
|
||||||
import { IIssue, Properties } from "types";
|
import { IIssue, Properties } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { STATE_LIST, USER_ISSUE } from "constants/fetch-keys";
|
import { USER_ISSUE } from "constants/fetch-keys";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issue: IIssue;
|
issue: IIssue;
|
||||||
@ -34,13 +37,6 @@ export const MyIssuesListItem: React.FC<Props> = ({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
const { data: states } = useSWR(
|
|
||||||
workspaceSlug && projectId ? STATE_LIST(projectId) : null,
|
|
||||||
workspaceSlug && projectId
|
|
||||||
? () => stateService.getStates(workspaceSlug as string, projectId)
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
const partialUpdateIssue = useCallback(
|
const partialUpdateIssue = useCallback(
|
||||||
(formData: Partial<IIssue>) => {
|
(formData: Partial<IIssue>) => {
|
||||||
if (!workspaceSlug) return;
|
if (!workspaceSlug) return;
|
||||||
@ -92,22 +88,21 @@ export const MyIssuesListItem: React.FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-shrink-0 flex-wrap items-center gap-x-1 gap-y-2 text-xs">
|
<div className="flex flex-shrink-0 flex-wrap items-center gap-x-1 gap-y-2 text-xs">
|
||||||
{properties.priority && (
|
{properties.priority && (
|
||||||
<PrioritySelect
|
<ViewPrioritySelect
|
||||||
issue={issue}
|
issue={issue}
|
||||||
partialUpdateIssue={partialUpdateIssue}
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
isNotAllowed={isNotAllowed}
|
isNotAllowed={isNotAllowed}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{properties.state && (
|
{properties.state && (
|
||||||
<StateSelect
|
<ViewStateSelect
|
||||||
issue={issue}
|
issue={issue}
|
||||||
states={states}
|
|
||||||
partialUpdateIssue={partialUpdateIssue}
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
isNotAllowed={isNotAllowed}
|
isNotAllowed={isNotAllowed}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{properties.due_date && (
|
{properties.due_date && (
|
||||||
<DueDateSelect
|
<ViewDueDateSelect
|
||||||
issue={issue}
|
issue={issue}
|
||||||
partialUpdateIssue={partialUpdateIssue}
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
isNotAllowed={isNotAllowed}
|
isNotAllowed={isNotAllowed}
|
||||||
|
@ -72,7 +72,7 @@ export const IssueLabelSelect: React.FC<Props> = ({ value, onChange, projectId }
|
|||||||
const options = issueLabels?.map((label) => ({
|
const options = issueLabels?.map((label) => ({
|
||||||
value: label.id,
|
value: label.id,
|
||||||
display: label.name,
|
display: label.name,
|
||||||
color: label.colour,
|
color: label.color,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const filteredOptions =
|
const filteredOptions =
|
||||||
|
@ -2,9 +2,10 @@ import React from "react";
|
|||||||
|
|
||||||
// headless ui
|
// headless ui
|
||||||
import { Listbox, Transition } from "@headlessui/react";
|
import { Listbox, Transition } from "@headlessui/react";
|
||||||
|
// icons
|
||||||
|
import { getPriorityIcon } from "components/icons/priority-icon";
|
||||||
// constants
|
// constants
|
||||||
import { getPriorityIcon } from "constants/global";
|
import { PRIORITIES } from "constants/project";
|
||||||
import { PRIORITIES } from "constants/";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
value: string | null;
|
value: string | null;
|
||||||
|
@ -1,17 +1,16 @@
|
|||||||
// react
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
// react-hook-form
|
// react-hook-form
|
||||||
import { Control, Controller, UseFormWatch } from "react-hook-form";
|
import { Control, Controller } from "react-hook-form";
|
||||||
// ui
|
// ui
|
||||||
import { ChartBarIcon } from "@heroicons/react/24/outline";
|
|
||||||
import { CustomSelect } from "components/ui";
|
import { CustomSelect } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
|
import { ChartBarIcon } from "@heroicons/react/24/outline";
|
||||||
|
import { getPriorityIcon } from "components/icons/priority-icon";
|
||||||
// types
|
// types
|
||||||
import { IIssue, UserAuth } from "types";
|
import { IIssue, UserAuth } from "types";
|
||||||
// common
|
|
||||||
// constants
|
// constants
|
||||||
import { getPriorityIcon } from "constants/global";
|
import { PRIORITIES } from "constants/project";
|
||||||
import { PRIORITIES } from "constants/";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
control: Control<IIssue, any>;
|
control: Control<IIssue, any>;
|
||||||
|
@ -56,7 +56,7 @@ type Props = {
|
|||||||
|
|
||||||
const defaultValues: Partial<IIssueLabels> = {
|
const defaultValues: Partial<IIssueLabels> = {
|
||||||
name: "",
|
name: "",
|
||||||
colour: "#ff0000",
|
color: "#ff0000",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssueDetailsSidebar: React.FC<Props> = ({
|
export const IssueDetailsSidebar: React.FC<Props> = ({
|
||||||
@ -316,7 +316,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="h-2 w-2 flex-shrink-0 rounded-full"
|
className="h-2 w-2 flex-shrink-0 rounded-full"
|
||||||
style={{ backgroundColor: singleLabel?.colour ?? "green" }}
|
style={{ backgroundColor: singleLabel?.color ?? "green" }}
|
||||||
/>
|
/>
|
||||||
{singleLabel.name}
|
{singleLabel.name}
|
||||||
<XMarkIcon className="h-2 w-2 group-hover:text-red-500" />
|
<XMarkIcon className="h-2 w-2 group-hover:text-red-500" />
|
||||||
@ -372,7 +372,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="h-2 w-2 flex-shrink-0 rounded-full"
|
className="h-2 w-2 flex-shrink-0 rounded-full"
|
||||||
style={{ backgroundColor: label.colour ?? "green" }}
|
style={{ backgroundColor: label.color ?? "green" }}
|
||||||
/>
|
/>
|
||||||
{label.name}
|
{label.name}
|
||||||
</Listbox.Option>
|
</Listbox.Option>
|
||||||
@ -422,11 +422,11 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
|||||||
<Popover.Button
|
<Popover.Button
|
||||||
className={`flex items-center gap-1 rounded-md bg-white p-1 outline-none focus:ring-2 focus:ring-indigo-500`}
|
className={`flex items-center gap-1 rounded-md bg-white p-1 outline-none focus:ring-2 focus:ring-indigo-500`}
|
||||||
>
|
>
|
||||||
{watch("colour") && watch("colour") !== "" && (
|
{watch("color") && watch("color") !== "" && (
|
||||||
<span
|
<span
|
||||||
className="h-5 w-5 rounded"
|
className="h-5 w-5 rounded"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: watch("colour") ?? "green",
|
backgroundColor: watch("color") ?? "green",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -444,7 +444,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
|||||||
>
|
>
|
||||||
<Popover.Panel className="absolute right-0 bottom-8 z-10 mt-1 max-w-xs transform px-2 sm:px-0">
|
<Popover.Panel className="absolute right-0 bottom-8 z-10 mt-1 max-w-xs transform px-2 sm:px-0">
|
||||||
<Controller
|
<Controller
|
||||||
name="colour"
|
name="color"
|
||||||
control={controlLabel}
|
control={controlLabel}
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<TwitterPicker
|
<TwitterPicker
|
||||||
|
@ -1,41 +1,59 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import useSWR from "swr";
|
||||||
|
|
||||||
// headless ui
|
// headless ui
|
||||||
import { Listbox, Transition } from "@headlessui/react";
|
import { Listbox, Transition } from "@headlessui/react";
|
||||||
|
// services
|
||||||
|
import projectService from "services/project.service";
|
||||||
// ui
|
// ui
|
||||||
import { AssigneesList, Avatar } from "components/ui";
|
import { AssigneesList, Avatar } from "components/ui";
|
||||||
// types
|
// types
|
||||||
import { IIssue, IProjectMember } from "types";
|
import { IIssue } from "types";
|
||||||
|
// fetch-keys
|
||||||
|
import { PROJECT_MEMBERS } from "constants/fetch-keys";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issue: IIssue;
|
issue: IIssue;
|
||||||
members: IProjectMember[] | undefined;
|
|
||||||
partialUpdateIssue: (formData: Partial<IIssue>) => void;
|
partialUpdateIssue: (formData: Partial<IIssue>) => void;
|
||||||
|
position?: "left" | "right";
|
||||||
isNotAllowed: boolean;
|
isNotAllowed: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AssigneeSelect: React.FC<Props> = ({
|
export const ViewAssigneeSelect: React.FC<Props> = ({
|
||||||
issue,
|
issue,
|
||||||
members,
|
|
||||||
partialUpdateIssue,
|
partialUpdateIssue,
|
||||||
|
position = "right",
|
||||||
isNotAllowed,
|
isNotAllowed,
|
||||||
}) => (
|
}) => {
|
||||||
<Listbox
|
const router = useRouter();
|
||||||
as="div"
|
const { workspaceSlug, projectId } = router.query;
|
||||||
value={issue.assignees}
|
|
||||||
onChange={(data: any) => {
|
|
||||||
const newData = issue.assignees ?? [];
|
|
||||||
|
|
||||||
if (newData.includes(data)) newData.splice(newData.indexOf(data), 1);
|
const { data: members } = useSWR(
|
||||||
else newData.push(data);
|
projectId ? PROJECT_MEMBERS(projectId as string) : null,
|
||||||
|
workspaceSlug && projectId
|
||||||
|
? () => projectService.projectMembers(workspaceSlug as string, projectId as string)
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
partialUpdateIssue({ assignees_list: newData });
|
return (
|
||||||
}}
|
<Listbox
|
||||||
className="group relative flex-shrink-0"
|
as="div"
|
||||||
disabled={isNotAllowed}
|
value={issue.assignees}
|
||||||
>
|
onChange={(data: any) => {
|
||||||
{({ open }) => (
|
const newData = issue.assignees ?? [];
|
||||||
<>
|
|
||||||
|
if (newData.includes(data)) newData.splice(newData.indexOf(data), 1);
|
||||||
|
else newData.push(data);
|
||||||
|
|
||||||
|
partialUpdateIssue({ assignees_list: newData });
|
||||||
|
}}
|
||||||
|
className="group relative flex-shrink-0"
|
||||||
|
disabled={isNotAllowed}
|
||||||
|
>
|
||||||
|
{({ open }) => (
|
||||||
<div>
|
<div>
|
||||||
<Listbox.Button>
|
<Listbox.Button>
|
||||||
<div
|
<div
|
||||||
@ -54,12 +72,16 @@ export const AssigneeSelect: React.FC<Props> = ({
|
|||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<Listbox.Options className="absolute right-0 z-10 mt-1 max-h-48 overflow-auto rounded-md bg-white py-1 text-xs shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
<Listbox.Options
|
||||||
|
className={`absolute z-10 mt-1 max-h-48 overflow-auto rounded-md bg-white py-1 text-xs shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none ${
|
||||||
|
position === "left" ? "left-0" : "right-0"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
{members?.map((member) => (
|
{members?.map((member) => (
|
||||||
<Listbox.Option
|
<Listbox.Option
|
||||||
key={member.member.id}
|
key={member.member.id}
|
||||||
className={({ active, selected }) =>
|
className={({ active, selected }) =>
|
||||||
`flex items-center gap-x-1 cursor-pointer select-none p-2 ${
|
`flex items-center gap-x-1 cursor-pointer select-none p-2 whitespace-nowrap ${
|
||||||
active ? "bg-indigo-50" : ""
|
active ? "bg-indigo-50" : ""
|
||||||
} ${
|
} ${
|
||||||
selected || issue.assignees?.includes(member.member.id)
|
selected || issue.assignees?.includes(member.member.id)
|
||||||
@ -70,25 +92,15 @@ export const AssigneeSelect: React.FC<Props> = ({
|
|||||||
value={member.member.id}
|
value={member.member.id}
|
||||||
>
|
>
|
||||||
<Avatar user={member.member} />
|
<Avatar user={member.member} />
|
||||||
<p>
|
{member.member.first_name && member.member.first_name !== ""
|
||||||
{member.member.first_name && member.member.first_name !== ""
|
? member.member.first_name
|
||||||
? member.member.first_name
|
: member.member.email}
|
||||||
: member.member.email}
|
|
||||||
</p>
|
|
||||||
</Listbox.Option>
|
</Listbox.Option>
|
||||||
))}
|
))}
|
||||||
</Listbox.Options>
|
</Listbox.Options>
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute bottom-full right-0 z-10 mb-2 hidden whitespace-nowrap rounded-md bg-white p-2 shadow-md group-hover:block">
|
)}
|
||||||
<h5 className="mb-1 font-medium">Assigned to</h5>
|
</Listbox>
|
||||||
<div>
|
);
|
||||||
{issue.assignee_details?.length > 0
|
};
|
||||||
? issue.assignee_details.map((assignee) => assignee.first_name).join(", ")
|
|
||||||
: "No one"}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Listbox>
|
|
||||||
);
|
|
36
apps/app/components/issues/view-select/due-date.tsx
Normal file
36
apps/app/components/issues/view-select/due-date.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// ui
|
||||||
|
import { CustomDatePicker } from "components/ui";
|
||||||
|
// helpers
|
||||||
|
import { findHowManyDaysLeft } from "helpers/date-time.helper";
|
||||||
|
// types
|
||||||
|
import { IIssue } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
issue: IIssue;
|
||||||
|
partialUpdateIssue: (formData: Partial<IIssue>) => void;
|
||||||
|
isNotAllowed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ViewDueDateSelect: React.FC<Props> = ({ issue, partialUpdateIssue, isNotAllowed }) => (
|
||||||
|
<div
|
||||||
|
className={`group relative ${
|
||||||
|
issue.target_date === null
|
||||||
|
? ""
|
||||||
|
: issue.target_date < new Date().toISOString()
|
||||||
|
? "text-red-600"
|
||||||
|
: findHowManyDaysLeft(issue.target_date) <= 3 && "text-orange-400"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<CustomDatePicker
|
||||||
|
placeholder="N/A"
|
||||||
|
value={issue?.target_date}
|
||||||
|
onChange={(val) =>
|
||||||
|
partialUpdateIssue({
|
||||||
|
target_date: val,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className={issue?.target_date ? "w-[6.5rem]" : "w-[3rem] text-center"}
|
||||||
|
disabled={isNotAllowed}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
88
apps/app/components/issues/view-select/priority.tsx
Normal file
88
apps/app/components/issues/view-select/priority.tsx
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// ui
|
||||||
|
import { Listbox, Transition } from "@headlessui/react";
|
||||||
|
// icons
|
||||||
|
import { getPriorityIcon } from "components/icons/priority-icon";
|
||||||
|
// types
|
||||||
|
import { IIssue } from "types";
|
||||||
|
// constants
|
||||||
|
import { PRIORITIES } from "constants/project";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
issue: IIssue;
|
||||||
|
partialUpdateIssue: (formData: Partial<IIssue>) => void;
|
||||||
|
position?: "left" | "right";
|
||||||
|
isNotAllowed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ViewPrioritySelect: React.FC<Props> = ({
|
||||||
|
issue,
|
||||||
|
partialUpdateIssue,
|
||||||
|
position = "right",
|
||||||
|
isNotAllowed,
|
||||||
|
}) => (
|
||||||
|
<Listbox
|
||||||
|
as="div"
|
||||||
|
value={issue.priority}
|
||||||
|
onChange={(data: string) => {
|
||||||
|
partialUpdateIssue({ priority: data });
|
||||||
|
}}
|
||||||
|
className="group relative flex-shrink-0"
|
||||||
|
disabled={isNotAllowed}
|
||||||
|
>
|
||||||
|
{({ open }) => (
|
||||||
|
<div>
|
||||||
|
<Listbox.Button
|
||||||
|
className={`flex ${
|
||||||
|
isNotAllowed ? "cursor-not-allowed" : "cursor-pointer"
|
||||||
|
} items-center gap-x-2 rounded px-2 py-0.5 capitalize shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 ${
|
||||||
|
issue.priority === "urgent"
|
||||||
|
? "bg-red-100 text-red-600"
|
||||||
|
: issue.priority === "high"
|
||||||
|
? "bg-orange-100 text-orange-500"
|
||||||
|
: issue.priority === "medium"
|
||||||
|
? "bg-yellow-100 text-yellow-500"
|
||||||
|
: issue.priority === "low"
|
||||||
|
? "bg-green-100 text-green-500"
|
||||||
|
: "bg-gray-100"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{getPriorityIcon(
|
||||||
|
issue.priority && issue.priority !== "" ? issue.priority ?? "" : "None",
|
||||||
|
"text-sm"
|
||||||
|
)}
|
||||||
|
</Listbox.Button>
|
||||||
|
|
||||||
|
<Transition
|
||||||
|
show={open}
|
||||||
|
as={React.Fragment}
|
||||||
|
leave="transition ease-in duration-100"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<Listbox.Options
|
||||||
|
className={`absolute z-10 mt-1 max-h-48 w-36 overflow-auto rounded-md bg-white py-1 text-xs shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none ${
|
||||||
|
position === "left" ? "left-0" : "right-0"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{PRIORITIES?.map((priority) => (
|
||||||
|
<Listbox.Option
|
||||||
|
key={priority}
|
||||||
|
className={({ active }) =>
|
||||||
|
`flex cursor-pointer select-none items-center gap-x-2 px-3 py-2 capitalize ${
|
||||||
|
active ? "bg-indigo-50" : "bg-white"
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
value={priority}
|
||||||
|
>
|
||||||
|
{getPriorityIcon(priority, "text-sm")}
|
||||||
|
{priority ?? "None"}
|
||||||
|
</Listbox.Option>
|
||||||
|
))}
|
||||||
|
</Listbox.Options>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Listbox>
|
||||||
|
);
|
75
apps/app/components/issues/view-select/state.tsx
Normal file
75
apps/app/components/issues/view-select/state.tsx
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
// services
|
||||||
|
import stateService from "services/state.service";
|
||||||
|
// ui
|
||||||
|
import { CustomSelect } from "components/ui";
|
||||||
|
// helpers
|
||||||
|
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||||
|
// types
|
||||||
|
import { IIssue, IState } from "types";
|
||||||
|
// fetch-keys
|
||||||
|
import { STATE_LIST } from "constants/fetch-keys";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
issue: IIssue;
|
||||||
|
partialUpdateIssue: (formData: Partial<IIssue>) => void;
|
||||||
|
position?: "left" | "right";
|
||||||
|
isNotAllowed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ViewStateSelect: React.FC<Props> = ({
|
||||||
|
issue,
|
||||||
|
partialUpdateIssue,
|
||||||
|
position,
|
||||||
|
isNotAllowed,
|
||||||
|
}) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
|
const { data: states } = useSWR<IState[]>(
|
||||||
|
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
|
||||||
|
workspaceSlug
|
||||||
|
? () => stateService.getStates(workspaceSlug as string, projectId as string)
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CustomSelect
|
||||||
|
label={
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
||||||
|
style={{
|
||||||
|
backgroundColor: states?.find((s) => s.id === issue.state)?.color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{addSpaceIfCamelCase(states?.find((s) => s.id === issue.state)?.name ?? "")}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
value={issue.state}
|
||||||
|
onChange={(data: string) => {
|
||||||
|
partialUpdateIssue({ state: data });
|
||||||
|
}}
|
||||||
|
maxHeight="md"
|
||||||
|
noChevron
|
||||||
|
disabled={isNotAllowed}
|
||||||
|
>
|
||||||
|
{states?.map((state) => (
|
||||||
|
<CustomSelect.Option key={state.id} value={state.id}>
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
||||||
|
style={{
|
||||||
|
backgroundColor: state.color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{addSpaceIfCamelCase(state.name)}
|
||||||
|
</>
|
||||||
|
</CustomSelect.Option>
|
||||||
|
))}
|
||||||
|
</CustomSelect>
|
||||||
|
);
|
||||||
|
};
|
@ -9,7 +9,7 @@ import { Squares2X2Icon } from "@heroicons/react/24/outline";
|
|||||||
// types
|
// types
|
||||||
import type { IModule } from "types";
|
import type { IModule } from "types";
|
||||||
// constants
|
// constants
|
||||||
import { MODULE_STATUS } from "constants/";
|
import { MODULE_STATUS } from "constants/module";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
control: Control<IModule, any>;
|
control: Control<IModule, any>;
|
||||||
|
@ -10,7 +10,7 @@ import { CustomSelect } from "components/ui";
|
|||||||
import { IModule } from "types";
|
import { IModule } from "types";
|
||||||
// common
|
// common
|
||||||
// constants
|
// constants
|
||||||
import { MODULE_STATUS } from "constants/";
|
import { MODULE_STATUS } from "constants/module";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
control: Control<Partial<IModule>, any>;
|
control: Control<Partial<IModule>, any>;
|
||||||
|
@ -14,7 +14,7 @@ import { renderShortNumericDateFormat } from "helpers/date-time.helper";
|
|||||||
// types
|
// types
|
||||||
import { IModule, SelectModuleType } from "types";
|
import { IModule, SelectModuleType } from "types";
|
||||||
// common
|
// common
|
||||||
import { MODULE_STATUS } from "constants/";
|
import { MODULE_STATUS } from "constants/module";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
module: IModule;
|
module: IModule;
|
||||||
|
@ -16,10 +16,10 @@ import workspaceService from "services/workspace.service";
|
|||||||
import { CustomSelect, Input } from "components/ui";
|
import { CustomSelect, Input } from "components/ui";
|
||||||
// types
|
// types
|
||||||
import { IWorkspace, IWorkspaceMemberInvitation } from "types";
|
import { IWorkspace, IWorkspaceMemberInvitation } from "types";
|
||||||
// constants
|
|
||||||
import { companySize } from "constants/";
|
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys";
|
import { USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys";
|
||||||
|
// constants
|
||||||
|
import { COMPANY_SIZE } from "constants/workspace";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
setStep: React.Dispatch<React.SetStateAction<number>>;
|
setStep: React.Dispatch<React.SetStateAction<number>>;
|
||||||
@ -186,7 +186,7 @@ const Workspace: React.FC<Props> = ({ setStep, setWorkspace }) => {
|
|||||||
label={value ? value.toString() : "Select company size"}
|
label={value ? value.toString() : "Select company size"}
|
||||||
input
|
input
|
||||||
>
|
>
|
||||||
{companySize?.map((item) => (
|
{COMPANY_SIZE?.map((item) => (
|
||||||
<CustomSelect.Option key={item.value} value={item.value}>
|
<CustomSelect.Option key={item.value} value={item.value}>
|
||||||
{item.label}
|
{item.label}
|
||||||
</CustomSelect.Option>
|
</CustomSelect.Option>
|
||||||
|
@ -17,13 +17,13 @@ import { Button, Input, TextArea, CustomSelect } from "components/ui";
|
|||||||
// components
|
// components
|
||||||
import EmojiIconPicker from "components/emoji-icon-picker";
|
import EmojiIconPicker from "components/emoji-icon-picker";
|
||||||
// helpers
|
// helpers
|
||||||
import { getRandomEmoji } from "helpers/functions.helper";
|
import { getRandomEmoji } from "helpers/common.helper";
|
||||||
// types
|
// types
|
||||||
import { IProject } from "types";
|
import { IProject } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { PROJECTS_LIST, WORKSPACE_MEMBERS_ME } from "constants/fetch-keys";
|
import { PROJECTS_LIST, WORKSPACE_MEMBERS_ME } from "constants/fetch-keys";
|
||||||
// constants
|
// constants
|
||||||
import { NETWORK_CHOICES } from "constants/";
|
import { NETWORK_CHOICES } from "constants/project";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
@ -8,8 +8,8 @@ import { useForm, Controller } from "react-hook-form";
|
|||||||
|
|
||||||
import { Dialog, Transition, Listbox } from "@headlessui/react";
|
import { Dialog, Transition, Listbox } from "@headlessui/react";
|
||||||
// ui
|
// ui
|
||||||
import { ChevronDownIcon, CheckIcon } from "@heroicons/react/20/solid";
|
import { ChevronDownIcon } from "@heroicons/react/20/solid";
|
||||||
import { Button, CustomSelect, Select, TextArea } from "components/ui";
|
import { Button, CustomSelect, TextArea } from "components/ui";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// services
|
// services
|
||||||
@ -17,10 +17,10 @@ import projectService from "services/project.service";
|
|||||||
import workspaceService from "services/workspace.service";
|
import workspaceService from "services/workspace.service";
|
||||||
// types
|
// types
|
||||||
import { IProjectMemberInvitation } from "types";
|
import { IProjectMemberInvitation } from "types";
|
||||||
// constants
|
// fetch - keys
|
||||||
import { ROLE } from "constants/";
|
|
||||||
import { PROJECT_INVITATIONS, WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
import { PROJECT_INVITATIONS, WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
||||||
// icons
|
// constants
|
||||||
|
import { ROLE } from "constants/workspace";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
@ -21,7 +21,7 @@ type Props = {
|
|||||||
|
|
||||||
const defaultValues: Partial<IIssueLabels> = {
|
const defaultValues: Partial<IIssueLabels> = {
|
||||||
name: "",
|
name: "",
|
||||||
colour: "#ff0000",
|
color: "#ff0000",
|
||||||
};
|
};
|
||||||
|
|
||||||
const SingleLabel: React.FC<Props> = ({ label, issueLabels, editLabel, handleLabelDelete }) => {
|
const SingleLabel: React.FC<Props> = ({ label, issueLabels, editLabel, handleLabelDelete }) => {
|
||||||
@ -45,7 +45,7 @@ const SingleLabel: React.FC<Props> = ({ label, issueLabels, editLabel, handleLab
|
|||||||
<span
|
<span
|
||||||
className="h-3 w-3 flex-shrink-0 rounded-full"
|
className="h-3 w-3 flex-shrink-0 rounded-full"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: label.colour,
|
backgroundColor: label.color,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<h6 className="text-sm">{label.name}</h6>
|
<h6 className="text-sm">{label.name}</h6>
|
||||||
@ -68,11 +68,11 @@ const SingleLabel: React.FC<Props> = ({ label, issueLabels, editLabel, handleLab
|
|||||||
open ? "text-gray-900" : "text-gray-500"
|
open ? "text-gray-900" : "text-gray-500"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{watch("colour") && watch("colour") !== "" && (
|
{watch("color") && watch("color") !== "" && (
|
||||||
<span
|
<span
|
||||||
className="h-4 w-4 rounded"
|
className="h-4 w-4 rounded"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: watch("colour") ?? "green",
|
backgroundColor: watch("color") ?? "green",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -89,7 +89,7 @@ const SingleLabel: React.FC<Props> = ({ label, issueLabels, editLabel, handleLab
|
|||||||
>
|
>
|
||||||
<Popover.Panel className="absolute top-full left-0 z-20 mt-3 w-screen max-w-xs px-2 sm:px-0">
|
<Popover.Panel className="absolute top-full left-0 z-20 mt-3 w-screen max-w-xs px-2 sm:px-0">
|
||||||
<Controller
|
<Controller
|
||||||
name="colour"
|
name="color"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<TwitterPicker
|
<TwitterPicker
|
||||||
|
@ -143,27 +143,34 @@ export const ProjectSidebarList: FC = () => {
|
|||||||
sidebarCollapse ? "" : "ml-[2.25rem]"
|
sidebarCollapse ? "" : "ml-[2.25rem]"
|
||||||
} flex flex-col gap-y-1`}
|
} flex flex-col gap-y-1`}
|
||||||
>
|
>
|
||||||
{navigation(workspaceSlug as string, project?.id).map((item) => (
|
{navigation(workspaceSlug as string, project?.id).map((item) => {
|
||||||
<Link key={item.name} href={item.href}>
|
const hi = "hi";
|
||||||
<a
|
|
||||||
className={`group flex items-center rounded-md px-2 py-2 text-xs font-medium outline-none ${
|
if (item.name === "Cycles" && !project.cycle_view) return;
|
||||||
item.href === router.asPath
|
if (item.name === "Modules" && !project.module_view) return;
|
||||||
? "bg-gray-200 text-gray-900"
|
|
||||||
: "text-gray-500 hover:bg-gray-100 hover:text-gray-900 focus:bg-gray-100 focus:text-gray-900"
|
return (
|
||||||
} ${sidebarCollapse ? "justify-center" : ""}`}
|
<Link key={item.name} href={item.href}>
|
||||||
>
|
<a
|
||||||
<item.icon
|
className={`group flex items-center rounded-md px-2 py-2 text-xs font-medium outline-none ${
|
||||||
className={`h-4 w-4 flex-shrink-0 ${
|
|
||||||
item.href === router.asPath
|
item.href === router.asPath
|
||||||
? "text-gray-900"
|
? "bg-gray-200 text-gray-900"
|
||||||
: "text-gray-500 group-hover:text-gray-900"
|
: "text-gray-500 hover:bg-gray-100 hover:text-gray-900 focus:bg-gray-100 focus:text-gray-900"
|
||||||
} ${!sidebarCollapse ? "mr-3" : ""}`}
|
} ${sidebarCollapse ? "justify-center" : ""}`}
|
||||||
aria-hidden="true"
|
>
|
||||||
/>
|
<item.icon
|
||||||
{!sidebarCollapse && item.name}
|
className={`h-4 w-4 flex-shrink-0 ${
|
||||||
</a>
|
item.href === router.asPath
|
||||||
</Link>
|
? "text-gray-900"
|
||||||
))}
|
: "text-gray-500 group-hover:text-gray-900"
|
||||||
|
} ${!sidebarCollapse ? "mr-3" : ""}`}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
{!sidebarCollapse && item.name}
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</Disclosure.Panel>
|
</Disclosure.Panel>
|
||||||
</Transition>
|
</Transition>
|
||||||
</>
|
</>
|
||||||
|
@ -153,7 +153,7 @@ const RemirrorRichTextEditor: FC<IRemirrorRichTextEditor> = (props) => {
|
|||||||
(value: any) => {
|
(value: any) => {
|
||||||
// Clear out old state when setting data from outside
|
// Clear out old state when setting data from outside
|
||||||
// This prevents e.g. the user from using CTRL-Z to go back to the old state
|
// This prevents e.g. the user from using CTRL-Z to go back to the old state
|
||||||
manager.view.updateState(manager.createState({ content: value }));
|
manager.view.updateState(manager.createState({ content: value ? value : "" }));
|
||||||
},
|
},
|
||||||
[manager]
|
[manager]
|
||||||
);
|
);
|
||||||
|
@ -13,13 +13,13 @@ import stateService from "services/state.service";
|
|||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// ui
|
// ui
|
||||||
import { Button, CustomSelect, Input, Select } from "components/ui";
|
import { Button, CustomSelect, Input } from "components/ui";
|
||||||
// types
|
// types
|
||||||
import type { IState } from "types";
|
import type { IState } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { STATE_LIST } from "constants/fetch-keys";
|
import { STATE_LIST } from "constants/fetch-keys";
|
||||||
// constants
|
// constants
|
||||||
import { GROUP_CHOICES } from "constants/";
|
import { GROUP_CHOICES } from "constants/project";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
workspaceSlug?: string;
|
workspaceSlug?: string;
|
||||||
|
@ -21,7 +21,7 @@ import type { IState } from "types";
|
|||||||
// fetch keys
|
// fetch keys
|
||||||
import { STATE_LIST } from "constants/fetch-keys";
|
import { STATE_LIST } from "constants/fetch-keys";
|
||||||
// constants
|
// constants
|
||||||
import { GROUP_CHOICES } from "constants/";
|
import { GROUP_CHOICES } from "constants/project";
|
||||||
|
|
||||||
// types
|
// types
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -13,6 +13,8 @@ import useToast from "hooks/use-toast";
|
|||||||
import { IWorkspaceMemberInvitation } from "types";
|
import { IWorkspaceMemberInvitation } from "types";
|
||||||
// fetch keys
|
// fetch keys
|
||||||
import { WORKSPACE_INVITATIONS } from "constants/fetch-keys";
|
import { WORKSPACE_INVITATIONS } from "constants/fetch-keys";
|
||||||
|
// constants
|
||||||
|
import { ROLE } from "constants/workspace";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -21,13 +23,6 @@ type Props = {
|
|||||||
members: any[];
|
members: any[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const ROLE = {
|
|
||||||
5: "Guest",
|
|
||||||
10: "Viewer",
|
|
||||||
15: "Member",
|
|
||||||
20: "Admin",
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultValues: Partial<IWorkspaceMemberInvitation> = {
|
const defaultValues: Partial<IWorkspaceMemberInvitation> = {
|
||||||
email: "",
|
email: "",
|
||||||
role: 5,
|
role: 5,
|
||||||
|
@ -1,69 +0,0 @@
|
|||||||
import type { IIssue, NestedKeyOf } from "types";
|
|
||||||
|
|
||||||
export const PRIORITIES = ["urgent", "high", "medium", "low", null];
|
|
||||||
|
|
||||||
export const ROLE = {
|
|
||||||
5: "Guest",
|
|
||||||
10: "Viewer",
|
|
||||||
15: "Member",
|
|
||||||
20: "Admin",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const NETWORK_CHOICES = { "0": "Secret", "2": "Public" };
|
|
||||||
|
|
||||||
export const GROUP_CHOICES = {
|
|
||||||
backlog: "Backlog",
|
|
||||||
unstarted: "Unstarted",
|
|
||||||
started: "Started",
|
|
||||||
completed: "Completed",
|
|
||||||
cancelled: "Cancelled",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MODULE_STATUS = [
|
|
||||||
{ label: "Backlog", value: "backlog", color: "#5e6ad2" },
|
|
||||||
{ label: "Planned", value: "planned", color: "#26b5ce" },
|
|
||||||
{ label: "In Progress", value: "in-progress", color: "#f2c94c" },
|
|
||||||
{ label: "Paused", value: "paused", color: "#ff6900" },
|
|
||||||
{ label: "Completed", value: "completed", color: "#4cb782" },
|
|
||||||
{ label: "Cancelled", value: "cancelled", color: "#cc1d10" },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const groupByOptions: Array<{ name: string; key: NestedKeyOf<IIssue> | null }> = [
|
|
||||||
{ name: "State", key: "state_detail.name" },
|
|
||||||
{ name: "Priority", key: "priority" },
|
|
||||||
{ name: "Created By", key: "created_by" },
|
|
||||||
{ name: "None", key: null },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const orderByOptions: Array<{ name: string; key: NestedKeyOf<IIssue> | "manual" | null }> = [
|
|
||||||
// { name: "Manual", key: "manual" },
|
|
||||||
{ name: "Last created", key: "created_at" },
|
|
||||||
{ name: "Last updated", key: "updated_at" },
|
|
||||||
{ name: "Priority", key: "priority" },
|
|
||||||
{ name: "None", key: null },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const filterIssueOptions: Array<{
|
|
||||||
name: string;
|
|
||||||
key: "activeIssue" | "backlogIssue" | null;
|
|
||||||
}> = [
|
|
||||||
{
|
|
||||||
name: "All",
|
|
||||||
key: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Active Issues",
|
|
||||||
key: "activeIssue",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Backlog Issues",
|
|
||||||
key: "backlogIssue",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const companySize = [
|
|
||||||
{ value: 5, label: "5" },
|
|
||||||
{ value: 10, label: "10" },
|
|
||||||
{ value: 25, label: "25" },
|
|
||||||
{ value: 50, label: "50" },
|
|
||||||
];
|
|
36
apps/app/constants/issue.ts
Normal file
36
apps/app/constants/issue.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// types
|
||||||
|
import { IIssue, NestedKeyOf } from "types";
|
||||||
|
|
||||||
|
export const GROUP_BY_OPTIONS: Array<{ name: string; key: NestedKeyOf<IIssue> | null }> = [
|
||||||
|
{ name: "State", key: "state_detail.name" },
|
||||||
|
{ name: "Priority", key: "priority" },
|
||||||
|
{ name: "Created By", key: "created_by" },
|
||||||
|
{ name: "None", key: null },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const ORDER_BY_OPTIONS: Array<{ name: string; key: NestedKeyOf<IIssue> | "manual" | null }> =
|
||||||
|
[
|
||||||
|
// { name: "Manual", key: "manual" },
|
||||||
|
{ name: "Last created", key: "created_at" },
|
||||||
|
{ name: "Last updated", key: "updated_at" },
|
||||||
|
{ name: "Priority", key: "priority" },
|
||||||
|
// { name: "None", key: null },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const FILTER_ISSUE_OPTIONS: Array<{
|
||||||
|
name: string;
|
||||||
|
key: "activeIssue" | "backlogIssue" | null;
|
||||||
|
}> = [
|
||||||
|
{
|
||||||
|
name: "All",
|
||||||
|
key: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Active Issues",
|
||||||
|
key: "activeIssue",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Backlog Issues",
|
||||||
|
key: "backlogIssue",
|
||||||
|
},
|
||||||
|
];
|
8
apps/app/constants/module.ts
Normal file
8
apps/app/constants/module.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export const MODULE_STATUS = [
|
||||||
|
{ label: "Backlog", value: "backlog", color: "#5e6ad2" },
|
||||||
|
{ label: "Planned", value: "planned", color: "#26b5ce" },
|
||||||
|
{ label: "In Progress", value: "in-progress", color: "#f2c94c" },
|
||||||
|
{ label: "Paused", value: "paused", color: "#ff6900" },
|
||||||
|
{ label: "Completed", value: "completed", color: "#4cb782" },
|
||||||
|
{ label: "Cancelled", value: "cancelled", color: "#cc1d10" },
|
||||||
|
];
|
11
apps/app/constants/project.ts
Normal file
11
apps/app/constants/project.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export const NETWORK_CHOICES = { "0": "Secret", "2": "Public" };
|
||||||
|
|
||||||
|
export const GROUP_CHOICES = {
|
||||||
|
backlog: "Backlog",
|
||||||
|
unstarted: "Unstarted",
|
||||||
|
started: "Started",
|
||||||
|
completed: "Completed",
|
||||||
|
cancelled: "Cancelled",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PRIORITIES = ["urgent", "high", "medium", "low", null];
|
13
apps/app/constants/workspace.ts
Normal file
13
apps/app/constants/workspace.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
export const ROLE = {
|
||||||
|
5: "Guest",
|
||||||
|
10: "Viewer",
|
||||||
|
15: "Member",
|
||||||
|
20: "Admin",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const COMPANY_SIZE = [
|
||||||
|
{ value: 5, label: "5" },
|
||||||
|
{ value: 10, label: "10" },
|
||||||
|
{ value: 25, label: "25" },
|
||||||
|
{ value: 50, label: "50" },
|
||||||
|
];
|
@ -51,6 +51,7 @@ const useIssuesProperties = (workspaceSlug?: string, projectId?: string) => {
|
|||||||
const updateIssueProperties = useCallback(
|
const updateIssueProperties = useCallback(
|
||||||
(key: keyof Properties) => {
|
(key: keyof Properties) => {
|
||||||
if (!workspaceSlug || !user) return;
|
if (!workspaceSlug || !user) return;
|
||||||
|
|
||||||
setProperties((prev) => ({ ...prev, [key]: !prev[key] }));
|
setProperties((prev) => ({ ...prev, [key]: !prev[key] }));
|
||||||
if (issueProperties && projectId) {
|
if (issueProperties && projectId) {
|
||||||
mutateIssueProperties(
|
mutateIssueProperties(
|
||||||
|
@ -11,11 +11,11 @@ import { issueViewContext } from "contexts/issue-view.context";
|
|||||||
// helpers
|
// helpers
|
||||||
import { groupBy, orderArrayBy } from "helpers/array.helper";
|
import { groupBy, orderArrayBy } from "helpers/array.helper";
|
||||||
// types
|
// types
|
||||||
import { IIssue } from "types";
|
import { IIssue, IState } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { STATE_LIST } from "constants/fetch-keys";
|
import { STATE_LIST } from "constants/fetch-keys";
|
||||||
// constants
|
// constants
|
||||||
import { PRIORITIES } from "constants/";
|
import { PRIORITIES } from "constants/project";
|
||||||
|
|
||||||
const useIssueView = (projectIssues: IIssue[]) => {
|
const useIssueView = (projectIssues: IIssue[]) => {
|
||||||
const {
|
const {
|
||||||
@ -44,28 +44,52 @@ const useIssueView = (projectIssues: IIssue[]) => {
|
|||||||
|
|
||||||
let groupedByIssues: {
|
let groupedByIssues: {
|
||||||
[key: string]: IIssue[];
|
[key: string]: IIssue[];
|
||||||
} = {
|
} = {};
|
||||||
|
|
||||||
|
const groupIssues = (states: IState[], issues: IIssue[]) => ({
|
||||||
...(groupByProperty === "state_detail.name"
|
...(groupByProperty === "state_detail.name"
|
||||||
? Object.fromEntries(
|
? Object.fromEntries(
|
||||||
states
|
states
|
||||||
?.sort((a, b) => a.sequence - b.sequence)
|
?.sort((a, b) => a.sequence - b.sequence)
|
||||||
?.map((state) => [
|
?.map((state) => [
|
||||||
state.name,
|
state.name,
|
||||||
projectIssues.filter((issue) => issue.state === state.name) ?? [],
|
issues.filter((issue) => issue.state === state.name) ?? [],
|
||||||
]) ?? []
|
]) ?? []
|
||||||
)
|
)
|
||||||
: groupByProperty === "priority"
|
: groupByProperty === "priority"
|
||||||
? Object.fromEntries(
|
? Object.fromEntries(
|
||||||
PRIORITIES.map((priority) => [
|
PRIORITIES.map((priority) => [
|
||||||
priority,
|
priority,
|
||||||
projectIssues.filter((issue) => issue.priority === priority) ?? [],
|
issues.filter((issue) => issue.priority === priority) ?? [],
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
: {}),
|
: {}),
|
||||||
...groupBy(projectIssues ?? [], groupByProperty ?? ""),
|
...groupBy(issues ?? [], groupByProperty ?? ""),
|
||||||
};
|
});
|
||||||
|
|
||||||
if (groupByProperty === "priority") delete groupedByIssues.None;
|
groupedByIssues = groupIssues(states ?? [], projectIssues);
|
||||||
|
|
||||||
|
if (filterIssue) {
|
||||||
|
if (filterIssue === "activeIssue") {
|
||||||
|
const filteredStates = states?.filter(
|
||||||
|
(s) => s.group === "started" || s.group === "unstarted"
|
||||||
|
);
|
||||||
|
const filteredIssues = projectIssues.filter(
|
||||||
|
(i) => i.state_detail.group === "started" || i.state_detail.group === "unstarted"
|
||||||
|
);
|
||||||
|
|
||||||
|
groupedByIssues = groupIssues(filteredStates ?? [], filteredIssues);
|
||||||
|
} else if (filterIssue === "backlogIssue") {
|
||||||
|
const filteredStates = states?.filter(
|
||||||
|
(s) => s.group === "backlog" || s.group === "cancelled"
|
||||||
|
);
|
||||||
|
const filteredIssues = projectIssues.filter(
|
||||||
|
(i) => i.state_detail.group === "backlog" || i.state_detail.group === "cancelled"
|
||||||
|
);
|
||||||
|
|
||||||
|
groupedByIssues = groupIssues(filteredStates ?? [], filteredIssues);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (orderBy) {
|
if (orderBy) {
|
||||||
groupedByIssues = Object.fromEntries(
|
groupedByIssues = Object.fromEntries(
|
||||||
@ -76,36 +100,9 @@ const useIssueView = (projectIssues: IIssue[]) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filterIssue) {
|
if (groupByProperty === "priority") {
|
||||||
if (filterIssue === "activeIssue") {
|
delete groupedByIssues.None;
|
||||||
const filteredStates = states?.filter(
|
if (orderBy === "priority") setOrderBy("created_at");
|
||||||
(state) => state.group === "started" || state.group === "unstarted"
|
|
||||||
);
|
|
||||||
groupedByIssues = Object.fromEntries(
|
|
||||||
filteredStates
|
|
||||||
?.sort((a, b) => a.sequence - b.sequence)
|
|
||||||
?.map((state) => [
|
|
||||||
state.name,
|
|
||||||
projectIssues.filter((issue) => issue.state === state.id) ?? [],
|
|
||||||
]) ?? []
|
|
||||||
);
|
|
||||||
} else if (filterIssue === "backlogIssue") {
|
|
||||||
const filteredStates = states?.filter(
|
|
||||||
(state) => state.group === "backlog" || state.group === "cancelled"
|
|
||||||
);
|
|
||||||
groupedByIssues = Object.fromEntries(
|
|
||||||
filteredStates
|
|
||||||
?.sort((a, b) => a.sequence - b.sequence)
|
|
||||||
?.map((state) => [
|
|
||||||
state.name,
|
|
||||||
projectIssues.filter((issue) => issue.state === state.id) ?? [],
|
|
||||||
]) ?? []
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (groupByProperty === "priority" && orderBy === "priority") {
|
|
||||||
setOrderBy(null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -13,7 +13,7 @@ import { Properties, NestedKeyOf, IIssue } from "types";
|
|||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { STATE_LIST } from "constants/fetch-keys";
|
import { STATE_LIST } from "constants/fetch-keys";
|
||||||
// constants
|
// constants
|
||||||
import { PRIORITIES } from "constants/";
|
import { PRIORITIES } from "constants/project";
|
||||||
|
|
||||||
const initialValues: Properties = {
|
const initialValues: Properties = {
|
||||||
key: true,
|
key: true,
|
||||||
|
@ -1,30 +1,38 @@
|
|||||||
import React, { FC, useState } from "react";
|
import React, { FC, useState } from "react";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
|
||||||
// services
|
// services
|
||||||
import projectService from "services/project.service";
|
import projectService from "services/project.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useUser from "hooks/use-user";
|
import useUser from "hooks/use-user";
|
||||||
// ui
|
// ui
|
||||||
import { Spinner } from "components/ui";
|
import { Button, Spinner } from "components/ui";
|
||||||
// components
|
// components
|
||||||
|
import { NotAuthorizedView } from "components/core";
|
||||||
import CommandPalette from "components/command-palette";
|
import CommandPalette from "components/command-palette";
|
||||||
import { JoinProject } from "components/project";
|
import { JoinProject } from "components/project";
|
||||||
// local components
|
// local components
|
||||||
import Container from "layouts/container";
|
import Container from "layouts/container";
|
||||||
import AppSidebar from "./app-sidebar";
|
import AppSidebar from "layouts/app-layout/app-sidebar";
|
||||||
import AppHeader from "./app-header";
|
import AppHeader from "layouts/app-layout/app-header";
|
||||||
|
import SettingsSidebar from "layouts/settings-layout/settings-sidebar";
|
||||||
|
// types
|
||||||
|
import { UserAuth } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { PROJECT_MEMBERS } from "constants/fetch-keys";
|
import { PROJECT_MEMBERS } from "constants/fetch-keys";
|
||||||
|
|
||||||
export type Meta = {
|
type Meta = {
|
||||||
title?: string | null;
|
title?: string | null;
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
image?: string | null;
|
image?: string | null;
|
||||||
url?: string | null;
|
url?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface AppLayoutProps {
|
type AppLayoutProps = {
|
||||||
meta?: Meta;
|
meta?: Meta;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
noPadding?: boolean;
|
noPadding?: boolean;
|
||||||
@ -33,7 +41,60 @@ export interface AppLayoutProps {
|
|||||||
breadcrumbs?: JSX.Element;
|
breadcrumbs?: JSX.Element;
|
||||||
left?: JSX.Element;
|
left?: JSX.Element;
|
||||||
right?: JSX.Element;
|
right?: JSX.Element;
|
||||||
}
|
settingsLayout?: "project" | "workspace";
|
||||||
|
memberType?: UserAuth;
|
||||||
|
};
|
||||||
|
|
||||||
|
const workspaceLinks: (wSlug: string) => Array<{
|
||||||
|
label: string;
|
||||||
|
href: string;
|
||||||
|
}> = (workspaceSlug) => [
|
||||||
|
{
|
||||||
|
label: "General",
|
||||||
|
href: `/${workspaceSlug}/settings`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Members",
|
||||||
|
href: `/${workspaceSlug}/settings/members`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Billing & Plans",
|
||||||
|
href: `/${workspaceSlug}/settings/billing`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const sidebarLinks: (
|
||||||
|
wSlug?: string,
|
||||||
|
pId?: string
|
||||||
|
) => Array<{
|
||||||
|
label: string;
|
||||||
|
href: string;
|
||||||
|
}> = (workspaceSlug, projectId) => [
|
||||||
|
{
|
||||||
|
label: "General",
|
||||||
|
href: `/${workspaceSlug}/projects/${projectId}/settings`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Control",
|
||||||
|
href: `/${workspaceSlug}/projects/${projectId}/settings/control`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Members",
|
||||||
|
href: `/${workspaceSlug}/projects/${projectId}/settings/members`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Features",
|
||||||
|
href: `/${workspaceSlug}/projects/${projectId}/settings/features`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "States",
|
||||||
|
href: `/${workspaceSlug}/projects/${projectId}/settings/states`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Labels",
|
||||||
|
href: `/${workspaceSlug}/projects/${projectId}/settings/labels`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const AppLayout: FC<AppLayoutProps> = ({
|
const AppLayout: FC<AppLayoutProps> = ({
|
||||||
meta,
|
meta,
|
||||||
@ -44,16 +105,18 @@ const AppLayout: FC<AppLayoutProps> = ({
|
|||||||
breadcrumbs,
|
breadcrumbs,
|
||||||
left,
|
left,
|
||||||
right,
|
right,
|
||||||
|
settingsLayout,
|
||||||
|
memberType,
|
||||||
}) => {
|
}) => {
|
||||||
// states
|
// states
|
||||||
const [toggleSidebar, setToggleSidebar] = useState(false);
|
const [toggleSidebar, setToggleSidebar] = useState(false);
|
||||||
const [isJoiningProject, setIsJoiningProject] = useState(false);
|
const [isJoiningProject, setIsJoiningProject] = useState(false);
|
||||||
// router
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
// user info
|
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
// fetching Project Members information
|
|
||||||
const { data: projectMembers, mutate: projectMembersMutate } = useSWR(
|
const { data: projectMembers, mutate: projectMembersMutate } = useSWR(
|
||||||
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null,
|
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null,
|
||||||
workspaceSlug && projectId
|
workspaceSlug && projectId
|
||||||
@ -86,32 +149,71 @@ const AppLayout: FC<AppLayoutProps> = ({
|
|||||||
<CommandPalette />
|
<CommandPalette />
|
||||||
<div className="flex h-screen w-full overflow-x-hidden">
|
<div className="flex h-screen w-full overflow-x-hidden">
|
||||||
<AppSidebar toggleSidebar={toggleSidebar} setToggleSidebar={setToggleSidebar} />
|
<AppSidebar toggleSidebar={toggleSidebar} setToggleSidebar={setToggleSidebar} />
|
||||||
<main className="flex h-screen w-full min-w-0 flex-col overflow-y-auto">
|
{settingsLayout && (memberType?.isGuest || memberType?.isViewer) ? (
|
||||||
{!noHeader && (
|
<NotAuthorizedView
|
||||||
<AppHeader
|
actionButton={
|
||||||
breadcrumbs={breadcrumbs}
|
(memberType?.isViewer || memberType?.isGuest) && projectId ? (
|
||||||
left={left}
|
<Link href={`/${workspaceSlug}/projects/${projectId}/issues`}>
|
||||||
right={right}
|
<Button size="sm" theme="secondary">
|
||||||
setToggleSidebar={setToggleSidebar}
|
Go to Issues
|
||||||
/>
|
</Button>
|
||||||
)}
|
</Link>
|
||||||
|
) : (
|
||||||
{projectId && !projectMembers ? (
|
(memberType?.isViewer || memberType?.isGuest) &&
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
workspaceSlug && (
|
||||||
<Spinner />
|
<Link href={`/${workspaceSlug}`}>
|
||||||
</div>
|
<Button size="sm" theme="secondary">
|
||||||
) : isMember ? (
|
Go to workspace
|
||||||
<div
|
</Button>
|
||||||
className={`w-full flex-grow ${noPadding ? "" : "p-5"} ${
|
</Link>
|
||||||
bg === "primary" ? "bg-primary" : bg === "secondary" ? "bg-secondary" : "bg-primary"
|
)
|
||||||
}`}
|
)
|
||||||
>
|
}
|
||||||
{children}
|
/>
|
||||||
</div>
|
) : (
|
||||||
) : (
|
<>
|
||||||
<JoinProject isJoiningProject={isJoiningProject} handleJoin={handleJoin} />
|
{settingsLayout && (
|
||||||
)}
|
<SettingsSidebar
|
||||||
</main>
|
links={
|
||||||
|
settingsLayout === "workspace"
|
||||||
|
? workspaceLinks(workspaceSlug as string)
|
||||||
|
: sidebarLinks(workspaceSlug as string, projectId as string)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<main className="flex h-screen w-full min-w-0 flex-col overflow-y-auto">
|
||||||
|
{!noHeader && (
|
||||||
|
<AppHeader
|
||||||
|
breadcrumbs={breadcrumbs}
|
||||||
|
left={left}
|
||||||
|
right={right}
|
||||||
|
setToggleSidebar={setToggleSidebar}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{projectId && !projectMembers ? (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
) : isMember ? (
|
||||||
|
<div
|
||||||
|
className={`w-full flex-grow ${
|
||||||
|
noPadding ? "" : settingsLayout ? "p-5 pb-5 lg:px-16 lg:pt-10" : "p-5"
|
||||||
|
} ${
|
||||||
|
bg === "primary"
|
||||||
|
? "bg-primary"
|
||||||
|
: bg === "secondary"
|
||||||
|
? "bg-secondary"
|
||||||
|
: "bg-primary"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<JoinProject isJoiningProject={isJoiningProject} handleJoin={handleJoin} />
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
|
@ -2,8 +2,6 @@ import React from "react";
|
|||||||
// next
|
// next
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
// types
|
|
||||||
import type { Props } from "./types";
|
|
||||||
// constants
|
// constants
|
||||||
import {
|
import {
|
||||||
SITE_NAME,
|
SITE_NAME,
|
||||||
@ -14,6 +12,24 @@ import {
|
|||||||
SITE_TITLE,
|
SITE_TITLE,
|
||||||
} from "constants/seo-variables";
|
} from "constants/seo-variables";
|
||||||
|
|
||||||
|
type Meta = {
|
||||||
|
title?: string | null;
|
||||||
|
description?: string | null;
|
||||||
|
image?: string | null;
|
||||||
|
url?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
meta?: Meta;
|
||||||
|
children: React.ReactNode;
|
||||||
|
noPadding?: boolean;
|
||||||
|
bg?: "primary" | "secondary";
|
||||||
|
noHeader?: boolean;
|
||||||
|
breadcrumbs?: JSX.Element;
|
||||||
|
left?: JSX.Element;
|
||||||
|
right?: JSX.Element;
|
||||||
|
};
|
||||||
|
|
||||||
const Container = ({ meta, children }: Props) => {
|
const Container = ({ meta, children }: Props) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const image = meta?.image || "/site-image.png";
|
const image = meta?.image || "/site-image.png";
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
// layouts
|
|
||||||
import Container from "layouts/container";
|
|
||||||
// types
|
|
||||||
import type { Props } from "./types";
|
|
||||||
|
|
||||||
const DefaultLayout: React.FC<Props> = ({ meta, children }) => (
|
|
||||||
<Container meta={meta}>
|
|
||||||
<div className="w-full h-screen overflow-auto bg-gray-50">
|
|
||||||
<>{children}</>
|
|
||||||
</div>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default DefaultLayout;
|
|
30
apps/app/layouts/default-layout/index.tsx
Normal file
30
apps/app/layouts/default-layout/index.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
// layouts
|
||||||
|
import Container from "layouts/container";
|
||||||
|
|
||||||
|
type Meta = {
|
||||||
|
title?: string | null;
|
||||||
|
description?: string | null;
|
||||||
|
image?: string | null;
|
||||||
|
url?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
meta?: Meta;
|
||||||
|
children: React.ReactNode;
|
||||||
|
noPadding?: boolean;
|
||||||
|
bg?: "primary" | "secondary";
|
||||||
|
noHeader?: boolean;
|
||||||
|
breadcrumbs?: JSX.Element;
|
||||||
|
left?: JSX.Element;
|
||||||
|
right?: JSX.Element;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DefaultLayout: React.FC<Props> = ({ meta, children }) => (
|
||||||
|
<Container meta={meta}>
|
||||||
|
<div className="w-full h-screen overflow-auto bg-gray-50">
|
||||||
|
<>{children}</>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default DefaultLayout;
|
@ -1,37 +0,0 @@
|
|||||||
// ui
|
|
||||||
import { Button } from "components/ui";
|
|
||||||
// icons
|
|
||||||
import { Bars3Icon } from "@heroicons/react/24/outline";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
breadcrumbs?: JSX.Element;
|
|
||||||
left?: JSX.Element;
|
|
||||||
right?: JSX.Element;
|
|
||||||
setToggleSidebar: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Header: React.FC<Props> = ({ breadcrumbs, left, right, setToggleSidebar }) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="flex w-full flex-col gap-y-4 border-b border-gray-200 bg-gray-50 px-5 py-4 lg:flex-row lg:items-center lg:justify-between">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="block md:hidden">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
theme="secondary"
|
|
||||||
className="h-8 w-8"
|
|
||||||
onClick={() => setToggleSidebar((prevData) => !prevData)}
|
|
||||||
>
|
|
||||||
<Bars3Icon className="h-5 w-5" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{breadcrumbs}
|
|
||||||
{left}
|
|
||||||
</div>
|
|
||||||
{right}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Header;
|
|
@ -1,188 +0,0 @@
|
|||||||
import { useRef, useState } from "react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { Transition } from "@headlessui/react";
|
|
||||||
// hooks
|
|
||||||
import useTheme from "hooks/use-theme";
|
|
||||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
|
||||||
// components
|
|
||||||
import ProjectsList from "components/sidebar/projects-list";
|
|
||||||
import WorkspaceOptions from "components/sidebar/workspace-options";
|
|
||||||
// icons
|
|
||||||
import {
|
|
||||||
Cog6ToothIcon,
|
|
||||||
RectangleStackIcon,
|
|
||||||
ArrowLongLeftIcon,
|
|
||||||
RectangleGroupIcon,
|
|
||||||
} from "@heroicons/react/24/outline";
|
|
||||||
import {
|
|
||||||
CyclesIcon,
|
|
||||||
QuestionMarkCircleIcon,
|
|
||||||
BoltIcon,
|
|
||||||
DocumentIcon,
|
|
||||||
DiscordIcon,
|
|
||||||
GithubIcon,
|
|
||||||
CommentIcon,
|
|
||||||
} from "components/icons";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
toggleSidebar: boolean;
|
|
||||||
setToggleSidebar: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const helpOptions = [
|
|
||||||
{
|
|
||||||
name: "Documentation",
|
|
||||||
href: "https://docs.plane.so/",
|
|
||||||
Icon: DocumentIcon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Join our Discord",
|
|
||||||
href: "https://discord.com/invite/A92xrEGCge",
|
|
||||||
Icon: DiscordIcon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Report a bug",
|
|
||||||
href: "https://github.com/makeplane/plane/issues/new/choose",
|
|
||||||
Icon: GithubIcon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Chat with us",
|
|
||||||
href: "mailto:hello@plane.so",
|
|
||||||
Icon: CommentIcon,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const navigation = (workspaceSlug: string, projectId: string) => [
|
|
||||||
{
|
|
||||||
name: "Issues",
|
|
||||||
href: `/${workspaceSlug}/projects/${projectId}/issues`,
|
|
||||||
icon: RectangleStackIcon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Cycles",
|
|
||||||
href: `/${workspaceSlug}/projects/${projectId}/cycles`,
|
|
||||||
icon: CyclesIcon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Modules",
|
|
||||||
href: `/${workspaceSlug}/projects/${projectId}/modules`,
|
|
||||||
icon: RectangleGroupIcon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Settings",
|
|
||||||
href: `/${workspaceSlug}/projects/${projectId}/settings`,
|
|
||||||
icon: Cog6ToothIcon,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const Sidebar: React.FC<Props> = ({ toggleSidebar, setToggleSidebar }) => {
|
|
||||||
const helpOptionsRef = useRef<HTMLDivElement | null>(null);
|
|
||||||
|
|
||||||
const { collapsed: sidebarCollapse, toggleCollapsed } = useTheme();
|
|
||||||
|
|
||||||
useOutsideClickDetector(helpOptionsRef, () => setIsNeedHelpOpen(false));
|
|
||||||
|
|
||||||
const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false);
|
|
||||||
|
|
||||||
const helpOptionMode = sidebarCollapse ? "left-full" : "left-[-75px]";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<nav className="relative z-20 h-screen">
|
|
||||||
<div
|
|
||||||
className={`${sidebarCollapse ? "" : "w-auto md:w-60"} fixed inset-y-0 top-0 ${
|
|
||||||
toggleSidebar ? "left-0" : "-left-60 md:left-0"
|
|
||||||
} flex h-full flex-col bg-white duration-300 md:relative`}
|
|
||||||
>
|
|
||||||
<div className="flex h-full flex-1 flex-col border-r border-gray-200">
|
|
||||||
<div className="flex h-full flex-1 flex-col pt-2">
|
|
||||||
<WorkspaceOptions sidebarCollapse={sidebarCollapse} />
|
|
||||||
<ProjectsList navigation={navigation} sidebarCollapse={sidebarCollapse} />
|
|
||||||
<div
|
|
||||||
className={`flex w-full items-center justify-between self-baseline bg-primary px-2 py-2 ${
|
|
||||||
sidebarCollapse ? "flex-col-reverse" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`hidden items-center gap-3 rounded-md px-2 py-2 text-xs font-medium text-gray-500 outline-none hover:bg-gray-100 hover:text-gray-900 md:flex ${
|
|
||||||
sidebarCollapse ? "w-full justify-center" : ""
|
|
||||||
}`}
|
|
||||||
onClick={() => toggleCollapsed()}
|
|
||||||
>
|
|
||||||
<ArrowLongLeftIcon
|
|
||||||
className={`h-4 w-4 flex-shrink-0 text-gray-500 duration-300 group-hover:text-gray-900 ${
|
|
||||||
sidebarCollapse ? "rotate-180" : ""
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="flex items-center gap-3 rounded-md px-2 py-2 text-xs font-medium text-gray-500 outline-none hover:bg-gray-100 hover:text-gray-900 md:hidden"
|
|
||||||
onClick={() => setToggleSidebar(false)}
|
|
||||||
>
|
|
||||||
<ArrowLongLeftIcon className="h-4 w-4 flex-shrink-0 text-gray-500 group-hover:text-gray-900" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`flex items-center gap-x-1 rounded-md px-2 py-2 text-xs font-medium text-gray-500 outline-none hover:bg-gray-100 hover:text-gray-900 ${
|
|
||||||
sidebarCollapse ? "w-full justify-center" : ""
|
|
||||||
}`}
|
|
||||||
onClick={() => {
|
|
||||||
const e = new KeyboardEvent("keydown", {
|
|
||||||
key: "h",
|
|
||||||
});
|
|
||||||
document.dispatchEvent(e);
|
|
||||||
}}
|
|
||||||
title="Shortcuts"
|
|
||||||
>
|
|
||||||
<BoltIcon className="h-4 w-4 text-gray-500" />
|
|
||||||
{!sidebarCollapse && <span>Shortcuts</span>}
|
|
||||||
</button>
|
|
||||||
<div className="relative">
|
|
||||||
<Transition
|
|
||||||
show={isNeedHelpOpen}
|
|
||||||
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"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`absolute bottom-2 ${helpOptionMode} space-y-2 rounded-sm bg-white py-3 shadow-md`}
|
|
||||||
ref={helpOptionsRef}
|
|
||||||
>
|
|
||||||
{helpOptions.map(({ name, Icon, href }) => (
|
|
||||||
<Link href={href} key={name}>
|
|
||||||
<a
|
|
||||||
target="_blank"
|
|
||||||
className="mx-3 flex items-center gap-x-2 rounded-md whitespace-nowrap px-2 py-2 text-xs hover:bg-gray-100"
|
|
||||||
>
|
|
||||||
<Icon className="h-5 w-5 text-gray-500" />
|
|
||||||
<span className="text-sm">{name}</span>
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`flex items-center gap-x-1 rounded-md px-2 py-2 text-xs font-medium text-gray-500 outline-none hover:bg-gray-100 hover:text-gray-900 ${
|
|
||||||
sidebarCollapse ? "w-full justify-center" : ""
|
|
||||||
}`}
|
|
||||||
onClick={() => setIsNeedHelpOpen((prev) => !prev)}
|
|
||||||
title="Help"
|
|
||||||
>
|
|
||||||
<QuestionMarkCircleIcon className="h-4 w-4 text-gray-500" />
|
|
||||||
{!sidebarCollapse && <span>Help?</span>}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Sidebar;
|
|
@ -1,177 +0,0 @@
|
|||||||
import React, { useState } from "react";
|
|
||||||
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
|
|
||||||
// layouts
|
|
||||||
import Container from "layouts/container";
|
|
||||||
import Header from "layouts/navbar/header";
|
|
||||||
import Sidebar from "layouts/navbar/main-sidebar";
|
|
||||||
import SettingsSidebar from "layouts/navbar/settings-sidebar";
|
|
||||||
// components
|
|
||||||
import { NotAuthorizedView } from "components/core";
|
|
||||||
import CommandPalette from "components/command-palette";
|
|
||||||
// ui
|
|
||||||
import { Button } from "components/ui";
|
|
||||||
// types
|
|
||||||
import { Meta } from "./types";
|
|
||||||
import AppSidebar from "./app-layout/app-sidebar";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
meta?: Meta;
|
|
||||||
children: React.ReactNode;
|
|
||||||
noPadding?: boolean;
|
|
||||||
bg?: "primary" | "secondary";
|
|
||||||
noHeader?: boolean;
|
|
||||||
breadcrumbs?: JSX.Element;
|
|
||||||
left?: JSX.Element;
|
|
||||||
right?: JSX.Element;
|
|
||||||
type: "workspace" | "project";
|
|
||||||
memberType?: {
|
|
||||||
isMember: boolean;
|
|
||||||
isOwner: boolean;
|
|
||||||
isViewer: boolean;
|
|
||||||
isGuest: boolean;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const workspaceLinks: (wSlug: string) => Array<{
|
|
||||||
label: string;
|
|
||||||
href: string;
|
|
||||||
}> = (workspaceSlug) => [
|
|
||||||
{
|
|
||||||
label: "General",
|
|
||||||
href: `/${workspaceSlug}/settings`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Members",
|
|
||||||
href: `/${workspaceSlug}/settings/members`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Features",
|
|
||||||
href: `/${workspaceSlug}/settings/features`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Billing & Plans",
|
|
||||||
href: `/${workspaceSlug}/settings/billing`,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const sidebarLinks: (
|
|
||||||
wSlug?: string,
|
|
||||||
pId?: string
|
|
||||||
) => Array<{
|
|
||||||
label: string;
|
|
||||||
href: string;
|
|
||||||
}> = (workspaceSlug, projectId) => [
|
|
||||||
{
|
|
||||||
label: "General",
|
|
||||||
href: `/${workspaceSlug}/projects/${projectId}/settings`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Control",
|
|
||||||
href: `/${workspaceSlug}/projects/${projectId}/settings/control`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Members",
|
|
||||||
href: `/${workspaceSlug}/projects/${projectId}/settings/members`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "States",
|
|
||||||
href: `/${workspaceSlug}/projects/${projectId}/settings/states`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Labels",
|
|
||||||
href: `/${workspaceSlug}/projects/${projectId}/settings/labels`,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const SettingsLayout: React.FC<Props> = ({
|
|
||||||
meta,
|
|
||||||
children,
|
|
||||||
noPadding,
|
|
||||||
bg,
|
|
||||||
noHeader,
|
|
||||||
breadcrumbs,
|
|
||||||
left,
|
|
||||||
right,
|
|
||||||
type,
|
|
||||||
memberType,
|
|
||||||
}) => {
|
|
||||||
const [toggleSidebar, setToggleSidebar] = useState(false);
|
|
||||||
|
|
||||||
const { isMember, isOwner, isViewer, isGuest } = memberType ?? {
|
|
||||||
isMember: false,
|
|
||||||
isOwner: false,
|
|
||||||
isViewer: false,
|
|
||||||
isGuest: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const {
|
|
||||||
query: { workspaceSlug, projectId },
|
|
||||||
} = useRouter();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container meta={meta}>
|
|
||||||
<div className="flex h-screen w-full overflow-x-hidden">
|
|
||||||
<AppSidebar toggleSidebar={toggleSidebar} setToggleSidebar={setToggleSidebar} />
|
|
||||||
<CommandPalette />
|
|
||||||
{isMember || isOwner ? (
|
|
||||||
<>
|
|
||||||
<SettingsSidebar
|
|
||||||
links={
|
|
||||||
type === "workspace"
|
|
||||||
? workspaceLinks(workspaceSlug as string)
|
|
||||||
: sidebarLinks(workspaceSlug as string, projectId as string)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<main className="flex h-screen w-full min-w-0 flex-col overflow-y-auto">
|
|
||||||
{noHeader ? null : (
|
|
||||||
<Header
|
|
||||||
breadcrumbs={breadcrumbs}
|
|
||||||
left={left}
|
|
||||||
right={right}
|
|
||||||
setToggleSidebar={setToggleSidebar}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div
|
|
||||||
className={`w-full flex-grow ${noPadding ? "" : "p-5 pb-5 lg:px-16 lg:pt-10"} ${
|
|
||||||
bg === "primary"
|
|
||||||
? "bg-primary"
|
|
||||||
: bg === "secondary"
|
|
||||||
? "bg-secondary"
|
|
||||||
: "bg-primary"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<NotAuthorizedView
|
|
||||||
actionButton={
|
|
||||||
(isViewer || isGuest) && projectId ? (
|
|
||||||
<Link href={`/${workspaceSlug}/projects/${projectId}/issues`}>
|
|
||||||
<Button size="sm" theme="secondary">
|
|
||||||
Go to Issues
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
(isViewer || isGuest) &&
|
|
||||||
workspaceSlug && (
|
|
||||||
<Link href={`/${workspaceSlug}`}>
|
|
||||||
<Button size="sm" theme="secondary">
|
|
||||||
Go to workspace
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SettingsLayout;
|
|
@ -1,5 +1,4 @@
|
|||||||
// next
|
// next
|
||||||
import { ArrowLeftIcon } from "@heroicons/react/24/outline";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
17
apps/app/layouts/types.d.ts
vendored
17
apps/app/layouts/types.d.ts
vendored
@ -1,17 +0,0 @@
|
|||||||
export type Meta = {
|
|
||||||
title?: string | null;
|
|
||||||
description?: string | null;
|
|
||||||
image?: string | null;
|
|
||||||
url?: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Props = {
|
|
||||||
meta?: Meta;
|
|
||||||
children: React.ReactNode;
|
|
||||||
noPadding?: boolean;
|
|
||||||
bg?: "primary" | "secondary";
|
|
||||||
noHeader?: boolean;
|
|
||||||
breadcrumbs?: JSX.Element;
|
|
||||||
left?: JSX.Element;
|
|
||||||
right?: JSX.Element;
|
|
||||||
};
|
|
@ -1,34 +1,34 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
// lib
|
||||||
|
import { requiredAuth } from "lib/auth";
|
||||||
|
// layouts
|
||||||
|
import AppLayout from "layouts/app-layout";
|
||||||
|
// hooks
|
||||||
|
import useProjects from "hooks/use-projects";
|
||||||
|
import useWorkspaceDetails from "hooks/use-workspace-details";
|
||||||
|
import useIssues from "hooks/use-issues";
|
||||||
|
// components
|
||||||
|
import { WorkspaceHomeCardsList, WorkspaceHomeGreetings } from "components/workspace";
|
||||||
|
// ui
|
||||||
|
import { Spinner } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import {
|
import {
|
||||||
ArrowRightIcon,
|
ArrowRightIcon,
|
||||||
CalendarDaysIcon,
|
CalendarDaysIcon,
|
||||||
ClipboardDocumentListIcon,
|
ClipboardDocumentListIcon,
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
// lib
|
|
||||||
import { requiredAuth } from "lib/auth";
|
|
||||||
// layouts
|
|
||||||
import AppLayout from "layouts/app-layout";
|
|
||||||
// components
|
|
||||||
import { Spinner } from "components/ui";
|
|
||||||
import { WorkspaceHomeCardsList, WorkspaceHomeGreetings } from "components/workspace";
|
|
||||||
// hooks
|
|
||||||
import useProjects from "hooks/use-projects";
|
|
||||||
import useWorkspaceDetails from "hooks/use-workspace-details";
|
|
||||||
import useIssues from "hooks/use-issues";
|
|
||||||
// icons
|
|
||||||
import { LayerDiagonalIcon } from "components/icons";
|
import { LayerDiagonalIcon } from "components/icons";
|
||||||
|
import { getPriorityIcon } from "components/icons/priority-icon";
|
||||||
// helpers
|
// helpers
|
||||||
import { renderShortNumericDateFormat, findHowManyDaysLeft } from "helpers/date-time.helper";
|
import { renderShortNumericDateFormat, findHowManyDaysLeft } from "helpers/date-time.helper";
|
||||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||||
import { groupBy } from "helpers/array.helper";
|
import { groupBy } from "helpers/array.helper";
|
||||||
// types
|
// types
|
||||||
import type { NextPage, NextPageContext } from "next";
|
import type { NextPage, NextPageContext } from "next";
|
||||||
// constants
|
|
||||||
import { getPriorityIcon } from "constants/global";
|
|
||||||
|
|
||||||
const WorkspacePage: NextPage = () => {
|
const WorkspacePage: NextPage = () => {
|
||||||
// router
|
// router
|
||||||
|
@ -9,7 +9,7 @@ import { Controller, useForm } from "react-hook-form";
|
|||||||
// lib
|
// lib
|
||||||
import { requiredAdmin } from "lib/auth";
|
import { requiredAdmin } from "lib/auth";
|
||||||
// layouts
|
// layouts
|
||||||
import SettingsLayout from "layouts/settings-layout";
|
import AppLayout from "layouts/app-layout";
|
||||||
// services
|
// services
|
||||||
import projectService from "services/project.service";
|
import projectService from "services/project.service";
|
||||||
import workspaceService from "services/workspace.service";
|
import workspaceService from "services/workspace.service";
|
||||||
@ -103,8 +103,8 @@ const ControlSettings: NextPage<TControlSettingsProps> = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsLayout
|
<AppLayout
|
||||||
type="project"
|
settingsLayout="project"
|
||||||
memberType={{ isMember, isOwner, isViewer, isGuest }}
|
memberType={{ isMember, isOwner, isViewer, isGuest }}
|
||||||
breadcrumbs={
|
breadcrumbs={
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
@ -247,7 +247,7 @@ const ControlSettings: NextPage<TControlSettingsProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</SettingsLayout>
|
</AppLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -0,0 +1,187 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import useSWR, { mutate } from "swr";
|
||||||
|
|
||||||
|
// services
|
||||||
|
import projectService from "services/project.service";
|
||||||
|
// lib
|
||||||
|
import { requiredAdmin } from "lib/auth";
|
||||||
|
// layouts
|
||||||
|
import AppLayout from "layouts/app-layout";
|
||||||
|
// hooks
|
||||||
|
import useToast from "hooks/use-toast";
|
||||||
|
// ui
|
||||||
|
import { Button } from "components/ui";
|
||||||
|
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||||
|
// types
|
||||||
|
import { IProject, UserAuth } from "types";
|
||||||
|
import type { NextPage, NextPageContext } from "next";
|
||||||
|
// fetch-keys
|
||||||
|
import { PROJECTS_LIST, PROJECT_DETAILS } from "constants/fetch-keys";
|
||||||
|
|
||||||
|
const FeaturesSettings: NextPage<UserAuth> = (props) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
|
const { data: projectDetails } = useSWR(
|
||||||
|
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
|
||||||
|
workspaceSlug && projectId
|
||||||
|
? () => projectService.getProject(workspaceSlug as string, projectId as string)
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSubmit = async (formData: Partial<IProject>) => {
|
||||||
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
|
mutate<IProject>(
|
||||||
|
PROJECT_DETAILS(projectId as string),
|
||||||
|
(prevData) => ({ ...(prevData as IProject), ...formData }),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
mutate<IProject[]>(
|
||||||
|
PROJECTS_LIST(workspaceSlug as string),
|
||||||
|
(prevData) =>
|
||||||
|
prevData?.map((p) => {
|
||||||
|
if (p.id === projectId)
|
||||||
|
return {
|
||||||
|
...p,
|
||||||
|
...formData,
|
||||||
|
};
|
||||||
|
|
||||||
|
return p;
|
||||||
|
}),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
await projectService
|
||||||
|
.updateProject(workspaceSlug as string, projectId as string, formData)
|
||||||
|
.then((res) => {
|
||||||
|
mutate(PROJECT_DETAILS(projectId as string));
|
||||||
|
mutate(PROJECTS_LIST(workspaceSlug as string));
|
||||||
|
|
||||||
|
setToastAlert({
|
||||||
|
title: "Success!",
|
||||||
|
type: "success",
|
||||||
|
message: "Project features updated successfully.",
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppLayout
|
||||||
|
settingsLayout="project"
|
||||||
|
memberType={props}
|
||||||
|
breadcrumbs={
|
||||||
|
<Breadcrumbs>
|
||||||
|
<BreadcrumbItem
|
||||||
|
title={`${projectDetails?.name ?? "Project"}`}
|
||||||
|
link={`/${workspaceSlug}/projects/${projectDetails?.id}/issues`}
|
||||||
|
/>
|
||||||
|
<BreadcrumbItem title="Features Settings" />
|
||||||
|
</Breadcrumbs>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<section className="space-y-8">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-3xl font-bold leading-6 text-gray-900">Project Features</h3>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-8 md:w-2/3">
|
||||||
|
<div className="flex items-center justify-between gap-x-10 gap-y-2">
|
||||||
|
<div>
|
||||||
|
<h4 className="text-md mb-1 leading-6 text-gray-900">Use cycles</h4>
|
||||||
|
<p className="mb-3 text-sm text-gray-500">
|
||||||
|
Cycles are enabled for all the projects in this workspace. Access it from the
|
||||||
|
navigation bar.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
|
||||||
|
projectDetails?.cycle_view ? "bg-indigo-500" : "bg-gray-200"
|
||||||
|
}`}
|
||||||
|
role="switch"
|
||||||
|
aria-checked={projectDetails?.cycle_view}
|
||||||
|
onClick={() => handleSubmit({ cycle_view: !projectDetails?.cycle_view })}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Use cycles</span>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={`inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
|
||||||
|
projectDetails?.cycle_view ? "translate-x-5" : "translate-x-0"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between gap-x-10 gap-y-2">
|
||||||
|
<div>
|
||||||
|
<h4 className="text-md mb-1 leading-6 text-gray-900">Use modules</h4>
|
||||||
|
<p className="mb-3 text-sm text-gray-500">
|
||||||
|
Modules are enabled for all the projects in this workspace. Access it from the
|
||||||
|
navigation bar.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
|
||||||
|
projectDetails?.module_view ? "bg-indigo-500" : "bg-gray-200"
|
||||||
|
}`}
|
||||||
|
role="switch"
|
||||||
|
aria-checked={projectDetails?.module_view}
|
||||||
|
onClick={() => handleSubmit({ module_view: !projectDetails?.module_view })}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Use cycles</span>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={`inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
|
||||||
|
projectDetails?.module_view ? "translate-x-5" : "translate-x-0"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<a href="https://plane.so/" target="_blank" rel="noreferrer">
|
||||||
|
<Button theme="secondary" size="rg" className="text-xs">
|
||||||
|
Plane is open-source, view Roadmap
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/makeplane/plane" target="_blank" rel="noreferrer">
|
||||||
|
<Button theme="secondary" size="rg" className="text-xs">
|
||||||
|
Star us on GitHub
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</AppLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getServerSideProps = async (ctx: NextPageContext) => {
|
||||||
|
const projectId = ctx.query.projectId as string;
|
||||||
|
const workspaceSlug = ctx.query.workspaceSlug as string;
|
||||||
|
|
||||||
|
const memberDetail = await requiredAdmin(workspaceSlug, projectId, ctx.req?.headers.cookie);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
isOwner: memberDetail?.role === 20,
|
||||||
|
isMember: memberDetail?.role === 15,
|
||||||
|
isViewer: memberDetail?.role === 10,
|
||||||
|
isGuest: memberDetail?.role === 5,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FeaturesSettings;
|
@ -6,11 +6,11 @@ import useSWR, { mutate } from "swr";
|
|||||||
|
|
||||||
// react-hook-form
|
// react-hook-form
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { IProject, IWorkspace } from "types";
|
import { IProject, IWorkspace, UserAuth } from "types";
|
||||||
// lib
|
// lib
|
||||||
import { requiredAdmin } from "lib/auth";
|
import { requiredAdmin } from "lib/auth";
|
||||||
// layouts
|
// layouts
|
||||||
import SettingsLayout from "layouts/settings-layout";
|
import AppLayout from "layouts/app-layout";
|
||||||
// services
|
// services
|
||||||
import projectService from "services/project.service";
|
import projectService from "services/project.service";
|
||||||
import workspaceService from "services/workspace.service";
|
import workspaceService from "services/workspace.service";
|
||||||
@ -24,13 +24,13 @@ import { Button, Input, TextArea, Loader, CustomSelect } from "components/ui";
|
|||||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||||
import OutlineButton from "components/ui/outline-button";
|
import OutlineButton from "components/ui/outline-button";
|
||||||
// helpers
|
// helpers
|
||||||
import { debounce } from "helpers/functions.helper";
|
import { debounce } from "helpers/common.helper";
|
||||||
// types
|
// types
|
||||||
import type { NextPage, NextPageContext } from "next";
|
import type { NextPage, NextPageContext } from "next";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { PROJECTS_LIST, PROJECT_DETAILS, WORKSPACE_DETAILS } from "constants/fetch-keys";
|
import { PROJECTS_LIST, PROJECT_DETAILS, WORKSPACE_DETAILS } from "constants/fetch-keys";
|
||||||
// constants
|
// constants
|
||||||
import { NETWORK_CHOICES } from "constants/";
|
import { NETWORK_CHOICES } from "constants/project";
|
||||||
|
|
||||||
const defaultValues: Partial<IProject> = {
|
const defaultValues: Partial<IProject> = {
|
||||||
name: "",
|
name: "",
|
||||||
@ -39,14 +39,7 @@ const defaultValues: Partial<IProject> = {
|
|||||||
network: 0,
|
network: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
type TGeneralSettingsProps = {
|
const GeneralSettings: NextPage<UserAuth> = (props) => {
|
||||||
isMember: boolean;
|
|
||||||
isOwner: boolean;
|
|
||||||
isViewer: boolean;
|
|
||||||
isGuest: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const GeneralSettings: NextPage<TGeneralSettingsProps> = (props) => {
|
|
||||||
const { isMember, isOwner, isViewer, isGuest } = props;
|
const { isMember, isOwner, isViewer, isGuest } = props;
|
||||||
|
|
||||||
const [selectProject, setSelectedProject] = useState<string | null>(null);
|
const [selectProject, setSelectedProject] = useState<string | null>(null);
|
||||||
@ -100,6 +93,7 @@ const GeneralSettings: NextPage<TGeneralSettingsProps> = (props) => {
|
|||||||
|
|
||||||
const onSubmit = async (formData: IProject) => {
|
const onSubmit = async (formData: IProject) => {
|
||||||
if (!activeWorkspace || !projectDetails) return;
|
if (!activeWorkspace || !projectDetails) return;
|
||||||
|
|
||||||
const payload: Partial<IProject> = {
|
const payload: Partial<IProject> = {
|
||||||
name: formData.name,
|
name: formData.name,
|
||||||
network: formData.network,
|
network: formData.network,
|
||||||
@ -109,6 +103,7 @@ const GeneralSettings: NextPage<TGeneralSettingsProps> = (props) => {
|
|||||||
project_lead: formData.project_lead,
|
project_lead: formData.project_lead,
|
||||||
icon: formData.icon,
|
icon: formData.icon,
|
||||||
};
|
};
|
||||||
|
|
||||||
await projectService
|
await projectService
|
||||||
.updateProject(activeWorkspace.slug, projectDetails.id, payload)
|
.updateProject(activeWorkspace.slug, projectDetails.id, payload)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
@ -130,9 +125,9 @@ const GeneralSettings: NextPage<TGeneralSettingsProps> = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsLayout
|
<AppLayout
|
||||||
|
settingsLayout="project"
|
||||||
memberType={{ isMember, isOwner, isViewer, isGuest }}
|
memberType={{ isMember, isOwner, isViewer, isGuest }}
|
||||||
type="project"
|
|
||||||
breadcrumbs={
|
breadcrumbs={
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
<BreadcrumbItem
|
<BreadcrumbItem
|
||||||
@ -341,7 +336,7 @@ const GeneralSettings: NextPage<TGeneralSettingsProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</SettingsLayout>
|
</AppLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,11 +1,15 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
// react-hook-form
|
||||||
import { Controller, SubmitHandler, useForm } from "react-hook-form";
|
import { Controller, SubmitHandler, useForm } from "react-hook-form";
|
||||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
// react-color
|
||||||
import { Popover, Transition } from "@headlessui/react";
|
|
||||||
import { TwitterPicker } from "react-color";
|
import { TwitterPicker } from "react-color";
|
||||||
import type { NextPageContext, NextPage } from "next";
|
// headless ui
|
||||||
|
import { Popover, Transition } from "@headlessui/react";
|
||||||
// services
|
// services
|
||||||
import projectService from "services/project.service";
|
import projectService from "services/project.service";
|
||||||
import workspaceService from "services/workspace.service";
|
import workspaceService from "services/workspace.service";
|
||||||
@ -13,20 +17,23 @@ import issuesService from "services/issues.service";
|
|||||||
// lib
|
// lib
|
||||||
import { requiredAdmin } from "lib/auth";
|
import { requiredAdmin } from "lib/auth";
|
||||||
// layouts
|
// layouts
|
||||||
import SettingsLayout from "layouts/settings-layout";
|
import AppLayout from "layouts/app-layout";
|
||||||
// components
|
// components
|
||||||
import SingleLabel from "components/project/settings/single-label";
|
import SingleLabel from "components/project/settings/single-label";
|
||||||
// ui
|
// ui
|
||||||
import { Button, Input, Loader } from "components/ui";
|
import { Button, Input, Loader } from "components/ui";
|
||||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||||
// fetch-keys
|
// icons
|
||||||
import { PROJECT_DETAILS, PROJECT_ISSUE_LABELS, WORKSPACE_DETAILS } from "constants/fetch-keys";
|
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||||
// types
|
// types
|
||||||
import { IIssueLabels } from "types";
|
import { IIssueLabels } from "types";
|
||||||
|
import type { NextPageContext, NextPage } from "next";
|
||||||
|
// fetch-keys
|
||||||
|
import { PROJECT_DETAILS, PROJECT_ISSUE_LABELS, WORKSPACE_DETAILS } from "constants/fetch-keys";
|
||||||
|
|
||||||
const defaultValues: Partial<IIssueLabels> = {
|
const defaultValues: Partial<IIssueLabels> = {
|
||||||
name: "",
|
name: "",
|
||||||
colour: "#ff0000",
|
color: "#ff0000",
|
||||||
};
|
};
|
||||||
|
|
||||||
type TLabelSettingsProps = {
|
type TLabelSettingsProps = {
|
||||||
@ -52,7 +59,7 @@ const LabelsSettings: NextPage<TLabelSettingsProps> = (props) => {
|
|||||||
() => (workspaceSlug ? workspaceService.getWorkspace(workspaceSlug as string) : null)
|
() => (workspaceSlug ? workspaceService.getWorkspace(workspaceSlug as string) : null)
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: activeProject } = useSWR(
|
const { data: projectDetails } = useSWR(
|
||||||
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
|
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
|
||||||
workspaceSlug && projectId
|
workspaceSlug && projectId
|
||||||
? () => projectService.getProject(workspaceSlug as string, projectId as string)
|
? () => projectService.getProject(workspaceSlug as string, projectId as string)
|
||||||
@ -77,9 +84,9 @@ const LabelsSettings: NextPage<TLabelSettingsProps> = (props) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleNewLabel: SubmitHandler<IIssueLabels> = async (formData) => {
|
const handleNewLabel: SubmitHandler<IIssueLabels> = async (formData) => {
|
||||||
if (!activeWorkspace || !activeProject || isSubmitting) return;
|
if (!activeWorkspace || !projectDetails || isSubmitting) return;
|
||||||
await issuesService
|
await issuesService
|
||||||
.createIssueLabel(activeWorkspace.slug, activeProject.id, formData)
|
.createIssueLabel(activeWorkspace.slug, projectDetails.id, formData)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
reset(defaultValues);
|
reset(defaultValues);
|
||||||
mutate((prevData) => [...(prevData ?? []), res], false);
|
mutate((prevData) => [...(prevData ?? []), res], false);
|
||||||
@ -89,16 +96,17 @@ const LabelsSettings: NextPage<TLabelSettingsProps> = (props) => {
|
|||||||
|
|
||||||
const editLabel = (label: IIssueLabels) => {
|
const editLabel = (label: IIssueLabels) => {
|
||||||
setNewLabelForm(true);
|
setNewLabelForm(true);
|
||||||
setValue("colour", label.colour);
|
setValue("color", label.color);
|
||||||
setValue("name", label.name);
|
setValue("name", label.name);
|
||||||
setIsUpdating(true);
|
setIsUpdating(true);
|
||||||
setLabelIdForUpdate(label.id);
|
setLabelIdForUpdate(label.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLabelUpdate: SubmitHandler<IIssueLabels> = async (formData) => {
|
const handleLabelUpdate: SubmitHandler<IIssueLabels> = async (formData) => {
|
||||||
if (!activeWorkspace || !activeProject || isSubmitting) return;
|
if (!activeWorkspace || !projectDetails || isSubmitting) return;
|
||||||
|
|
||||||
await issuesService
|
await issuesService
|
||||||
.patchIssueLabel(activeWorkspace.slug, activeProject.id, labelIdForUpdate ?? "", formData)
|
.patchIssueLabel(activeWorkspace.slug, projectDetails.id, labelIdForUpdate ?? "", formData)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
console.log(res);
|
console.log(res);
|
||||||
reset(defaultValues);
|
reset(defaultValues);
|
||||||
@ -112,10 +120,10 @@ const LabelsSettings: NextPage<TLabelSettingsProps> = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleLabelDelete = (labelId: string) => {
|
const handleLabelDelete = (labelId: string) => {
|
||||||
if (activeWorkspace && activeProject) {
|
if (activeWorkspace && projectDetails) {
|
||||||
mutate((prevData) => prevData?.filter((p) => p.id !== labelId), false);
|
mutate((prevData) => prevData?.filter((p) => p.id !== labelId), false);
|
||||||
issuesService
|
issuesService
|
||||||
.deleteIssueLabel(activeWorkspace.slug, activeProject.id, labelId)
|
.deleteIssueLabel(activeWorkspace.slug, projectDetails.id, labelId)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
console.log(res);
|
console.log(res);
|
||||||
})
|
})
|
||||||
@ -126,14 +134,14 @@ const LabelsSettings: NextPage<TLabelSettingsProps> = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsLayout
|
<AppLayout
|
||||||
type="project"
|
settingsLayout="project"
|
||||||
memberType={{ isMember, isOwner, isViewer, isGuest }}
|
memberType={{ isMember, isOwner, isViewer, isGuest }}
|
||||||
breadcrumbs={
|
breadcrumbs={
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
<BreadcrumbItem
|
<BreadcrumbItem
|
||||||
title={`${activeProject?.name ?? "Project"}`}
|
title={`${projectDetails?.name ?? "Project"}`}
|
||||||
link={`/${workspaceSlug}/projects/${activeProject?.id}/issues`}
|
link={`/${workspaceSlug}/projects/${projectDetails?.id}/issues`}
|
||||||
/>
|
/>
|
||||||
<BreadcrumbItem title="Labels Settings" />
|
<BreadcrumbItem title="Labels Settings" />
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
@ -170,13 +178,13 @@ const LabelsSettings: NextPage<TLabelSettingsProps> = (props) => {
|
|||||||
open ? "text-gray-900" : "text-gray-500"
|
open ? "text-gray-900" : "text-gray-500"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{watch("colour") && watch("colour") !== "" && (
|
{watch("color") && watch("color") !== "" && (
|
||||||
<span
|
<span
|
||||||
className="h-4 w-4 rounded"
|
className="h-4 w-4 rounded"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: watch("colour") ?? "green",
|
backgroundColor: watch("color") ?? "green",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Popover.Button>
|
</Popover.Button>
|
||||||
|
|
||||||
@ -191,7 +199,7 @@ const LabelsSettings: NextPage<TLabelSettingsProps> = (props) => {
|
|||||||
>
|
>
|
||||||
<Popover.Panel className="absolute top-full left-0 z-20 mt-3 w-screen max-w-xs px-2 sm:px-0">
|
<Popover.Panel className="absolute top-full left-0 z-20 mt-3 w-screen max-w-xs px-2 sm:px-0">
|
||||||
<Controller
|
<Controller
|
||||||
name="colour"
|
name="color"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<TwitterPicker
|
<TwitterPicker
|
||||||
@ -258,7 +266,7 @@ const LabelsSettings: NextPage<TLabelSettingsProps> = (props) => {
|
|||||||
</>
|
</>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</SettingsLayout>
|
</AppLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2,9 +2,8 @@ import { useState } from "react";
|
|||||||
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
|
||||||
import type { NextPage, NextPageContext } from "next";
|
|
||||||
|
|
||||||
// services
|
// services
|
||||||
import projectService from "services/project.service";
|
import projectService from "services/project.service";
|
||||||
@ -13,10 +12,8 @@ import workspaceService from "services/workspace.service";
|
|||||||
import { requiredAdmin } from "lib/auth";
|
import { requiredAdmin } from "lib/auth";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// constants
|
|
||||||
import { ROLE } from "constants/";
|
|
||||||
// layouts
|
// layouts
|
||||||
import SettingsLayout from "layouts/settings-layout";
|
import AppLayout from "layouts/app-layout";
|
||||||
// components
|
// components
|
||||||
import ConfirmProjectMemberRemove from "components/project/confirm-project-member-remove";
|
import ConfirmProjectMemberRemove from "components/project/confirm-project-member-remove";
|
||||||
import SendProjectInvitationModal from "components/project/send-project-invitation-modal";
|
import SendProjectInvitationModal from "components/project/send-project-invitation-modal";
|
||||||
@ -24,6 +21,9 @@ import SendProjectInvitationModal from "components/project/send-project-invitati
|
|||||||
import { Button, CustomListbox, CustomMenu, Loader } from "components/ui";
|
import { Button, CustomListbox, CustomMenu, Loader } from "components/ui";
|
||||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||||
// icons
|
// icons
|
||||||
|
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||||
|
// types
|
||||||
|
import type { NextPage, NextPageContext } from "next";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import {
|
import {
|
||||||
PROJECT_DETAILS,
|
PROJECT_DETAILS,
|
||||||
@ -31,6 +31,8 @@ import {
|
|||||||
PROJECT_MEMBERS,
|
PROJECT_MEMBERS,
|
||||||
WORKSPACE_DETAILS,
|
WORKSPACE_DETAILS,
|
||||||
} from "constants/fetch-keys";
|
} from "constants/fetch-keys";
|
||||||
|
// constants
|
||||||
|
import { ROLE } from "constants/workspace";
|
||||||
|
|
||||||
type TMemberSettingsProps = {
|
type TMemberSettingsProps = {
|
||||||
isMember: boolean;
|
isMember: boolean;
|
||||||
@ -58,7 +60,7 @@ const MembersSettings: NextPage<TMemberSettingsProps> = (props) => {
|
|||||||
() => (workspaceSlug ? workspaceService.getWorkspace(workspaceSlug as string) : null)
|
() => (workspaceSlug ? workspaceService.getWorkspace(workspaceSlug as string) : null)
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: activeProject } = useSWR(
|
const { data: projectDetails } = useSWR(
|
||||||
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
|
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
|
||||||
workspaceSlug && projectId
|
workspaceSlug && projectId
|
||||||
? () => projectService.getProject(workspaceSlug as string, projectId as string)
|
? () => projectService.getProject(workspaceSlug as string, projectId as string)
|
||||||
@ -120,11 +122,11 @@ const MembersSettings: NextPage<TMemberSettingsProps> = (props) => {
|
|||||||
(item) => item.id === selectedRemoveMember || item.id === selectedInviteRemoveMember
|
(item) => item.id === selectedRemoveMember || item.id === selectedInviteRemoveMember
|
||||||
)}
|
)}
|
||||||
handleDelete={async () => {
|
handleDelete={async () => {
|
||||||
if (!activeWorkspace || !activeProject) return;
|
if (!activeWorkspace || !projectDetails) return;
|
||||||
if (selectedRemoveMember) {
|
if (selectedRemoveMember) {
|
||||||
await projectService.deleteProjectMember(
|
await projectService.deleteProjectMember(
|
||||||
activeWorkspace.slug,
|
activeWorkspace.slug,
|
||||||
activeProject.id,
|
projectDetails.id,
|
||||||
selectedRemoveMember
|
selectedRemoveMember
|
||||||
);
|
);
|
||||||
mutateMembers(
|
mutateMembers(
|
||||||
@ -135,7 +137,7 @@ const MembersSettings: NextPage<TMemberSettingsProps> = (props) => {
|
|||||||
if (selectedInviteRemoveMember) {
|
if (selectedInviteRemoveMember) {
|
||||||
await projectService.deleteProjectInvitation(
|
await projectService.deleteProjectInvitation(
|
||||||
activeWorkspace.slug,
|
activeWorkspace.slug,
|
||||||
activeProject.id,
|
projectDetails.id,
|
||||||
selectedInviteRemoveMember
|
selectedInviteRemoveMember
|
||||||
);
|
);
|
||||||
mutateInvitations(
|
mutateInvitations(
|
||||||
@ -155,14 +157,14 @@ const MembersSettings: NextPage<TMemberSettingsProps> = (props) => {
|
|||||||
setIsOpen={setInviteModal}
|
setIsOpen={setInviteModal}
|
||||||
members={members}
|
members={members}
|
||||||
/>
|
/>
|
||||||
<SettingsLayout
|
<AppLayout
|
||||||
type="project"
|
settingsLayout="project"
|
||||||
memberType={{ isMember, isOwner, isViewer, isGuest }}
|
memberType={{ isMember, isOwner, isViewer, isGuest }}
|
||||||
breadcrumbs={
|
breadcrumbs={
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
<BreadcrumbItem
|
<BreadcrumbItem
|
||||||
title={`${activeProject?.name ?? "Project"}`}
|
title={`${projectDetails?.name ?? "Project"}`}
|
||||||
link={`/${workspaceSlug}/projects/${activeProject?.id}/issues`}
|
link={`/${workspaceSlug}/projects/${projectDetails?.id}/issues`}
|
||||||
/>
|
/>
|
||||||
<BreadcrumbItem title="Members Settings" />
|
<BreadcrumbItem title="Members Settings" />
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
@ -235,11 +237,11 @@ const MembersSettings: NextPage<TMemberSettingsProps> = (props) => {
|
|||||||
title={ROLE[member.role as keyof typeof ROLE] ?? "Select Role"}
|
title={ROLE[member.role as keyof typeof ROLE] ?? "Select Role"}
|
||||||
value={member.role}
|
value={member.role}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
if (!activeWorkspace || !activeProject) return;
|
if (!activeWorkspace || !projectDetails) return;
|
||||||
projectService
|
projectService
|
||||||
.updateProjectMember(
|
.updateProjectMember(
|
||||||
activeWorkspace.slug,
|
activeWorkspace.slug,
|
||||||
activeProject.id,
|
projectDetails.id,
|
||||||
member.id,
|
member.id,
|
||||||
{
|
{
|
||||||
role: value,
|
role: value,
|
||||||
@ -306,7 +308,7 @@ const MembersSettings: NextPage<TMemberSettingsProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
</SettingsLayout>
|
</AppLayout>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -11,7 +11,7 @@ import projectService from "services/project.service";
|
|||||||
// lib
|
// lib
|
||||||
import { requiredAdmin } from "lib/auth";
|
import { requiredAdmin } from "lib/auth";
|
||||||
// layouts
|
// layouts
|
||||||
import SettingsLayout from "layouts/settings-layout";
|
import AppLayout from "layouts/app-layout";
|
||||||
// components
|
// components
|
||||||
import { CreateUpdateStateInline, DeleteStateModal, StateGroup } from "components/states";
|
import { CreateUpdateStateInline, DeleteStateModal, StateGroup } from "components/states";
|
||||||
// ui
|
// ui
|
||||||
@ -43,7 +43,7 @@ const StatesSettings: NextPage<TStateSettingsProps> = (props) => {
|
|||||||
query: { workspaceSlug, projectId },
|
query: { workspaceSlug, projectId },
|
||||||
} = useRouter();
|
} = useRouter();
|
||||||
|
|
||||||
const { data: activeProject } = useSWR(
|
const { data: projectDetails } = useSWR(
|
||||||
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
|
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
|
||||||
workspaceSlug && projectId
|
workspaceSlug && projectId
|
||||||
? () => projectService.getProject(workspaceSlug as string, projectId as string)
|
? () => projectService.getProject(workspaceSlug as string, projectId as string)
|
||||||
@ -68,14 +68,14 @@ const StatesSettings: NextPage<TStateSettingsProps> = (props) => {
|
|||||||
data={states?.find((state) => state.id === selectDeleteState) ?? null}
|
data={states?.find((state) => state.id === selectDeleteState) ?? null}
|
||||||
onClose={() => setSelectDeleteState(null)}
|
onClose={() => setSelectDeleteState(null)}
|
||||||
/>
|
/>
|
||||||
<SettingsLayout
|
<AppLayout
|
||||||
type="project"
|
settingsLayout="project"
|
||||||
memberType={{ isMember, isOwner, isViewer, isGuest }}
|
memberType={{ isMember, isOwner, isViewer, isGuest }}
|
||||||
breadcrumbs={
|
breadcrumbs={
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
<BreadcrumbItem
|
<BreadcrumbItem
|
||||||
title={`${activeProject?.name ?? "Project"}`}
|
title={`${projectDetails?.name ?? "Project"}`}
|
||||||
link={`/${workspaceSlug}/projects/${activeProject?.id}/issues`}
|
link={`/${workspaceSlug}/projects/${projectDetails?.id}/issues`}
|
||||||
/>
|
/>
|
||||||
<BreadcrumbItem title="States Settings" />
|
<BreadcrumbItem title="States Settings" />
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
@ -87,7 +87,7 @@ const StatesSettings: NextPage<TStateSettingsProps> = (props) => {
|
|||||||
<p className="mt-4 text-sm text-gray-500">Manage the state of this project.</p>
|
<p className="mt-4 text-sm text-gray-500">Manage the state of this project.</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col justify-between gap-4">
|
<div className="flex flex-col justify-between gap-4">
|
||||||
{states && activeProject ? (
|
{states && projectDetails ? (
|
||||||
Object.keys(groupedStates).map((key) => (
|
Object.keys(groupedStates).map((key) => (
|
||||||
<div key={key}>
|
<div key={key}>
|
||||||
<div className="mb-2 flex w-full justify-between md:w-2/3">
|
<div className="mb-2 flex w-full justify-between md:w-2/3">
|
||||||
@ -104,7 +104,7 @@ const StatesSettings: NextPage<TStateSettingsProps> = (props) => {
|
|||||||
<div className="space-y-1 rounded-xl border p-1 md:w-2/3">
|
<div className="space-y-1 rounded-xl border p-1 md:w-2/3">
|
||||||
{key === activeGroup && (
|
{key === activeGroup && (
|
||||||
<CreateUpdateStateInline
|
<CreateUpdateStateInline
|
||||||
projectId={activeProject.id}
|
projectId={projectDetails.id}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setActiveGroup(null);
|
setActiveGroup(null);
|
||||||
setSelectedState(null);
|
setSelectedState(null);
|
||||||
@ -143,7 +143,7 @@ const StatesSettings: NextPage<TStateSettingsProps> = (props) => {
|
|||||||
) : (
|
) : (
|
||||||
<div className="border-b last:border-b-0" key={state.id}>
|
<div className="border-b last:border-b-0" key={state.id}>
|
||||||
<CreateUpdateStateInline
|
<CreateUpdateStateInline
|
||||||
projectId={activeProject.id}
|
projectId={projectDetails.id}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setActiveGroup(null);
|
setActiveGroup(null);
|
||||||
setSelectedState(null);
|
setSelectedState(null);
|
||||||
@ -168,7 +168,7 @@ const StatesSettings: NextPage<TStateSettingsProps> = (props) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SettingsLayout>
|
</AppLayout>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,19 +1,21 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
|
||||||
// lib
|
// lib
|
||||||
import type { NextPage, GetServerSideProps } from "next";
|
|
||||||
import { requiredWorkspaceAdmin } from "lib/auth";
|
import { requiredWorkspaceAdmin } from "lib/auth";
|
||||||
// constants
|
|
||||||
// services
|
// services
|
||||||
import workspaceService from "services/workspace.service";
|
import workspaceService from "services/workspace.service";
|
||||||
// layouts
|
// layouts
|
||||||
import SettingsLayout from "layouts/settings-layout";
|
import AppLayout from "layouts/app-layout";
|
||||||
// ui
|
// ui
|
||||||
import { Button } from "components/ui";
|
import { Button } from "components/ui";
|
||||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||||
|
// types
|
||||||
|
import type { NextPage, GetServerSideProps } from "next";
|
||||||
|
// fetch-keys
|
||||||
import { WORKSPACE_DETAILS } from "constants/fetch-keys";
|
import { WORKSPACE_DETAILS } from "constants/fetch-keys";
|
||||||
|
|
||||||
type TBillingSettingsProps = {
|
type TBillingSettingsProps = {
|
||||||
@ -35,9 +37,9 @@ const BillingSettings: NextPage<TBillingSettingsProps> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SettingsLayout
|
<AppLayout
|
||||||
memberType={{ ...props }}
|
settingsLayout="workspace"
|
||||||
type="workspace"
|
memberType={props}
|
||||||
breadcrumbs={
|
breadcrumbs={
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
<BreadcrumbItem
|
<BreadcrumbItem
|
||||||
@ -75,7 +77,7 @@ const BillingSettings: NextPage<TBillingSettingsProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</SettingsLayout>
|
</AppLayout>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,173 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import useSWR from "swr";
|
|
||||||
|
|
||||||
// lib
|
|
||||||
import type { GetServerSideProps, NextPage } from "next";
|
|
||||||
import { requiredWorkspaceAdmin } from "lib/auth";
|
|
||||||
// constants
|
|
||||||
// services
|
|
||||||
import workspaceService from "services/workspace.service";
|
|
||||||
// layouts
|
|
||||||
import SettingsLayout from "layouts/settings-layout";
|
|
||||||
// ui
|
|
||||||
import { Button } from "components/ui";
|
|
||||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
|
||||||
import { WORKSPACE_DETAILS } from "constants/fetch-keys";
|
|
||||||
|
|
||||||
type TFeatureSettingsProps = {
|
|
||||||
isOwner: boolean;
|
|
||||||
isMember: boolean;
|
|
||||||
isViewer: boolean;
|
|
||||||
isGuest: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const FeaturesSettings: NextPage<TFeatureSettingsProps> = (props) => {
|
|
||||||
const {
|
|
||||||
query: { workspaceSlug },
|
|
||||||
} = useRouter();
|
|
||||||
|
|
||||||
const { data: activeWorkspace } = useSWR(
|
|
||||||
workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug as string) : null,
|
|
||||||
() => (workspaceSlug ? workspaceService.getWorkspace(workspaceSlug as string) : null)
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<SettingsLayout
|
|
||||||
memberType={{
|
|
||||||
...props,
|
|
||||||
}}
|
|
||||||
type="workspace"
|
|
||||||
breadcrumbs={
|
|
||||||
<Breadcrumbs>
|
|
||||||
<BreadcrumbItem
|
|
||||||
title={`${activeWorkspace?.name ?? "Workspace"}`}
|
|
||||||
link={`/${workspaceSlug}`}
|
|
||||||
/>
|
|
||||||
<BreadcrumbItem title="Members Settings" />
|
|
||||||
</Breadcrumbs>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<section className="space-y-8">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-3xl font-bold leading-6 text-gray-900">Workspace Features</h3>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-8 md:w-2/3">
|
|
||||||
<div className="flex items-center gap-x-10 gap-y-2">
|
|
||||||
<div>
|
|
||||||
<h4 className="text-md mb-1 leading-6 text-gray-900">Use modules</h4>
|
|
||||||
<p className="mb-3 text-sm text-gray-500">
|
|
||||||
Modules are enabled for all the projects in this workspace. Access it from the
|
|
||||||
navigation bar.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{/* Disabled- bg-gray-200, translate-x-0 */}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="pointer-events-none relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent bg-indigo-500 transition-colors duration-200 ease-in-out focus:outline-none"
|
|
||||||
role="switch"
|
|
||||||
aria-checked="false"
|
|
||||||
>
|
|
||||||
<span className="sr-only">Use setting</span>
|
|
||||||
<span
|
|
||||||
aria-hidden="true"
|
|
||||||
className="pointer-events-none inline-block h-5 w-5 translate-x-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-x-10 gap-y-2">
|
|
||||||
<div>
|
|
||||||
<h4 className="text-md mb-1 leading-6 text-gray-900">Use cycles</h4>
|
|
||||||
<p className="mb-3 text-sm text-gray-500">
|
|
||||||
Cycles are enabled for all the projects in this workspace. Access it from the
|
|
||||||
navigation bar.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{/* Disabled- bg-gray-200, translate-x-0 */}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="pointer-events-none relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent bg-indigo-500 transition-colors duration-200 ease-in-out focus:outline-none"
|
|
||||||
role="switch"
|
|
||||||
aria-checked="false"
|
|
||||||
>
|
|
||||||
<span className="sr-only">Use setting</span>
|
|
||||||
<span
|
|
||||||
aria-hidden="true"
|
|
||||||
className="pointer-events-none inline-block h-5 w-5 translate-x-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-x-10 gap-y-2">
|
|
||||||
<div>
|
|
||||||
<h4 className="text-md mb-1 leading-6 text-gray-900">Use backlogs</h4>
|
|
||||||
<p className="mb-3 text-sm text-gray-500">
|
|
||||||
Backlog are enabled for all the projects in this workspace. Access it from the
|
|
||||||
navigation bar.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{/* Disabled- bg-gray-200, translate-x-0 */}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="pointer-events-none relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent bg-indigo-500 transition-colors duration-200 ease-in-out focus:outline-none"
|
|
||||||
role="switch"
|
|
||||||
aria-checked="false"
|
|
||||||
>
|
|
||||||
<span className="sr-only">Use setting</span>
|
|
||||||
<span
|
|
||||||
aria-hidden="true"
|
|
||||||
className="pointer-events-none inline-block h-5 w-5 translate-x-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<a href="https://plane.so/" target="_blank" rel="noreferrer">
|
|
||||||
<Button theme="secondary" size="rg" className="text-xs">
|
|
||||||
Plane is open-source, view Roadmap
|
|
||||||
</Button>
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/makeplane/plane" target="_blank" rel="noreferrer">
|
|
||||||
<Button theme="secondary" size="rg" className="text-xs">
|
|
||||||
Star us on GitHub
|
|
||||||
</Button>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</SettingsLayout>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
|
||||||
const workspaceSlug = ctx.params?.workspaceSlug as string;
|
|
||||||
|
|
||||||
const memberDetail = await requiredWorkspaceAdmin(workspaceSlug, ctx.req.headers.cookie);
|
|
||||||
|
|
||||||
if (memberDetail === null) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: "/",
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
isOwner: memberDetail?.role === 20,
|
|
||||||
isMember: memberDetail?.role === 15,
|
|
||||||
isViewer: memberDetail?.role === 10,
|
|
||||||
isGuest: memberDetail?.role === 5,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FeaturesSettings;
|
|
@ -17,7 +17,7 @@ import { requiredWorkspaceAdmin } from "lib/auth";
|
|||||||
import workspaceService from "services/workspace.service";
|
import workspaceService from "services/workspace.service";
|
||||||
import fileServices from "services/file.service";
|
import fileServices from "services/file.service";
|
||||||
// layouts
|
// layouts
|
||||||
import SettingsLayout from "layouts/settings-layout";
|
import AppLayout from "layouts/app-layout";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// components
|
// components
|
||||||
@ -35,7 +35,7 @@ import type { GetServerSideProps, NextPage } from "next";
|
|||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { WORKSPACE_DETAILS, USER_WORKSPACES } from "constants/fetch-keys";
|
import { WORKSPACE_DETAILS, USER_WORKSPACES } from "constants/fetch-keys";
|
||||||
// constants
|
// constants
|
||||||
import { companySize } from "constants/";
|
import { COMPANY_SIZE } from "constants/workspace";
|
||||||
|
|
||||||
const defaultValues: Partial<IWorkspace> = {
|
const defaultValues: Partial<IWorkspace> = {
|
||||||
name: "",
|
name: "",
|
||||||
@ -85,11 +85,13 @@ const WorkspaceSettings: NextPage<TWorkspaceSettingsProps> = (props) => {
|
|||||||
|
|
||||||
const onSubmit = async (formData: IWorkspace) => {
|
const onSubmit = async (formData: IWorkspace) => {
|
||||||
if (!activeWorkspace) return;
|
if (!activeWorkspace) return;
|
||||||
|
|
||||||
const payload: Partial<IWorkspace> = {
|
const payload: Partial<IWorkspace> = {
|
||||||
logo: formData.logo,
|
logo: formData.logo,
|
||||||
name: formData.name,
|
name: formData.name,
|
||||||
company_size: formData.company_size,
|
company_size: formData.company_size,
|
||||||
};
|
};
|
||||||
|
|
||||||
await workspaceService
|
await workspaceService
|
||||||
.updateWorkspace(activeWorkspace.slug, payload)
|
.updateWorkspace(activeWorkspace.slug, payload)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
@ -106,9 +108,9 @@ const WorkspaceSettings: NextPage<TWorkspaceSettingsProps> = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsLayout
|
<AppLayout
|
||||||
memberType={{ ...props }}
|
settingsLayout="workspace"
|
||||||
type="workspace"
|
memberType={props}
|
||||||
meta={{
|
meta={{
|
||||||
title: "Plane - Workspace Settings",
|
title: "Plane - Workspace Settings",
|
||||||
}}
|
}}
|
||||||
@ -278,7 +280,7 @@ const WorkspaceSettings: NextPage<TWorkspaceSettingsProps> = (props) => {
|
|||||||
label={value ? value.toString() : "Select company size"}
|
label={value ? value.toString() : "Select company size"}
|
||||||
input
|
input
|
||||||
>
|
>
|
||||||
{companySize?.map((item) => (
|
{COMPANY_SIZE?.map((item) => (
|
||||||
<CustomSelect.Option key={item.value} value={item.value}>
|
<CustomSelect.Option key={item.value} value={item.value}>
|
||||||
{item.label}
|
{item.label}
|
||||||
</CustomSelect.Option>
|
</CustomSelect.Option>
|
||||||
@ -315,7 +317,7 @@ const WorkspaceSettings: NextPage<TWorkspaceSettingsProps> = (props) => {
|
|||||||
<Spinner />
|
<Spinner />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</SettingsLayout>
|
</AppLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2,29 +2,31 @@ import { useState } from "react";
|
|||||||
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
|
||||||
|
|
||||||
// lib
|
// lib
|
||||||
import type { GetServerSideProps, NextPage } from "next";
|
|
||||||
import { requiredWorkspaceAdmin } from "lib/auth";
|
import { requiredWorkspaceAdmin } from "lib/auth";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// services
|
// services
|
||||||
import workspaceService from "services/workspace.service";
|
import workspaceService from "services/workspace.service";
|
||||||
// constants
|
|
||||||
// layouts
|
// layouts
|
||||||
import SettingsLayout from "layouts/settings-layout";
|
import AppLayout from "layouts/app-layout";
|
||||||
// components
|
// components
|
||||||
import ConfirmWorkspaceMemberRemove from "components/workspace/confirm-workspace-member-remove";
|
import ConfirmWorkspaceMemberRemove from "components/workspace/confirm-workspace-member-remove";
|
||||||
import SendWorkspaceInvitationModal from "components/workspace/send-workspace-invitation-modal";
|
import SendWorkspaceInvitationModal from "components/workspace/send-workspace-invitation-modal";
|
||||||
// ui
|
// ui
|
||||||
import { Button, CustomListbox, CustomMenu, Loader } from "components/ui";
|
import { Button, CustomListbox, CustomMenu, Loader } from "components/ui";
|
||||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||||
import { ROLE } from "constants/";
|
|
||||||
// icons
|
// icons
|
||||||
|
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||||
|
// types
|
||||||
|
import type { GetServerSideProps, NextPage } from "next";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { WORKSPACE_DETAILS, WORKSPACE_INVITATIONS, WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
import { WORKSPACE_DETAILS, WORKSPACE_INVITATIONS, WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
||||||
|
// constants
|
||||||
|
import { ROLE } from "constants/workspace";
|
||||||
|
|
||||||
type TMembersSettingsProps = {
|
type TMembersSettingsProps = {
|
||||||
isOwner: boolean;
|
isOwner: boolean;
|
||||||
@ -135,11 +137,9 @@ const MembersSettings: NextPage<TMembersSettingsProps> = (props) => {
|
|||||||
workspace_slug={workspaceSlug as string}
|
workspace_slug={workspaceSlug as string}
|
||||||
members={members}
|
members={members}
|
||||||
/>
|
/>
|
||||||
<SettingsLayout
|
<AppLayout
|
||||||
memberType={{
|
settingsLayout="workspace"
|
||||||
...props,
|
memberType={props}
|
||||||
}}
|
|
||||||
type="workspace"
|
|
||||||
breadcrumbs={
|
breadcrumbs={
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
<BreadcrumbItem
|
<BreadcrumbItem
|
||||||
@ -283,7 +283,7 @@ const MembersSettings: NextPage<TMembersSettingsProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
</SettingsLayout>
|
</AppLayout>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -25,7 +25,7 @@ import type { NextPage, NextPageContext } from "next";
|
|||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { USER_WORKSPACES } from "constants/fetch-keys";
|
import { USER_WORKSPACES } from "constants/fetch-keys";
|
||||||
// constants
|
// constants
|
||||||
import { companySize } from "constants/";
|
import { COMPANY_SIZE } from "constants/workspace";
|
||||||
|
|
||||||
const defaultValues = {
|
const defaultValues = {
|
||||||
name: "",
|
name: "",
|
||||||
@ -145,7 +145,7 @@ const CreateWorkspace: NextPage = () => {
|
|||||||
label={value ? value.toString() : "Select company size"}
|
label={value ? value.toString() : "Select company size"}
|
||||||
input
|
input
|
||||||
>
|
>
|
||||||
{companySize?.map((item) => (
|
{COMPANY_SIZE?.map((item) => (
|
||||||
<CustomSelect.Option key={item.value} value={item.value}>
|
<CustomSelect.Option key={item.value} value={item.value}>
|
||||||
{item.label}
|
{item.label}
|
||||||
</CustomSelect.Option>
|
</CustomSelect.Option>
|
||||||
|
@ -1,21 +1,21 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import type { NextPage } from "next";
|
|
||||||
|
|
||||||
// layouts
|
// layouts
|
||||||
import DefaultLayout from "layouts/default-layout";
|
import DefaultLayout from "layouts/default-layout";
|
||||||
|
// types
|
||||||
|
import type { NextPage } from "next";
|
||||||
|
|
||||||
const ErrorPage: NextPage = () => (
|
const ErrorPage: NextPage = () => (
|
||||||
<DefaultLayout
|
<DefaultLayout
|
||||||
meta={{
|
meta={{
|
||||||
title: "Plane - An error occurred",
|
title: "Plane - An error occurred",
|
||||||
description: "We were unable to get this page for you.",
|
description: "We were unable to get this page for you.",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="h-full w-full">
|
<div className="h-full w-full">
|
||||||
<h2 className="text-3xl">Error!</h2>
|
<h2 className="text-3xl">Error!</h2>
|
||||||
</div>
|
</div>
|
||||||
</DefaultLayout>
|
</DefaultLayout>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default ErrorPage;
|
export default ErrorPage;
|
||||||
|
@ -4,15 +4,16 @@ import { useRouter } from "next/router";
|
|||||||
|
|
||||||
import { mutate } from "swr";
|
import { mutate } from "swr";
|
||||||
|
|
||||||
|
// layouts
|
||||||
|
import DefaultLayout from "layouts/default-layout";
|
||||||
// services
|
// services
|
||||||
import type { NextPage } from "next";
|
|
||||||
import authenticationService from "services/authentication.service";
|
import authenticationService from "services/authentication.service";
|
||||||
// constants
|
|
||||||
// hooks
|
// hooks
|
||||||
import useUser from "hooks/use-user";
|
import useUser from "hooks/use-user";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// layouts
|
// types
|
||||||
import DefaultLayout from "layouts/default-layout";
|
import type { NextPage } from "next";
|
||||||
|
// constants
|
||||||
import { USER_WORKSPACES } from "constants/fetch-keys";
|
import { USER_WORKSPACES } from "constants/fetch-keys";
|
||||||
|
|
||||||
const MagicSignIn: NextPage = () => {
|
const MagicSignIn: NextPage = () => {
|
||||||
|
@ -2,11 +2,13 @@ import { useState } from "react";
|
|||||||
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
// hooks
|
|
||||||
import type { NextPage, NextPageContext } from "next";
|
|
||||||
import useUser from "hooks/use-user";
|
|
||||||
// lib
|
// lib
|
||||||
import { requiredAuth } from "lib/auth";
|
import { requiredAuth } from "lib/auth";
|
||||||
|
// services
|
||||||
|
import userService from "services/user.service";
|
||||||
|
// hooks
|
||||||
|
import useUser from "hooks/use-user";
|
||||||
// layouts
|
// layouts
|
||||||
import DefaultLayout from "layouts/default-layout";
|
import DefaultLayout from "layouts/default-layout";
|
||||||
// components
|
// components
|
||||||
@ -20,7 +22,8 @@ import InviteMembers from "components/onboarding/invite-members";
|
|||||||
import CommandMenu from "components/onboarding/command-menu";
|
import CommandMenu from "components/onboarding/command-menu";
|
||||||
// images
|
// images
|
||||||
import Logo from "public/onboarding/logo.svg";
|
import Logo from "public/onboarding/logo.svg";
|
||||||
import userService from "services/user.service";
|
// types
|
||||||
|
import type { NextPage, NextPageContext } from "next";
|
||||||
|
|
||||||
const Onboarding: NextPage = () => {
|
const Onboarding: NextPage = () => {
|
||||||
const [step, setStep] = useState(1);
|
const [step, setStep] = useState(1);
|
||||||
|
@ -13,7 +13,6 @@ import {
|
|||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
// swr
|
// swr
|
||||||
// services
|
// services
|
||||||
import type { NextPage } from "next";
|
|
||||||
import workspaceService from "services/workspace.service";
|
import workspaceService from "services/workspace.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useUser from "hooks/use-user";
|
import useUser from "hooks/use-user";
|
||||||
@ -23,6 +22,8 @@ import DefaultLayout from "layouts/default-layout";
|
|||||||
import { Spinner } from "components/ui";
|
import { Spinner } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { EmptySpace, EmptySpaceItem } from "components/ui/empty-space";
|
import { EmptySpace, EmptySpaceItem } from "components/ui/empty-space";
|
||||||
|
// types
|
||||||
|
import type { NextPage } from "next";
|
||||||
// constants
|
// constants
|
||||||
import { WORKSPACE_INVITATION } from "constants/fetch-keys";
|
import { WORKSPACE_INVITATION } from "constants/fetch-keys";
|
||||||
|
|
||||||
|
2
apps/app/types/issues.d.ts
vendored
2
apps/app/types/issues.d.ts
vendored
@ -171,7 +171,7 @@ export interface IIssueLabels {
|
|||||||
updated_at: Date;
|
updated_at: Date;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
colour: string;
|
color: string;
|
||||||
created_by: string;
|
created_by: string;
|
||||||
updated_by: string;
|
updated_by: string;
|
||||||
project: string;
|
project: string;
|
||||||
|
24
apps/app/types/projects.d.ts
vendored
24
apps/app/types/projects.d.ts
vendored
@ -1,20 +1,22 @@
|
|||||||
import type { IUserLite, IWorkspace } from "./";
|
import type { IUserLite, IWorkspace } from "./";
|
||||||
|
|
||||||
export interface IProject {
|
export interface IProject {
|
||||||
id: string;
|
|
||||||
workspace: IWorkspace | string;
|
|
||||||
default_assignee: IUser | string | null;
|
|
||||||
project_lead: IUser | string | null;
|
|
||||||
created_at: Date;
|
created_at: Date;
|
||||||
updated_at: Date;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
network: number;
|
|
||||||
identifier: string;
|
|
||||||
slug: string;
|
|
||||||
created_by: string;
|
created_by: string;
|
||||||
updated_by: string;
|
cycle_view: boolean;
|
||||||
|
default_assignee: IUser | string | null;
|
||||||
|
description: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
|
id: string;
|
||||||
|
identifier: string;
|
||||||
|
module_view: boolean;
|
||||||
|
name: string;
|
||||||
|
network: number;
|
||||||
|
project_lead: IUser | string | null;
|
||||||
|
slug: string;
|
||||||
|
updated_at: Date;
|
||||||
|
updated_by: string;
|
||||||
|
workspace: IWorkspace | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProjectViewTheme = {
|
type ProjectViewTheme = {
|
||||||
|
Loading…
Reference in New Issue
Block a user