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,13 +39,7 @@ export const AllBoards: React.FC<Props> = ({
<div className="h-[calc(100vh-157px)] lg:h-[calc(100vh-115px)] w-full">
<DragDropContext onDragEnd={handleOnDragEnd}>
<div className="h-full w-full overflow-hidden">
<StrictModeDroppable droppableId="state" type="state" direction="horizontal">
{(provided) => (
<div
className="h-full w-full"
{...provided.droppableProps}
ref={provided.innerRef}
>
<div className="h-full w-full">
<div className="flex h-full gap-x-4 overflow-x-auto overflow-y-hidden">
{Object.keys(groupedByIssues).map((singleGroup, index) => {
const stateId =
@ -61,7 +55,6 @@ export const AllBoards: React.FC<Props> = ({
return (
<SingleBoard
key={index}
index={index}
type={type}
bgColor={bgColor}
groupTitle={singleGroup}
@ -77,10 +70,7 @@ export const AllBoards: React.FC<Props> = ({
);
})}
</div>
{provided.placeholder}
</div>
)}
</StrictModeDroppable>
</div>
</DragDropContext>
</div>

View File

@ -12,15 +12,13 @@ import {
// helpers
import { addSpaceIfCamelCase } from "helpers/string.helper";
// types
import { IIssue, NestedKeyOf } from "types";
import { IIssue } from "types";
type Props = {
provided: DraggableProvided;
isCollapsed: boolean;
setIsCollapsed: React.Dispatch<React.SetStateAction<boolean>>;
groupedByIssues: {
[key: string]: IIssue[];
};
selectedGroup: NestedKeyOf<IIssue> | null;
groupTitle: string;
createdBy: string | null;
bgColor?: string;
@ -30,9 +28,7 @@ type Props = {
export const BoardHeader: React.FC<Props> = ({
isCollapsed,
setIsCollapsed,
provided,
groupedByIssues,
selectedGroup,
groupTitle,
createdBy,
bgColor,
@ -44,16 +40,6 @@ export const BoardHeader: React.FC<Props> = ({
}`}
>
<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
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" : ""

View File

@ -17,7 +17,6 @@ import { PlusIcon } from "@heroicons/react/24/outline";
import { IIssue, IProjectMember, NestedKeyOf, UserAuth } from "types";
type Props = {
index: number;
type?: "issue" | "cycle" | "module";
bgColor?: string;
groupTitle: string;
@ -34,7 +33,6 @@ type Props = {
};
export const SingleBoard: React.FC<Props> = ({
index,
type,
bgColor,
groupTitle,
@ -70,20 +68,9 @@ export const SingleBoard: React.FC<Props> = ({
: (bgColor = "#ff0000");
return (
<Draggable draggableId={groupTitle} index={index}>
{(provided, snapshot) => (
<div
className={`h-full flex-shrink-0 rounded ${
snapshot && snapshot.isDragging ? "border-theme shadow-lg" : ""
} ${!isCollapsed ? "" : "w-80 border bg-gray-50"}`}
ref={provided?.innerRef}
{...provided?.draggableProps}
>
<div
className={`${!isCollapsed ? "" : "flex h-full flex-col space-y-3 overflow-y-auto"}`}
>
<div className={`h-full flex-shrink-0 rounded ${!isCollapsed ? "" : "w-80 border bg-gray-50"}`}>
<div className={`${!isCollapsed ? "" : "flex h-full flex-col space-y-3 overflow-y-auto"}`}>
<BoardHeader
provided={provided}
addIssueToState={addIssueToState}
bgColor={bgColor}
createdBy={createdBy}
@ -91,12 +78,11 @@ export const SingleBoard: React.FC<Props> = ({
groupedByIssues={groupedByIssues}
isCollapsed={isCollapsed}
setIsCollapsed={setIsCollapsed}
selectedGroup={selectedGroup}
/>
<StrictModeDroppable key={groupTitle} droppableId={groupTitle}>
{(provided, snapshot) => (
<div
className={`relative mt-3 h-full space-y-3 overflow-y-auto px-3 pb-3 ${
className={`relative mt-3 h-full space-y-3 px-3 pb-3 ${
snapshot.isDraggingOver ? "bg-indigo-50 bg-opacity-50" : ""
} ${!isCollapsed ? "hidden" : "block"}`}
ref={provided.innerRef}
@ -108,8 +94,8 @@ export const SingleBoard: React.FC<Props> = ({
index={index}
type={type}
issue={issue}
selectedGroup={selectedGroup}
properties={properties}
members={members}
handleDeleteIssue={handleDeleteIssue}
orderBy={orderBy}
userAuth={userAuth}
@ -143,9 +129,7 @@ export const SingleBoard: React.FC<Props> = ({
optionsPosition="left"
noBorder
>
<CustomMenu.MenuItem onClick={addIssueToState}>
Create new
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={addIssueToState}>Create new</CustomMenu.MenuItem>
{openIssuesListModal && (
<CustomMenu.MenuItem onClick={openIssuesListModal}>
Add an existing issue
@ -158,7 +142,5 @@ export const SingleBoard: React.FC<Props> = ({
</StrictModeDroppable>
</div>
</div>
)}
</Draggable>
);
};

View File

@ -16,14 +16,17 @@ import {
import { TrashIcon } from "@heroicons/react/24/outline";
// services
import issuesService from "services/issues.service";
import stateService from "services/state.service";
// components
import { AssigneeSelect, DueDateSelect, PrioritySelect, StateSelect } from "components/core/select";
import {
ViewAssigneeSelect,
ViewDueDateSelect,
ViewPrioritySelect,
ViewStateSelect,
} from "components/issues/view-select";
// types
import {
CycleIssueResponse,
IIssue,
IProjectMember,
IssueResponse,
ModuleIssueResponse,
NestedKeyOf,
@ -31,14 +34,14 @@ import {
UserAuth,
} from "types";
// 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 = {
index: number;
type?: string;
issue: IIssue;
selectedGroup: NestedKeyOf<IIssue> | null;
properties: Properties;
members: IProjectMember[] | undefined;
handleDeleteIssue: (issue: IIssue) => void;
orderBy: NestedKeyOf<IIssue> | "manual" | null;
userAuth: UserAuth;
@ -48,8 +51,8 @@ export const SingleBoardIssue: React.FC<Props> = ({
index,
type,
issue,
selectedGroup,
properties,
members,
handleDeleteIssue,
orderBy,
userAuth,
@ -57,13 +60,6 @@ export const SingleBoardIssue: React.FC<Props> = ({
const router = useRouter();
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(
(formData: Partial<IIssue>) => {
if (!workspaceSlug || !projectId) return;
@ -125,16 +121,8 @@ export const SingleBoardIssue: React.FC<Props> = ({
issuesService
.patchIssue(workspaceSlug as string, projectId as string, issue.id, formData)
.then((res) => {
mutate(
cycleId
? CYCLE_ISSUES(cycleId as string)
: CYCLE_ISSUES(issue?.issue_cycle?.cycle ?? "")
);
mutate(
moduleId
? MODULE_ISSUES(moduleId as string)
: MODULE_ISSUES(issue?.issue_module?.module ?? "")
);
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));
})
@ -164,7 +152,12 @@ export const SingleBoardIssue: React.FC<Props> = ({
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
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) => (
<div
className={`rounded border bg-white shadow-sm ${
@ -204,22 +197,22 @@ export const SingleBoardIssue: React.FC<Props> = ({
</Link>
<div className="flex flex-wrap items-center gap-x-1 gap-y-2 text-xs">
{properties.priority && (
<PrioritySelect
<ViewPrioritySelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
isNotAllowed={isNotAllowed}
position="left"
/>
)}
{properties.state && (
<StateSelect
<ViewStateSelect
issue={issue}
states={states}
partialUpdateIssue={partialUpdateIssue}
isNotAllowed={isNotAllowed}
/>
)}
{properties.due_date && (
<DueDateSelect
<ViewDueDateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
isNotAllowed={isNotAllowed}
@ -232,9 +225,8 @@ export const SingleBoardIssue: React.FC<Props> = ({
</div>
)}
{properties.assignee && (
<AssigneeSelect
<ViewAssigneeSelect
issue={issue}
members={members}
partialUpdateIssue={partialUpdateIssue}
isNotAllowed={isNotAllowed}
/>

View File

@ -17,7 +17,7 @@ import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
// types
import { IIssue, Properties } from "types";
// common
import { filterIssueOptions, groupByOptions, orderByOptions } from "constants/";
import { GROUP_BY_OPTIONS, ORDER_BY_OPTIONS, FILTER_ISSUE_OPTIONS } from "constants/issue";
type Props = {
issues?: IIssue[];
@ -99,12 +99,12 @@ export const IssuesFilterView: React.FC<Props> = ({ issues }) => {
<h4 className="text-sm text-gray-600">Group by</h4>
<CustomMenu
label={
groupByOptions.find((option) => option.key === groupByProperty)
GROUP_BY_OPTIONS.find((option) => option.key === groupByProperty)
?.name ?? "Select"
}
width="lg"
>
{groupByOptions.map((option) =>
{GROUP_BY_OPTIONS.map((option) =>
issueView === "kanban" && option.key === null ? null : (
<CustomMenu.MenuItem
key={option.key}
@ -120,12 +120,12 @@ export const IssuesFilterView: React.FC<Props> = ({ issues }) => {
<h4 className="text-sm text-gray-600">Order by</h4>
<CustomMenu
label={
orderByOptions.find((option) => option.key === orderBy)?.name ??
ORDER_BY_OPTIONS.find((option) => option.key === orderBy)?.name ??
"Select"
}
width="lg"
>
{orderByOptions.map((option) =>
{ORDER_BY_OPTIONS.map((option) =>
groupByProperty === "priority" &&
option.key === "priority" ? null : (
<CustomMenu.MenuItem
@ -142,12 +142,12 @@ export const IssuesFilterView: React.FC<Props> = ({ issues }) => {
<h4 className="text-sm text-gray-600">Issue type</h4>
<CustomMenu
label={
filterIssueOptions.find((option) => option.key === filterIssue)
FILTER_ISSUE_OPTIONS.find((option) => option.key === filterIssue)
?.name ?? "Select"
}
width="lg"
>
{filterIssueOptions.map((option) => (
{FILTER_ISSUE_OPTIONS.map((option) => (
<CustomMenu.MenuItem
key={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 { data: states, mutate: mutateState } = useSWR<IState[]>(
const { data: states } = useSWR<IState[]>(
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
workspaceSlug
? () => stateService.getStates(workspaceSlug as string, projectId as string)
@ -86,45 +86,10 @@ export const IssuesView: React.FC<Props> = ({
(result: DropResult) => {
if (!result.destination || !workspaceSlug || !projectId) return;
const { source, destination, type } = result;
const { source, destination } = result;
if (type === "state") {
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 =
prevSequenceNumber && nextSequenceNumber
? (prevSequenceNumber + nextSequenceNumber) / 2
: nextSequenceNumber
? nextSequenceNumber - 15000 / 2
: prevSequenceNumber
? prevSequenceNumber + 15000 / 2
: 15000;
newStates[destination.index].sequence = sequenceNumber;
mutateState(newStates, false);
stateService
.patchState(
workspaceSlug 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
@ -208,16 +173,8 @@ export const IssuesView: React.FC<Props> = ({
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 ?? "")
);
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));
});
@ -306,32 +263,15 @@ export const IssuesView: React.FC<Props> = ({
state: destinationStateId,
})
.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 ?? "")
);
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,
mutateState,
groupedByIssues,
projectId,
selectedGroup,
states,
]
[workspaceSlug, cycleId, moduleId, groupedByIssues, projectId, selectedGroup, states]
);
const addIssueToState = (groupTitle: string, stateId: string | null) => {

View File

@ -3,20 +3,23 @@ import React, { useCallback } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import useSWR, { mutate } from "swr";
import { mutate } from "swr";
// services
import issuesService from "services/issues.service";
import stateService from "services/state.service";
// components
import { AssigneeSelect, DueDateSelect, PrioritySelect, StateSelect } from "components/core/select";
import {
ViewAssigneeSelect,
ViewDueDateSelect,
ViewPrioritySelect,
ViewStateSelect,
} from "components/issues/view-select";
// ui
import { CustomMenu } from "components/ui";
// types
import {
CycleIssueResponse,
IIssue,
IProjectMember,
IssueResponse,
ModuleIssueResponse,
Properties,
@ -29,7 +32,6 @@ type Props = {
type?: string;
issue: IIssue;
properties: Properties;
members: IProjectMember[] | undefined;
editIssue: () => void;
removeIssue?: (() => void) | null;
handleDeleteIssue: (issue: IIssue) => void;
@ -40,7 +42,6 @@ export const SingleListIssue: React.FC<Props> = ({
type,
issue,
properties,
members,
editIssue,
removeIssue,
handleDeleteIssue,
@ -49,13 +50,6 @@ export const SingleListIssue: React.FC<Props> = ({
const router = useRouter();
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(
(formData: Partial<IIssue>) => {
if (!workspaceSlug || !projectId) return;
@ -117,16 +111,8 @@ export const SingleListIssue: React.FC<Props> = ({
issuesService
.patchIssue(workspaceSlug as string, projectId as string, issue.id, formData)
.then((res) => {
mutate(
cycleId
? CYCLE_ISSUES(cycleId as string)
: CYCLE_ISSUES(issue?.issue_cycle?.cycle ?? "")
);
mutate(
moduleId
? MODULE_ISSUES(moduleId as string)
: MODULE_ISSUES(issue?.issue_module?.module ?? "")
);
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));
})
@ -161,22 +147,21 @@ export const SingleListIssue: React.FC<Props> = ({
</div>
<div className="flex flex-shrink-0 flex-wrap items-center gap-x-1 gap-y-2 text-xs">
{properties.priority && (
<PrioritySelect
<ViewPrioritySelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
isNotAllowed={isNotAllowed}
/>
)}
{properties.state && (
<StateSelect
<ViewStateSelect
issue={issue}
states={states}
partialUpdateIssue={partialUpdateIssue}
isNotAllowed={isNotAllowed}
/>
)}
{properties.due_date && (
<DueDateSelect
<ViewDueDateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
isNotAllowed={isNotAllowed}
@ -188,9 +173,8 @@ export const SingleListIssue: React.FC<Props> = ({
</div>
)}
{properties.assignee && (
<AssigneeSelect
<ViewAssigneeSelect
issue={issue}
members={members}
partialUpdateIssue={partialUpdateIssue}
isNotAllowed={isNotAllowed}
/>

View File

@ -101,7 +101,6 @@ export const SingleList: React.FC<Props> = ({
type={type}
issue={issue}
properties={properties}
members={members}
editIssue={() => handleEditIssue(issue)}
handleDeleteIssue={handleDeleteIssue}
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";
// helpers
import { getRecentEmojis, saveRecentEmoji } from "./helpers";
import { getRandomEmoji } from "helpers/functions.helper";
import { getRandomEmoji } from "helpers/common.helper";
// hooks
import useOutsideClickDetector from "hooks/use-outside-click-detector";

View File

@ -8,6 +8,7 @@ import {
CalendarDaysIcon,
ChartBarIcon,
ChatBubbleBottomCenterTextIcon,
RectangleGroupIcon,
Squares2X2Icon,
UserIcon,
} from "@heroicons/react/24/outline";
@ -18,7 +19,7 @@ import { CommentCard } from "components/issues/comment";
// ui
import { Loader } from "components/ui";
// icons
import { BlockedIcon, BlockerIcon, TagIcon, UserGroupIcon } from "components/icons";
import { BlockedIcon, BlockerIcon, CyclesIcon, TagIcon, UserGroupIcon } from "components/icons";
// helpers
import { renderShortNumericDateFormat, timeAgo } from "helpers/date-time.helper";
import { addSpaceIfCamelCase } from "helpers/string.helper";
@ -47,9 +48,17 @@ const activityDetails: {
message: "marked this issue is blocking",
icon: <BlockerIcon height="16" width="16" />,
},
cycles: {
message: "set the cycle to",
icon: <CyclesIcon height="16" width="16" />,
},
labels: {
icon: <TagIcon height="16" width="16" />,
},
modules: {
message: "set the module to",
icon: <RectangleGroupIcon className="h-4 w-4" />,
},
state: {
message: "set the state to",
icon: <Squares2X2Icon className="h-4 w-4" />,
@ -76,10 +85,12 @@ const activityDetails: {
},
};
export const IssueActivitySection: React.FC<{
type Props = {
issueActivities: IIssueActivity[];
mutate: KeyedMutator<IIssueActivity[]>;
}> = ({ issueActivities, mutate }) => {
};
export const IssueActivitySection: React.FC<Props> = ({ issueActivities, mutate }) => {
const router = useRouter();
const { workspaceSlug, projectId, issueId } = router.query;
@ -183,7 +194,9 @@ export const IssueActivitySection: React.FC<{
?.message}{" "}
</span>
<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>
) : activity.field === "description" ? null : activity.field === "state" ? (
activity.new_value ? (

View File

@ -10,7 +10,7 @@ import issuesServices from "services/issues.service";
// ui
import { Loader } from "components/ui";
// helpers
import { debounce } from "helpers/functions.helper";
import { debounce } from "helpers/common.helper";
// types
import type { IIssueActivity, IIssueComment } from "types";
import type { KeyedMutator } from "swr";

View File

@ -3,20 +3,23 @@ import React, { useCallback } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import useSWR, { mutate } from "swr";
import { mutate } from "swr";
// services
import stateService from "services/state.service";
import issuesService from "services/issues.service";
// components
import { DueDateSelect, PrioritySelect, StateSelect } from "components/core/select";
import {
ViewDueDateSelect,
ViewPrioritySelect,
ViewStateSelect,
} from "components/issues/view-select";
// ui
import { AssigneesList } from "components/ui/avatar";
import { CustomMenu } from "components/ui";
// types
import { IIssue, Properties } from "types";
// fetch-keys
import { STATE_LIST, USER_ISSUE } from "constants/fetch-keys";
import { USER_ISSUE } from "constants/fetch-keys";
type Props = {
issue: IIssue;
@ -34,13 +37,6 @@ export const MyIssuesListItem: React.FC<Props> = ({
const router = useRouter();
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(
(formData: Partial<IIssue>) => {
if (!workspaceSlug) return;
@ -92,22 +88,21 @@ export const MyIssuesListItem: React.FC<Props> = ({
</div>
<div className="flex flex-shrink-0 flex-wrap items-center gap-x-1 gap-y-2 text-xs">
{properties.priority && (
<PrioritySelect
<ViewPrioritySelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
isNotAllowed={isNotAllowed}
/>
)}
{properties.state && (
<StateSelect
<ViewStateSelect
issue={issue}
states={states}
partialUpdateIssue={partialUpdateIssue}
isNotAllowed={isNotAllowed}
/>
)}
{properties.due_date && (
<DueDateSelect
<ViewDueDateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
isNotAllowed={isNotAllowed}

View File

@ -72,7 +72,7 @@ export const IssueLabelSelect: React.FC<Props> = ({ value, onChange, projectId }
const options = issueLabels?.map((label) => ({
value: label.id,
display: label.name,
color: label.colour,
color: label.color,
}));
const filteredOptions =

View File

@ -2,9 +2,10 @@ import React from "react";
// headless ui
import { Listbox, Transition } from "@headlessui/react";
// icons
import { getPriorityIcon } from "components/icons/priority-icon";
// constants
import { getPriorityIcon } from "constants/global";
import { PRIORITIES } from "constants/";
import { PRIORITIES } from "constants/project";
type Props = {
value: string | null;

View File

@ -1,17 +1,16 @@
// react
import React from "react";
// react-hook-form
import { Control, Controller, UseFormWatch } from "react-hook-form";
import { Control, Controller } from "react-hook-form";
// ui
import { ChartBarIcon } from "@heroicons/react/24/outline";
import { CustomSelect } from "components/ui";
// icons
import { ChartBarIcon } from "@heroicons/react/24/outline";
import { getPriorityIcon } from "components/icons/priority-icon";
// types
import { IIssue, UserAuth } from "types";
// common
// constants
import { getPriorityIcon } from "constants/global";
import { PRIORITIES } from "constants/";
import { PRIORITIES } from "constants/project";
type Props = {
control: Control<IIssue, any>;

View File

@ -56,7 +56,7 @@ type Props = {
const defaultValues: Partial<IIssueLabels> = {
name: "",
colour: "#ff0000",
color: "#ff0000",
};
export const IssueDetailsSidebar: React.FC<Props> = ({
@ -316,7 +316,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
>
<span
className="h-2 w-2 flex-shrink-0 rounded-full"
style={{ backgroundColor: singleLabel?.colour ?? "green" }}
style={{ backgroundColor: singleLabel?.color ?? "green" }}
/>
{singleLabel.name}
<XMarkIcon className="h-2 w-2 group-hover:text-red-500" />
@ -372,7 +372,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
>
<span
className="h-2 w-2 flex-shrink-0 rounded-full"
style={{ backgroundColor: label.colour ?? "green" }}
style={{ backgroundColor: label.color ?? "green" }}
/>
{label.name}
</Listbox.Option>
@ -422,11 +422,11 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
<Popover.Button
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
className="h-5 w-5 rounded"
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">
<Controller
name="colour"
name="color"
control={controlLabel}
render={({ field: { value, onChange } }) => (
<TwitterPicker

View File

@ -1,25 +1,44 @@
import React from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
// headless ui
import { Listbox, Transition } from "@headlessui/react";
// services
import projectService from "services/project.service";
// ui
import { AssigneesList, Avatar } from "components/ui";
// types
import { IIssue, IProjectMember } from "types";
import { IIssue } from "types";
// fetch-keys
import { PROJECT_MEMBERS } from "constants/fetch-keys";
type Props = {
issue: IIssue;
members: IProjectMember[] | undefined;
partialUpdateIssue: (formData: Partial<IIssue>) => void;
position?: "left" | "right";
isNotAllowed: boolean;
};
export const AssigneeSelect: React.FC<Props> = ({
export const ViewAssigneeSelect: React.FC<Props> = ({
issue,
members,
partialUpdateIssue,
position = "right",
isNotAllowed,
}) => (
}) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { data: members } = useSWR(
projectId ? PROJECT_MEMBERS(projectId as string) : null,
workspaceSlug && projectId
? () => projectService.projectMembers(workspaceSlug as string, projectId as string)
: null
);
return (
<Listbox
as="div"
value={issue.assignees}
@ -35,7 +54,6 @@ export const AssigneeSelect: React.FC<Props> = ({
disabled={isNotAllowed}
>
{({ open }) => (
<>
<div>
<Listbox.Button>
<div
@ -54,12 +72,16 @@ export const AssigneeSelect: React.FC<Props> = ({
leaveFrom="opacity-100"
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) => (
<Listbox.Option
key={member.member.id}
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" : ""
} ${
selected || issue.assignees?.includes(member.member.id)
@ -70,25 +92,15 @@ export const AssigneeSelect: React.FC<Props> = ({
value={member.member.id}
>
<Avatar user={member.member} />
<p>
{member.member.first_name && member.member.first_name !== ""
? member.member.first_name
: member.member.email}
</p>
</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">Assigned to</h5>
<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
import type { IModule } from "types";
// constants
import { MODULE_STATUS } from "constants/";
import { MODULE_STATUS } from "constants/module";
type Props = {
control: Control<IModule, any>;

View File

@ -10,7 +10,7 @@ import { CustomSelect } from "components/ui";
import { IModule } from "types";
// common
// constants
import { MODULE_STATUS } from "constants/";
import { MODULE_STATUS } from "constants/module";
type Props = {
control: Control<Partial<IModule>, any>;

View File

@ -14,7 +14,7 @@ import { renderShortNumericDateFormat } from "helpers/date-time.helper";
// types
import { IModule, SelectModuleType } from "types";
// common
import { MODULE_STATUS } from "constants/";
import { MODULE_STATUS } from "constants/module";
type Props = {
module: IModule;

View File

@ -16,10 +16,10 @@ import workspaceService from "services/workspace.service";
import { CustomSelect, Input } from "components/ui";
// types
import { IWorkspace, IWorkspaceMemberInvitation } from "types";
// constants
import { companySize } from "constants/";
// fetch-keys
import { USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys";
// constants
import { COMPANY_SIZE } from "constants/workspace";
type Props = {
setStep: React.Dispatch<React.SetStateAction<number>>;
@ -186,7 +186,7 @@ const Workspace: React.FC<Props> = ({ setStep, setWorkspace }) => {
label={value ? value.toString() : "Select company size"}
input
>
{companySize?.map((item) => (
{COMPANY_SIZE?.map((item) => (
<CustomSelect.Option key={item.value} value={item.value}>
{item.label}
</CustomSelect.Option>

View File

@ -17,13 +17,13 @@ import { Button, Input, TextArea, CustomSelect } from "components/ui";
// components
import EmojiIconPicker from "components/emoji-icon-picker";
// helpers
import { getRandomEmoji } from "helpers/functions.helper";
import { getRandomEmoji } from "helpers/common.helper";
// types
import { IProject } from "types";
// fetch-keys
import { PROJECTS_LIST, WORKSPACE_MEMBERS_ME } from "constants/fetch-keys";
// constants
import { NETWORK_CHOICES } from "constants/";
import { NETWORK_CHOICES } from "constants/project";
type Props = {
isOpen: boolean;

View File

@ -8,8 +8,8 @@ import { useForm, Controller } from "react-hook-form";
import { Dialog, Transition, Listbox } from "@headlessui/react";
// ui
import { ChevronDownIcon, CheckIcon } from "@heroicons/react/20/solid";
import { Button, CustomSelect, Select, TextArea } from "components/ui";
import { ChevronDownIcon } from "@heroicons/react/20/solid";
import { Button, CustomSelect, TextArea } from "components/ui";
// hooks
import useToast from "hooks/use-toast";
// services
@ -17,10 +17,10 @@ import projectService from "services/project.service";
import workspaceService from "services/workspace.service";
// types
import { IProjectMemberInvitation } from "types";
// constants
import { ROLE } from "constants/";
// fetch - keys
import { PROJECT_INVITATIONS, WORKSPACE_MEMBERS } from "constants/fetch-keys";
// icons
// constants
import { ROLE } from "constants/workspace";
type Props = {
isOpen: boolean;

View File

@ -21,7 +21,7 @@ type Props = {
const defaultValues: Partial<IIssueLabels> = {
name: "",
colour: "#ff0000",
color: "#ff0000",
};
const SingleLabel: React.FC<Props> = ({ label, issueLabels, editLabel, handleLabelDelete }) => {
@ -45,7 +45,7 @@ const SingleLabel: React.FC<Props> = ({ label, issueLabels, editLabel, handleLab
<span
className="h-3 w-3 flex-shrink-0 rounded-full"
style={{
backgroundColor: label.colour,
backgroundColor: label.color,
}}
/>
<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"
}`}
>
{watch("colour") && watch("colour") !== "" && (
{watch("color") && watch("color") !== "" && (
<span
className="h-4 w-4 rounded"
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">
<Controller
name="colour"
name="color"
control={control}
render={({ field: { value, onChange } }) => (
<TwitterPicker

View File

@ -143,7 +143,13 @@ export const ProjectSidebarList: FC = () => {
sidebarCollapse ? "" : "ml-[2.25rem]"
} flex flex-col gap-y-1`}
>
{navigation(workspaceSlug as string, project?.id).map((item) => (
{navigation(workspaceSlug as string, project?.id).map((item) => {
const hi = "hi";
if (item.name === "Cycles" && !project.cycle_view) return;
if (item.name === "Modules" && !project.module_view) return;
return (
<Link key={item.name} href={item.href}>
<a
className={`group flex items-center rounded-md px-2 py-2 text-xs font-medium outline-none ${
@ -163,7 +169,8 @@ export const ProjectSidebarList: FC = () => {
{!sidebarCollapse && item.name}
</a>
</Link>
))}
);
})}
</Disclosure.Panel>
</Transition>
</>

View File

@ -153,7 +153,7 @@ const RemirrorRichTextEditor: FC<IRemirrorRichTextEditor> = (props) => {
(value: any) => {
// 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
manager.view.updateState(manager.createState({ content: value }));
manager.view.updateState(manager.createState({ content: value ? value : "" }));
},
[manager]
);

View File

@ -13,13 +13,13 @@ import stateService from "services/state.service";
// hooks
import useToast from "hooks/use-toast";
// ui
import { Button, CustomSelect, Input, Select } from "components/ui";
import { Button, CustomSelect, Input } from "components/ui";
// types
import type { IState } from "types";
// fetch-keys
import { STATE_LIST } from "constants/fetch-keys";
// constants
import { GROUP_CHOICES } from "constants/";
import { GROUP_CHOICES } from "constants/project";
type Props = {
workspaceSlug?: string;

View File

@ -21,7 +21,7 @@ import type { IState } from "types";
// fetch keys
import { STATE_LIST } from "constants/fetch-keys";
// constants
import { GROUP_CHOICES } from "constants/";
import { GROUP_CHOICES } from "constants/project";
// types
type Props = {

View File

@ -13,6 +13,8 @@ import useToast from "hooks/use-toast";
import { IWorkspaceMemberInvitation } from "types";
// fetch keys
import { WORKSPACE_INVITATIONS } from "constants/fetch-keys";
// constants
import { ROLE } from "constants/workspace";
type Props = {
isOpen: boolean;
@ -21,13 +23,6 @@ type Props = {
members: any[];
};
const ROLE = {
5: "Guest",
10: "Viewer",
15: "Member",
20: "Admin",
};
const defaultValues: Partial<IWorkspaceMemberInvitation> = {
email: "",
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(
(key: keyof Properties) => {
if (!workspaceSlug || !user) return;
setProperties((prev) => ({ ...prev, [key]: !prev[key] }));
if (issueProperties && projectId) {
mutateIssueProperties(

View File

@ -11,11 +11,11 @@ import { issueViewContext } from "contexts/issue-view.context";
// helpers
import { groupBy, orderArrayBy } from "helpers/array.helper";
// types
import { IIssue } from "types";
import { IIssue, IState } from "types";
// fetch-keys
import { STATE_LIST } from "constants/fetch-keys";
// constants
import { PRIORITIES } from "constants/";
import { PRIORITIES } from "constants/project";
const useIssueView = (projectIssues: IIssue[]) => {
const {
@ -44,28 +44,52 @@ const useIssueView = (projectIssues: IIssue[]) => {
let groupedByIssues: {
[key: string]: IIssue[];
} = {
} = {};
const groupIssues = (states: IState[], issues: IIssue[]) => ({
...(groupByProperty === "state_detail.name"
? Object.fromEntries(
states
?.sort((a, b) => a.sequence - b.sequence)
?.map((state) => [
state.name,
projectIssues.filter((issue) => issue.state === state.name) ?? [],
issues.filter((issue) => issue.state === state.name) ?? [],
]) ?? []
)
: groupByProperty === "priority"
? Object.fromEntries(
PRIORITIES.map((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) {
groupedByIssues = Object.fromEntries(
@ -76,36 +100,9 @@ const useIssueView = (projectIssues: IIssue[]) => {
);
}
if (filterIssue) {
if (filterIssue === "activeIssue") {
const filteredStates = states?.filter(
(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);
if (groupByProperty === "priority") {
delete groupedByIssues.None;
if (orderBy === "priority") setOrderBy("created_at");
}
return {

View File

@ -13,7 +13,7 @@ import { Properties, NestedKeyOf, IIssue } from "types";
// fetch-keys
import { STATE_LIST } from "constants/fetch-keys";
// constants
import { PRIORITIES } from "constants/";
import { PRIORITIES } from "constants/project";
const initialValues: Properties = {
key: true,

View File

@ -1,30 +1,38 @@
import React, { FC, useState } from "react";
import { useRouter } from "next/router";
import Link from "next/link";
import useSWR from "swr";
// services
import projectService from "services/project.service";
// hooks
import useUser from "hooks/use-user";
// ui
import { Spinner } from "components/ui";
import { Button, Spinner } from "components/ui";
// components
import { NotAuthorizedView } from "components/core";
import CommandPalette from "components/command-palette";
import { JoinProject } from "components/project";
// local components
import Container from "layouts/container";
import AppSidebar from "./app-sidebar";
import AppHeader from "./app-header";
import AppSidebar from "layouts/app-layout/app-sidebar";
import AppHeader from "layouts/app-layout/app-header";
import SettingsSidebar from "layouts/settings-layout/settings-sidebar";
// types
import { UserAuth } from "types";
// fetch-keys
import { PROJECT_MEMBERS } from "constants/fetch-keys";
export type Meta = {
type Meta = {
title?: string | null;
description?: string | null;
image?: string | null;
url?: string | null;
};
export interface AppLayoutProps {
type AppLayoutProps = {
meta?: Meta;
children: React.ReactNode;
noPadding?: boolean;
@ -33,7 +41,60 @@ export interface AppLayoutProps {
breadcrumbs?: JSX.Element;
left?: 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> = ({
meta,
@ -44,16 +105,18 @@ const AppLayout: FC<AppLayoutProps> = ({
breadcrumbs,
left,
right,
settingsLayout,
memberType,
}) => {
// states
const [toggleSidebar, setToggleSidebar] = useState(false);
const [isJoiningProject, setIsJoiningProject] = useState(false);
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// user info
const { user } = useUser();
// fetching Project Members information
const { data: projectMembers, mutate: projectMembersMutate } = useSWR(
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null,
workspaceSlug && projectId
@ -86,6 +149,38 @@ const AppLayout: FC<AppLayoutProps> = ({
<CommandPalette />
<div className="flex h-screen w-full overflow-x-hidden">
<AppSidebar toggleSidebar={toggleSidebar} setToggleSidebar={setToggleSidebar} />
{settingsLayout && (memberType?.isGuest || memberType?.isViewer) ? (
<NotAuthorizedView
actionButton={
(memberType?.isViewer || memberType?.isGuest) && projectId ? (
<Link href={`/${workspaceSlug}/projects/${projectId}/issues`}>
<Button size="sm" theme="secondary">
Go to Issues
</Button>
</Link>
) : (
(memberType?.isViewer || memberType?.isGuest) &&
workspaceSlug && (
<Link href={`/${workspaceSlug}`}>
<Button size="sm" theme="secondary">
Go to workspace
</Button>
</Link>
)
)
}
/>
) : (
<>
{settingsLayout && (
<SettingsSidebar
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
@ -95,15 +190,20 @@ const AppLayout: FC<AppLayoutProps> = ({
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 ? "" : "p-5"} ${
bg === "primary" ? "bg-primary" : bg === "secondary" ? "bg-secondary" : "bg-primary"
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}
@ -112,6 +212,8 @@ const AppLayout: FC<AppLayoutProps> = ({
<JoinProject isJoiningProject={isJoiningProject} handleJoin={handleJoin} />
)}
</main>
</>
)}
</div>
</Container>
);

View File

@ -2,8 +2,6 @@ import React from "react";
// next
import Head from "next/head";
import { useRouter } from "next/router";
// types
import type { Props } from "./types";
// constants
import {
SITE_NAME,
@ -14,6 +12,24 @@ import {
SITE_TITLE,
} 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 router = useRouter();
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
import { ArrowLeftIcon } from "@heroicons/react/24/outline";
import Link from "next/link";
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 Link from "next/link";
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
import {
ArrowRightIcon,
CalendarDaysIcon,
ClipboardDocumentListIcon,
} 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 { getPriorityIcon } from "components/icons/priority-icon";
// helpers
import { renderShortNumericDateFormat, findHowManyDaysLeft } from "helpers/date-time.helper";
import { addSpaceIfCamelCase } from "helpers/string.helper";
import { groupBy } from "helpers/array.helper";
// types
import type { NextPage, NextPageContext } from "next";
// constants
import { getPriorityIcon } from "constants/global";
const WorkspacePage: NextPage = () => {
// router

View File

@ -9,7 +9,7 @@ import { Controller, useForm } from "react-hook-form";
// lib
import { requiredAdmin } from "lib/auth";
// layouts
import SettingsLayout from "layouts/settings-layout";
import AppLayout from "layouts/app-layout";
// services
import projectService from "services/project.service";
import workspaceService from "services/workspace.service";
@ -103,8 +103,8 @@ const ControlSettings: NextPage<TControlSettingsProps> = (props) => {
};
return (
<SettingsLayout
type="project"
<AppLayout
settingsLayout="project"
memberType={{ isMember, isOwner, isViewer, isGuest }}
breadcrumbs={
<Breadcrumbs>
@ -247,7 +247,7 @@ const ControlSettings: NextPage<TControlSettingsProps> = (props) => {
</div>
</div>
</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
import { Controller, useForm } from "react-hook-form";
import { IProject, IWorkspace } from "types";
import { IProject, IWorkspace, UserAuth } from "types";
// lib
import { requiredAdmin } from "lib/auth";
// layouts
import SettingsLayout from "layouts/settings-layout";
import AppLayout from "layouts/app-layout";
// services
import projectService from "services/project.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 OutlineButton from "components/ui/outline-button";
// helpers
import { debounce } from "helpers/functions.helper";
import { debounce } from "helpers/common.helper";
// types
import type { NextPage, NextPageContext } from "next";
// fetch-keys
import { PROJECTS_LIST, PROJECT_DETAILS, WORKSPACE_DETAILS } from "constants/fetch-keys";
// constants
import { NETWORK_CHOICES } from "constants/";
import { NETWORK_CHOICES } from "constants/project";
const defaultValues: Partial<IProject> = {
name: "",
@ -39,14 +39,7 @@ const defaultValues: Partial<IProject> = {
network: 0,
};
type TGeneralSettingsProps = {
isMember: boolean;
isOwner: boolean;
isViewer: boolean;
isGuest: boolean;
};
const GeneralSettings: NextPage<TGeneralSettingsProps> = (props) => {
const GeneralSettings: NextPage<UserAuth> = (props) => {
const { isMember, isOwner, isViewer, isGuest } = props;
const [selectProject, setSelectedProject] = useState<string | null>(null);
@ -100,6 +93,7 @@ const GeneralSettings: NextPage<TGeneralSettingsProps> = (props) => {
const onSubmit = async (formData: IProject) => {
if (!activeWorkspace || !projectDetails) return;
const payload: Partial<IProject> = {
name: formData.name,
network: formData.network,
@ -109,6 +103,7 @@ const GeneralSettings: NextPage<TGeneralSettingsProps> = (props) => {
project_lead: formData.project_lead,
icon: formData.icon,
};
await projectService
.updateProject(activeWorkspace.slug, projectDetails.id, payload)
.then((res) => {
@ -130,9 +125,9 @@ const GeneralSettings: NextPage<TGeneralSettingsProps> = (props) => {
};
return (
<SettingsLayout
<AppLayout
settingsLayout="project"
memberType={{ isMember, isOwner, isViewer, isGuest }}
type="project"
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem
@ -341,7 +336,7 @@ const GeneralSettings: NextPage<TGeneralSettingsProps> = (props) => {
</div>
</div>
</form>
</SettingsLayout>
</AppLayout>
);
};

View File

@ -1,11 +1,15 @@
import React, { useState } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
// react-hook-form
import { Controller, SubmitHandler, useForm } from "react-hook-form";
import { PlusIcon } from "@heroicons/react/24/outline";
import { Popover, Transition } from "@headlessui/react";
// react-color
import { TwitterPicker } from "react-color";
import type { NextPageContext, NextPage } from "next";
// headless ui
import { Popover, Transition } from "@headlessui/react";
// services
import projectService from "services/project.service";
import workspaceService from "services/workspace.service";
@ -13,20 +17,23 @@ import issuesService from "services/issues.service";
// lib
import { requiredAdmin } from "lib/auth";
// layouts
import SettingsLayout from "layouts/settings-layout";
import AppLayout from "layouts/app-layout";
// components
import SingleLabel from "components/project/settings/single-label";
// ui
import { Button, Input, Loader } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// fetch-keys
import { PROJECT_DETAILS, PROJECT_ISSUE_LABELS, WORKSPACE_DETAILS } from "constants/fetch-keys";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
// 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> = {
name: "",
colour: "#ff0000",
color: "#ff0000",
};
type TLabelSettingsProps = {
@ -52,7 +59,7 @@ const LabelsSettings: NextPage<TLabelSettingsProps> = (props) => {
() => (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
? () => projectService.getProject(workspaceSlug as string, projectId as string)
@ -77,9 +84,9 @@ const LabelsSettings: NextPage<TLabelSettingsProps> = (props) => {
);
const handleNewLabel: SubmitHandler<IIssueLabels> = async (formData) => {
if (!activeWorkspace || !activeProject || isSubmitting) return;
if (!activeWorkspace || !projectDetails || isSubmitting) return;
await issuesService
.createIssueLabel(activeWorkspace.slug, activeProject.id, formData)
.createIssueLabel(activeWorkspace.slug, projectDetails.id, formData)
.then((res) => {
reset(defaultValues);
mutate((prevData) => [...(prevData ?? []), res], false);
@ -89,16 +96,17 @@ const LabelsSettings: NextPage<TLabelSettingsProps> = (props) => {
const editLabel = (label: IIssueLabels) => {
setNewLabelForm(true);
setValue("colour", label.colour);
setValue("color", label.color);
setValue("name", label.name);
setIsUpdating(true);
setLabelIdForUpdate(label.id);
};
const handleLabelUpdate: SubmitHandler<IIssueLabels> = async (formData) => {
if (!activeWorkspace || !activeProject || isSubmitting) return;
if (!activeWorkspace || !projectDetails || isSubmitting) return;
await issuesService
.patchIssueLabel(activeWorkspace.slug, activeProject.id, labelIdForUpdate ?? "", formData)
.patchIssueLabel(activeWorkspace.slug, projectDetails.id, labelIdForUpdate ?? "", formData)
.then((res) => {
console.log(res);
reset(defaultValues);
@ -112,10 +120,10 @@ const LabelsSettings: NextPage<TLabelSettingsProps> = (props) => {
};
const handleLabelDelete = (labelId: string) => {
if (activeWorkspace && activeProject) {
if (activeWorkspace && projectDetails) {
mutate((prevData) => prevData?.filter((p) => p.id !== labelId), false);
issuesService
.deleteIssueLabel(activeWorkspace.slug, activeProject.id, labelId)
.deleteIssueLabel(activeWorkspace.slug, projectDetails.id, labelId)
.then((res) => {
console.log(res);
})
@ -126,14 +134,14 @@ const LabelsSettings: NextPage<TLabelSettingsProps> = (props) => {
};
return (
<SettingsLayout
type="project"
<AppLayout
settingsLayout="project"
memberType={{ isMember, isOwner, isViewer, isGuest }}
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem
title={`${activeProject?.name ?? "Project"}`}
link={`/${workspaceSlug}/projects/${activeProject?.id}/issues`}
title={`${projectDetails?.name ?? "Project"}`}
link={`/${workspaceSlug}/projects/${projectDetails?.id}/issues`}
/>
<BreadcrumbItem title="Labels Settings" />
</Breadcrumbs>
@ -170,11 +178,11 @@ const LabelsSettings: NextPage<TLabelSettingsProps> = (props) => {
open ? "text-gray-900" : "text-gray-500"
}`}
>
{watch("colour") && watch("colour") !== "" && (
{watch("color") && watch("color") !== "" && (
<span
className="h-4 w-4 rounded"
style={{
backgroundColor: watch("colour") ?? "green",
backgroundColor: watch("color") ?? "green",
}}
/>
)}
@ -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">
<Controller
name="colour"
name="color"
control={control}
render={({ field: { value, onChange } }) => (
<TwitterPicker
@ -258,7 +266,7 @@ const LabelsSettings: NextPage<TLabelSettingsProps> = (props) => {
</>
</div>
</section>
</SettingsLayout>
</AppLayout>
);
};

View File

@ -2,9 +2,8 @@ import { useState } from "react";
import Image from "next/image";
import { useRouter } from "next/router";
import useSWR from "swr";
import { PlusIcon } from "@heroicons/react/24/outline";
import type { NextPage, NextPageContext } from "next";
// services
import projectService from "services/project.service";
@ -13,10 +12,8 @@ import workspaceService from "services/workspace.service";
import { requiredAdmin } from "lib/auth";
// hooks
import useToast from "hooks/use-toast";
// constants
import { ROLE } from "constants/";
// layouts
import SettingsLayout from "layouts/settings-layout";
import AppLayout from "layouts/app-layout";
// components
import ConfirmProjectMemberRemove from "components/project/confirm-project-member-remove";
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 { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
// types
import type { NextPage, NextPageContext } from "next";
// fetch-keys
import {
PROJECT_DETAILS,
@ -31,6 +31,8 @@ import {
PROJECT_MEMBERS,
WORKSPACE_DETAILS,
} from "constants/fetch-keys";
// constants
import { ROLE } from "constants/workspace";
type TMemberSettingsProps = {
isMember: boolean;
@ -58,7 +60,7 @@ const MembersSettings: NextPage<TMemberSettingsProps> = (props) => {
() => (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
? () => projectService.getProject(workspaceSlug as string, projectId as string)
@ -120,11 +122,11 @@ const MembersSettings: NextPage<TMemberSettingsProps> = (props) => {
(item) => item.id === selectedRemoveMember || item.id === selectedInviteRemoveMember
)}
handleDelete={async () => {
if (!activeWorkspace || !activeProject) return;
if (!activeWorkspace || !projectDetails) return;
if (selectedRemoveMember) {
await projectService.deleteProjectMember(
activeWorkspace.slug,
activeProject.id,
projectDetails.id,
selectedRemoveMember
);
mutateMembers(
@ -135,7 +137,7 @@ const MembersSettings: NextPage<TMemberSettingsProps> = (props) => {
if (selectedInviteRemoveMember) {
await projectService.deleteProjectInvitation(
activeWorkspace.slug,
activeProject.id,
projectDetails.id,
selectedInviteRemoveMember
);
mutateInvitations(
@ -155,14 +157,14 @@ const MembersSettings: NextPage<TMemberSettingsProps> = (props) => {
setIsOpen={setInviteModal}
members={members}
/>
<SettingsLayout
type="project"
<AppLayout
settingsLayout="project"
memberType={{ isMember, isOwner, isViewer, isGuest }}
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem
title={`${activeProject?.name ?? "Project"}`}
link={`/${workspaceSlug}/projects/${activeProject?.id}/issues`}
title={`${projectDetails?.name ?? "Project"}`}
link={`/${workspaceSlug}/projects/${projectDetails?.id}/issues`}
/>
<BreadcrumbItem title="Members Settings" />
</Breadcrumbs>
@ -235,11 +237,11 @@ const MembersSettings: NextPage<TMemberSettingsProps> = (props) => {
title={ROLE[member.role as keyof typeof ROLE] ?? "Select Role"}
value={member.role}
onChange={(value) => {
if (!activeWorkspace || !activeProject) return;
if (!activeWorkspace || !projectDetails) return;
projectService
.updateProjectMember(
activeWorkspace.slug,
activeProject.id,
projectDetails.id,
member.id,
{
role: value,
@ -306,7 +308,7 @@ const MembersSettings: NextPage<TMemberSettingsProps> = (props) => {
</div>
)}
</section>
</SettingsLayout>
</AppLayout>
</>
);
};

View File

@ -11,7 +11,7 @@ import projectService from "services/project.service";
// lib
import { requiredAdmin } from "lib/auth";
// layouts
import SettingsLayout from "layouts/settings-layout";
import AppLayout from "layouts/app-layout";
// components
import { CreateUpdateStateInline, DeleteStateModal, StateGroup } from "components/states";
// ui
@ -43,7 +43,7 @@ const StatesSettings: NextPage<TStateSettingsProps> = (props) => {
query: { workspaceSlug, projectId },
} = useRouter();
const { data: activeProject } = useSWR(
const { data: projectDetails } = useSWR(
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
workspaceSlug && projectId
? () => 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}
onClose={() => setSelectDeleteState(null)}
/>
<SettingsLayout
type="project"
<AppLayout
settingsLayout="project"
memberType={{ isMember, isOwner, isViewer, isGuest }}
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem
title={`${activeProject?.name ?? "Project"}`}
link={`/${workspaceSlug}/projects/${activeProject?.id}/issues`}
title={`${projectDetails?.name ?? "Project"}`}
link={`/${workspaceSlug}/projects/${projectDetails?.id}/issues`}
/>
<BreadcrumbItem title="States Settings" />
</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>
</div>
<div className="flex flex-col justify-between gap-4">
{states && activeProject ? (
{states && projectDetails ? (
Object.keys(groupedStates).map((key) => (
<div key={key}>
<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">
{key === activeGroup && (
<CreateUpdateStateInline
projectId={activeProject.id}
projectId={projectDetails.id}
onClose={() => {
setActiveGroup(null);
setSelectedState(null);
@ -143,7 +143,7 @@ const StatesSettings: NextPage<TStateSettingsProps> = (props) => {
) : (
<div className="border-b last:border-b-0" key={state.id}>
<CreateUpdateStateInline
projectId={activeProject.id}
projectId={projectDetails.id}
onClose={() => {
setActiveGroup(null);
setSelectedState(null);
@ -168,7 +168,7 @@ const StatesSettings: NextPage<TStateSettingsProps> = (props) => {
)}
</div>
</div>
</SettingsLayout>
</AppLayout>
</>
);
};

View File

@ -1,19 +1,21 @@
import React from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
// lib
import type { NextPage, GetServerSideProps } from "next";
import { requiredWorkspaceAdmin } from "lib/auth";
// constants
// services
import workspaceService from "services/workspace.service";
// layouts
import SettingsLayout from "layouts/settings-layout";
import AppLayout from "layouts/app-layout";
// ui
import { Button } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// types
import type { NextPage, GetServerSideProps } from "next";
// fetch-keys
import { WORKSPACE_DETAILS } from "constants/fetch-keys";
type TBillingSettingsProps = {
@ -35,9 +37,9 @@ const BillingSettings: NextPage<TBillingSettingsProps> = (props) => {
return (
<>
<SettingsLayout
memberType={{ ...props }}
type="workspace"
<AppLayout
settingsLayout="workspace"
memberType={props}
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem
@ -75,7 +77,7 @@ const BillingSettings: NextPage<TBillingSettingsProps> = (props) => {
</div>
</div>
</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 fileServices from "services/file.service";
// layouts
import SettingsLayout from "layouts/settings-layout";
import AppLayout from "layouts/app-layout";
// hooks
import useToast from "hooks/use-toast";
// components
@ -35,7 +35,7 @@ import type { GetServerSideProps, NextPage } from "next";
// fetch-keys
import { WORKSPACE_DETAILS, USER_WORKSPACES } from "constants/fetch-keys";
// constants
import { companySize } from "constants/";
import { COMPANY_SIZE } from "constants/workspace";
const defaultValues: Partial<IWorkspace> = {
name: "",
@ -85,11 +85,13 @@ const WorkspaceSettings: NextPage<TWorkspaceSettingsProps> = (props) => {
const onSubmit = async (formData: IWorkspace) => {
if (!activeWorkspace) return;
const payload: Partial<IWorkspace> = {
logo: formData.logo,
name: formData.name,
company_size: formData.company_size,
};
await workspaceService
.updateWorkspace(activeWorkspace.slug, payload)
.then((res) => {
@ -106,9 +108,9 @@ const WorkspaceSettings: NextPage<TWorkspaceSettingsProps> = (props) => {
};
return (
<SettingsLayout
memberType={{ ...props }}
type="workspace"
<AppLayout
settingsLayout="workspace"
memberType={props}
meta={{
title: "Plane - Workspace Settings",
}}
@ -278,7 +280,7 @@ const WorkspaceSettings: NextPage<TWorkspaceSettingsProps> = (props) => {
label={value ? value.toString() : "Select company size"}
input
>
{companySize?.map((item) => (
{COMPANY_SIZE?.map((item) => (
<CustomSelect.Option key={item.value} value={item.value}>
{item.label}
</CustomSelect.Option>
@ -315,7 +317,7 @@ const WorkspaceSettings: NextPage<TWorkspaceSettingsProps> = (props) => {
<Spinner />
</div>
)}
</SettingsLayout>
</AppLayout>
);
};

View File

@ -2,29 +2,31 @@ import { useState } from "react";
import Image from "next/image";
import { useRouter } from "next/router";
import useSWR from "swr";
import { PlusIcon } from "@heroicons/react/24/outline";
// lib
import type { GetServerSideProps, NextPage } from "next";
import { requiredWorkspaceAdmin } from "lib/auth";
// hooks
import useToast from "hooks/use-toast";
// services
import workspaceService from "services/workspace.service";
// constants
// layouts
import SettingsLayout from "layouts/settings-layout";
import AppLayout from "layouts/app-layout";
// components
import ConfirmWorkspaceMemberRemove from "components/workspace/confirm-workspace-member-remove";
import SendWorkspaceInvitationModal from "components/workspace/send-workspace-invitation-modal";
// ui
import { Button, CustomListbox, CustomMenu, Loader } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
import { ROLE } from "constants/";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
// types
import type { GetServerSideProps, NextPage } from "next";
// fetch-keys
import { WORKSPACE_DETAILS, WORKSPACE_INVITATIONS, WORKSPACE_MEMBERS } from "constants/fetch-keys";
// constants
import { ROLE } from "constants/workspace";
type TMembersSettingsProps = {
isOwner: boolean;
@ -135,11 +137,9 @@ const MembersSettings: NextPage<TMembersSettingsProps> = (props) => {
workspace_slug={workspaceSlug as string}
members={members}
/>
<SettingsLayout
memberType={{
...props,
}}
type="workspace"
<AppLayout
settingsLayout="workspace"
memberType={props}
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem
@ -283,7 +283,7 @@ const MembersSettings: NextPage<TMembersSettingsProps> = (props) => {
</div>
)}
</section>
</SettingsLayout>
</AppLayout>
</>
);
};

View File

@ -25,7 +25,7 @@ import type { NextPage, NextPageContext } from "next";
// fetch-keys
import { USER_WORKSPACES } from "constants/fetch-keys";
// constants
import { companySize } from "constants/";
import { COMPANY_SIZE } from "constants/workspace";
const defaultValues = {
name: "",
@ -145,7 +145,7 @@ const CreateWorkspace: NextPage = () => {
label={value ? value.toString() : "Select company size"}
input
>
{companySize?.map((item) => (
{COMPANY_SIZE?.map((item) => (
<CustomSelect.Option key={item.value} value={item.value}>
{item.label}
</CustomSelect.Option>

View File

@ -1,9 +1,9 @@
import React from "react";
import type { NextPage } from "next";
// layouts
import DefaultLayout from "layouts/default-layout";
// types
import type { NextPage } from "next";
const ErrorPage: NextPage = () => (
<DefaultLayout
@ -16,6 +16,6 @@ const ErrorPage: NextPage = () => (
<h2 className="text-3xl">Error!</h2>
</div>
</DefaultLayout>
);
);
export default ErrorPage;

View File

@ -4,15 +4,16 @@ import { useRouter } from "next/router";
import { mutate } from "swr";
// layouts
import DefaultLayout from "layouts/default-layout";
// services
import type { NextPage } from "next";
import authenticationService from "services/authentication.service";
// constants
// hooks
import useUser from "hooks/use-user";
import useToast from "hooks/use-toast";
// layouts
import DefaultLayout from "layouts/default-layout";
// types
import type { NextPage } from "next";
// constants
import { USER_WORKSPACES } from "constants/fetch-keys";
const MagicSignIn: NextPage = () => {

View File

@ -2,11 +2,13 @@ import { useState } from "react";
import Image from "next/image";
import { useRouter } from "next/router";
// hooks
import type { NextPage, NextPageContext } from "next";
import useUser from "hooks/use-user";
// lib
import { requiredAuth } from "lib/auth";
// services
import userService from "services/user.service";
// hooks
import useUser from "hooks/use-user";
// layouts
import DefaultLayout from "layouts/default-layout";
// components
@ -20,7 +22,8 @@ import InviteMembers from "components/onboarding/invite-members";
import CommandMenu from "components/onboarding/command-menu";
// images
import Logo from "public/onboarding/logo.svg";
import userService from "services/user.service";
// types
import type { NextPage, NextPageContext } from "next";
const Onboarding: NextPage = () => {
const [step, setStep] = useState(1);

View File

@ -13,7 +13,6 @@ import {
} from "@heroicons/react/24/outline";
// swr
// services
import type { NextPage } from "next";
import workspaceService from "services/workspace.service";
// hooks
import useUser from "hooks/use-user";
@ -23,6 +22,8 @@ import DefaultLayout from "layouts/default-layout";
import { Spinner } from "components/ui";
// icons
import { EmptySpace, EmptySpaceItem } from "components/ui/empty-space";
// types
import type { NextPage } from "next";
// constants
import { WORKSPACE_INVITATION } from "constants/fetch-keys";

View File

@ -171,7 +171,7 @@ export interface IIssueLabels {
updated_at: Date;
name: string;
description: string;
colour: string;
color: string;
created_by: string;
updated_by: string;
project: string;

View File

@ -1,20 +1,22 @@
import type { IUserLite, IWorkspace } from "./";
export interface IProject {
id: string;
workspace: IWorkspace | string;
default_assignee: IUser | string | null;
project_lead: IUser | string | null;
created_at: Date;
updated_at: Date;
name: string;
description: string;
network: number;
identifier: string;
slug: string;
created_by: string;
updated_by: string;
cycle_view: boolean;
default_assignee: IUser | string | null;
description: 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 = {