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:
Aaryan Khandelwal 2023-02-08 10:13:07 +05:30 committed by GitHub
parent 4e27e93739
commit 76cc634a46
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
73 changed files with 1283 additions and 1648 deletions

View File

@ -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>

View File

@ -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" : ""

View File

@ -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>
); );
}; };

View File

@ -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}
/> />

View File

@ -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)}

View File

@ -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) => {

View File

@ -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}
/> />

View File

@ -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={() => {

View File

@ -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>
);

View File

@ -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>
);

View File

@ -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>
);

View File

@ -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";

View File

@ -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 ? (

View File

@ -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";

View File

@ -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}

View File

@ -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 =

View File

@ -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;

View File

@ -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>;

View File

@ -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

View File

@ -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>
);

View 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>
);

View 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>
);

View 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>
);
};

View File

@ -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>;

View File

@ -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>;

View File

@ -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;

View File

@ -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>

View File

@ -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;

View File

@ -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;

View File

@ -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

View File

@ -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>
</> </>

View File

@ -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]
); );

View File

@ -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;

View File

@ -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 = {

View File

@ -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,

View File

@ -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" },
];

View 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",
},
];

View 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" },
];

View 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];

View 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" },
];

View File

@ -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(

View File

@ -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 {

View File

@ -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,

View File

@ -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>
); );

View File

@ -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";

View File

@ -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;

View 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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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";

View File

@ -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;
};

View File

@ -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

View File

@ -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>
); );
}; };

View File

@ -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;

View File

@ -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>
); );
}; };

View File

@ -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>
); );
}; };

View File

@ -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>
</> </>
); );
}; };

View File

@ -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>
</> </>
); );
}; };

View File

@ -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>
</> </>
); );
}; };

View File

@ -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;

View File

@ -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>
); );
}; };

View File

@ -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>
</> </>
); );
}; };

View File

@ -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>

View File

@ -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;

View File

@ -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 = () => {

View File

@ -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);

View File

@ -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";

View File

@ -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;

View File

@ -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 = {