mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
feat: cycles and modules toggle in settings, refactor: folder structure (#247)
* feat: link option in remirror * fix: removed link import from remirror toolbar * refactor: constants folder * refactor: layouts folder structure * fix: issue view context * feat: cycles and modules toggle in settings
This commit is contained in:
parent
4e27e93739
commit
76cc634a46
@ -39,48 +39,38 @@ export const AllBoards: React.FC<Props> = ({
|
||||
<div className="h-[calc(100vh-157px)] lg:h-[calc(100vh-115px)] w-full">
|
||||
<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="flex h-full gap-x-4 overflow-x-auto overflow-y-hidden">
|
||||
{Object.keys(groupedByIssues).map((singleGroup, index) => {
|
||||
const stateId =
|
||||
selectedGroup === "state_detail.name"
|
||||
? states?.find((s) => s.name === singleGroup)?.id ?? null
|
||||
: null;
|
||||
<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 =
|
||||
selectedGroup === "state_detail.name"
|
||||
? states?.find((s) => s.name === singleGroup)?.id ?? null
|
||||
: null;
|
||||
|
||||
const bgColor =
|
||||
selectedGroup === "state_detail.name"
|
||||
? states?.find((s) => s.name === singleGroup)?.color
|
||||
: "#000000";
|
||||
const bgColor =
|
||||
selectedGroup === "state_detail.name"
|
||||
? states?.find((s) => s.name === singleGroup)?.color
|
||||
: "#000000";
|
||||
|
||||
return (
|
||||
<SingleBoard
|
||||
key={index}
|
||||
index={index}
|
||||
type={type}
|
||||
bgColor={bgColor}
|
||||
groupTitle={singleGroup}
|
||||
groupedByIssues={groupedByIssues}
|
||||
selectedGroup={selectedGroup}
|
||||
members={members}
|
||||
addIssueToState={() => addIssueToState(singleGroup, stateId)}
|
||||
handleDeleteIssue={handleDeleteIssue}
|
||||
openIssuesListModal={openIssuesListModal ?? null}
|
||||
orderBy={orderBy}
|
||||
userAuth={userAuth}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</StrictModeDroppable>
|
||||
return (
|
||||
<SingleBoard
|
||||
key={index}
|
||||
type={type}
|
||||
bgColor={bgColor}
|
||||
groupTitle={singleGroup}
|
||||
groupedByIssues={groupedByIssues}
|
||||
selectedGroup={selectedGroup}
|
||||
members={members}
|
||||
addIssueToState={() => addIssueToState(singleGroup, stateId)}
|
||||
handleDeleteIssue={handleDeleteIssue}
|
||||
openIssuesListModal={openIssuesListModal ?? null}
|
||||
orderBy={orderBy}
|
||||
userAuth={userAuth}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DragDropContext>
|
||||
</div>
|
||||
|
@ -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" : ""
|
||||
|
@ -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,95 +68,79 @@ 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"}`}
|
||||
>
|
||||
<BoardHeader
|
||||
provided={provided}
|
||||
addIssueToState={addIssueToState}
|
||||
bgColor={bgColor}
|
||||
createdBy={createdBy}
|
||||
groupTitle={groupTitle}
|
||||
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 ${
|
||||
snapshot.isDraggingOver ? "bg-indigo-50 bg-opacity-50" : ""
|
||||
} ${!isCollapsed ? "hidden" : "block"}`}
|
||||
ref={provided.innerRef}
|
||||
{...provided.droppableProps}
|
||||
<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
|
||||
addIssueToState={addIssueToState}
|
||||
bgColor={bgColor}
|
||||
createdBy={createdBy}
|
||||
groupTitle={groupTitle}
|
||||
groupedByIssues={groupedByIssues}
|
||||
isCollapsed={isCollapsed}
|
||||
setIsCollapsed={setIsCollapsed}
|
||||
/>
|
||||
<StrictModeDroppable key={groupTitle} droppableId={groupTitle}>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
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}
|
||||
{...provided.droppableProps}
|
||||
>
|
||||
{groupedByIssues[groupTitle].map((issue, index: number) => (
|
||||
<SingleBoardIssue
|
||||
key={index}
|
||||
index={index}
|
||||
type={type}
|
||||
issue={issue}
|
||||
selectedGroup={selectedGroup}
|
||||
properties={properties}
|
||||
handleDeleteIssue={handleDeleteIssue}
|
||||
orderBy={orderBy}
|
||||
userAuth={userAuth}
|
||||
/>
|
||||
))}
|
||||
<span
|
||||
style={{
|
||||
display: orderBy === "manual" ? "inline" : "none",
|
||||
}}
|
||||
>
|
||||
{provided.placeholder}
|
||||
</span>
|
||||
{type === "issue" ? (
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center rounded p-2 text-xs font-medium outline-none duration-300 hover:bg-gray-100"
|
||||
onClick={addIssueToState}
|
||||
>
|
||||
{groupedByIssues[groupTitle].map((issue, index: number) => (
|
||||
<SingleBoardIssue
|
||||
key={index}
|
||||
index={index}
|
||||
type={type}
|
||||
issue={issue}
|
||||
properties={properties}
|
||||
members={members}
|
||||
handleDeleteIssue={handleDeleteIssue}
|
||||
orderBy={orderBy}
|
||||
userAuth={userAuth}
|
||||
/>
|
||||
))}
|
||||
<span
|
||||
style={{
|
||||
display: orderBy === "manual" ? "inline" : "none",
|
||||
}}
|
||||
>
|
||||
{provided.placeholder}
|
||||
</span>
|
||||
{type === "issue" ? (
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center rounded p-2 text-xs font-medium outline-none duration-300 hover:bg-gray-100"
|
||||
onClick={addIssueToState}
|
||||
>
|
||||
<PlusIcon className="mr-1 h-3 w-3" />
|
||||
Create
|
||||
</button>
|
||||
) : (
|
||||
<CustomMenu
|
||||
label={
|
||||
<span className="flex items-center gap-1">
|
||||
<PlusIcon className="h-3 w-3" />
|
||||
Add issue
|
||||
</span>
|
||||
}
|
||||
className="mt-1"
|
||||
optionsPosition="left"
|
||||
noBorder
|
||||
>
|
||||
<CustomMenu.MenuItem onClick={addIssueToState}>
|
||||
Create new
|
||||
</CustomMenu.MenuItem>
|
||||
{openIssuesListModal && (
|
||||
<CustomMenu.MenuItem onClick={openIssuesListModal}>
|
||||
Add an existing issue
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
</CustomMenu>
|
||||
<PlusIcon className="mr-1 h-3 w-3" />
|
||||
Create
|
||||
</button>
|
||||
) : (
|
||||
<CustomMenu
|
||||
label={
|
||||
<span className="flex items-center gap-1">
|
||||
<PlusIcon className="h-3 w-3" />
|
||||
Add issue
|
||||
</span>
|
||||
}
|
||||
className="mt-1"
|
||||
optionsPosition="left"
|
||||
noBorder
|
||||
>
|
||||
<CustomMenu.MenuItem onClick={addIssueToState}>Create new</CustomMenu.MenuItem>
|
||||
{openIssuesListModal && (
|
||||
<CustomMenu.MenuItem onClick={openIssuesListModal}>
|
||||
Add an existing issue
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
</div>
|
||||
</CustomMenu>
|
||||
)}
|
||||
</StrictModeDroppable>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
</div>
|
||||
)}
|
||||
</StrictModeDroppable>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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)}
|
||||
|
@ -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,252 +86,192 @@ 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 draggedItem = groupedByIssues[source.droppableId][source.index];
|
||||
|
||||
const sequenceNumber =
|
||||
prevSequenceNumber && nextSequenceNumber
|
||||
? (prevSequenceNumber + nextSequenceNumber) / 2
|
||||
: nextSequenceNumber
|
||||
? nextSequenceNumber - 15000 / 2
|
||||
: prevSequenceNumber
|
||||
? prevSequenceNumber + 15000 / 2
|
||||
: 15000;
|
||||
if (source.droppableId !== destination.droppableId) {
|
||||
const sourceGroup = source.droppableId; // source group id
|
||||
const destinationGroup = destination.droppableId; // destination group id
|
||||
|
||||
newStates[destination.index].sequence = sequenceNumber;
|
||||
if (!sourceGroup || !destinationGroup) return;
|
||||
|
||||
mutateState(newStates, false);
|
||||
if (selectedGroup === "priority") {
|
||||
// update the removed item for mutation
|
||||
draggedItem.priority = destinationGroup;
|
||||
|
||||
stateService
|
||||
.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
|
||||
|
||||
if (!sourceGroup || !destinationGroup) return;
|
||||
|
||||
if (selectedGroup === "priority") {
|
||||
// update the removed item for mutation
|
||||
draggedItem.priority = destinationGroup;
|
||||
|
||||
if (cycleId)
|
||||
mutate<CycleIssueResponse[]>(
|
||||
CYCLE_ISSUES(cycleId as string),
|
||||
(prevData) => {
|
||||
if (!prevData) return prevData;
|
||||
const updatedIssues = prevData.map((issue) => {
|
||||
if (issue.issue_detail.id === draggedItem.id) {
|
||||
return {
|
||||
...issue,
|
||||
issue_detail: {
|
||||
...draggedItem,
|
||||
priority: destinationGroup,
|
||||
},
|
||||
};
|
||||
}
|
||||
return issue;
|
||||
});
|
||||
return [...updatedIssues];
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
if (moduleId)
|
||||
mutate<ModuleIssueResponse[]>(
|
||||
MODULE_ISSUES(moduleId as string),
|
||||
(prevData) => {
|
||||
if (!prevData) return prevData;
|
||||
const updatedIssues = prevData.map((issue) => {
|
||||
if (issue.issue_detail.id === draggedItem.id) {
|
||||
return {
|
||||
...issue,
|
||||
issue_detail: {
|
||||
...draggedItem,
|
||||
priority: destinationGroup,
|
||||
},
|
||||
};
|
||||
}
|
||||
return issue;
|
||||
});
|
||||
return [...updatedIssues];
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
mutate<IssueResponse>(
|
||||
PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string),
|
||||
if (cycleId)
|
||||
mutate<CycleIssueResponse[]>(
|
||||
CYCLE_ISSUES(cycleId as string),
|
||||
(prevData) => {
|
||||
if (!prevData) return prevData;
|
||||
|
||||
const updatedIssues = prevData.results.map((issue) => {
|
||||
if (issue.id === draggedItem.id)
|
||||
const updatedIssues = prevData.map((issue) => {
|
||||
if (issue.issue_detail.id === draggedItem.id) {
|
||||
return {
|
||||
...draggedItem,
|
||||
priority: destinationGroup,
|
||||
...issue,
|
||||
issue_detail: {
|
||||
...draggedItem,
|
||||
priority: destinationGroup,
|
||||
},
|
||||
};
|
||||
|
||||
}
|
||||
return issue;
|
||||
});
|
||||
|
||||
return {
|
||||
...prevData,
|
||||
results: updatedIssues,
|
||||
};
|
||||
return [...updatedIssues];
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
// patch request
|
||||
issuesService
|
||||
.patchIssue(workspaceSlug as string, projectId as string, draggedItem.id, {
|
||||
priority: destinationGroup,
|
||||
})
|
||||
.then((res) => {
|
||||
mutate(
|
||||
cycleId
|
||||
? CYCLE_ISSUES(cycleId as string)
|
||||
: CYCLE_ISSUES(draggedItem.issue_cycle?.cycle ?? "")
|
||||
);
|
||||
mutate(
|
||||
moduleId
|
||||
? MODULE_ISSUES(moduleId as string)
|
||||
: MODULE_ISSUES(draggedItem.issue_module?.module ?? "")
|
||||
);
|
||||
|
||||
mutate(PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string));
|
||||
});
|
||||
} else if (selectedGroup === "state_detail.name") {
|
||||
const destinationState = states?.find((s) => s.name === destinationGroup);
|
||||
const destinationStateId = destinationState?.id;
|
||||
|
||||
// update the removed item for mutation
|
||||
if (!destinationStateId || !destinationState) return;
|
||||
draggedItem.state = destinationStateId;
|
||||
draggedItem.state_detail = destinationState;
|
||||
|
||||
if (cycleId)
|
||||
mutate<CycleIssueResponse[]>(
|
||||
CYCLE_ISSUES(cycleId as string),
|
||||
(prevData) => {
|
||||
if (!prevData) return prevData;
|
||||
const updatedIssues = prevData.map((issue) => {
|
||||
if (issue.issue_detail.id === draggedItem.id) {
|
||||
return {
|
||||
...issue,
|
||||
issue_detail: {
|
||||
...draggedItem,
|
||||
state_detail: destinationState,
|
||||
state: destinationStateId,
|
||||
},
|
||||
};
|
||||
}
|
||||
return issue;
|
||||
});
|
||||
return [...updatedIssues];
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
if (moduleId)
|
||||
mutate<ModuleIssueResponse[]>(
|
||||
MODULE_ISSUES(moduleId as string),
|
||||
(prevData) => {
|
||||
if (!prevData) return prevData;
|
||||
const updatedIssues = prevData.map((issue) => {
|
||||
if (issue.issue_detail.id === draggedItem.id) {
|
||||
return {
|
||||
...issue,
|
||||
issue_detail: {
|
||||
...draggedItem,
|
||||
state_detail: destinationState,
|
||||
state: destinationStateId,
|
||||
},
|
||||
};
|
||||
}
|
||||
return issue;
|
||||
});
|
||||
return [...updatedIssues];
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
mutate<IssueResponse>(
|
||||
PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string),
|
||||
if (moduleId)
|
||||
mutate<ModuleIssueResponse[]>(
|
||||
MODULE_ISSUES(moduleId as string),
|
||||
(prevData) => {
|
||||
if (!prevData) return prevData;
|
||||
|
||||
const updatedIssues = prevData.results.map((issue) => {
|
||||
if (issue.id === draggedItem.id)
|
||||
const updatedIssues = prevData.map((issue) => {
|
||||
if (issue.issue_detail.id === draggedItem.id) {
|
||||
return {
|
||||
...draggedItem,
|
||||
state_detail: destinationState,
|
||||
state: destinationStateId,
|
||||
...issue,
|
||||
issue_detail: {
|
||||
...draggedItem,
|
||||
priority: destinationGroup,
|
||||
},
|
||||
};
|
||||
|
||||
}
|
||||
return issue;
|
||||
});
|
||||
|
||||
return {
|
||||
...prevData,
|
||||
results: updatedIssues,
|
||||
};
|
||||
return [...updatedIssues];
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
// patch request
|
||||
issuesService
|
||||
.patchIssue(workspaceSlug as string, projectId as string, draggedItem.id, {
|
||||
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 ?? "")
|
||||
);
|
||||
mutate(PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string));
|
||||
mutate<IssueResponse>(
|
||||
PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string),
|
||||
(prevData) => {
|
||||
if (!prevData) return prevData;
|
||||
|
||||
const updatedIssues = prevData.results.map((issue) => {
|
||||
if (issue.id === draggedItem.id)
|
||||
return {
|
||||
...draggedItem,
|
||||
priority: destinationGroup,
|
||||
};
|
||||
|
||||
return issue;
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...prevData,
|
||||
results: updatedIssues,
|
||||
};
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
// patch request
|
||||
issuesService
|
||||
.patchIssue(workspaceSlug as string, projectId as string, draggedItem.id, {
|
||||
priority: destinationGroup,
|
||||
})
|
||||
.then((res) => {
|
||||
if (cycleId) mutate(CYCLE_ISSUES(cycleId as string));
|
||||
if (moduleId) mutate(MODULE_ISSUES(moduleId as string));
|
||||
|
||||
mutate(PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string));
|
||||
});
|
||||
} else if (selectedGroup === "state_detail.name") {
|
||||
const destinationState = states?.find((s) => s.name === destinationGroup);
|
||||
const destinationStateId = destinationState?.id;
|
||||
|
||||
// update the removed item for mutation
|
||||
if (!destinationStateId || !destinationState) return;
|
||||
draggedItem.state = destinationStateId;
|
||||
draggedItem.state_detail = destinationState;
|
||||
|
||||
if (cycleId)
|
||||
mutate<CycleIssueResponse[]>(
|
||||
CYCLE_ISSUES(cycleId as string),
|
||||
(prevData) => {
|
||||
if (!prevData) return prevData;
|
||||
const updatedIssues = prevData.map((issue) => {
|
||||
if (issue.issue_detail.id === draggedItem.id) {
|
||||
return {
|
||||
...issue,
|
||||
issue_detail: {
|
||||
...draggedItem,
|
||||
state_detail: destinationState,
|
||||
state: destinationStateId,
|
||||
},
|
||||
};
|
||||
}
|
||||
return issue;
|
||||
});
|
||||
return [...updatedIssues];
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
if (moduleId)
|
||||
mutate<ModuleIssueResponse[]>(
|
||||
MODULE_ISSUES(moduleId as string),
|
||||
(prevData) => {
|
||||
if (!prevData) return prevData;
|
||||
const updatedIssues = prevData.map((issue) => {
|
||||
if (issue.issue_detail.id === draggedItem.id) {
|
||||
return {
|
||||
...issue,
|
||||
issue_detail: {
|
||||
...draggedItem,
|
||||
state_detail: destinationState,
|
||||
state: destinationStateId,
|
||||
},
|
||||
};
|
||||
}
|
||||
return issue;
|
||||
});
|
||||
return [...updatedIssues];
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
mutate<IssueResponse>(
|
||||
PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string),
|
||||
(prevData) => {
|
||||
if (!prevData) return prevData;
|
||||
|
||||
const updatedIssues = prevData.results.map((issue) => {
|
||||
if (issue.id === draggedItem.id)
|
||||
return {
|
||||
...draggedItem,
|
||||
state_detail: destinationState,
|
||||
state: destinationStateId,
|
||||
};
|
||||
|
||||
return issue;
|
||||
});
|
||||
|
||||
return {
|
||||
...prevData,
|
||||
results: updatedIssues,
|
||||
};
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
// patch request
|
||||
issuesService
|
||||
.patchIssue(workspaceSlug as string, projectId as string, draggedItem.id, {
|
||||
state: destinationStateId,
|
||||
})
|
||||
.then((res) => {
|
||||
if (cycleId) mutate(CYCLE_ISSUES(cycleId as string));
|
||||
if (moduleId) mutate(MODULE_ISSUES(moduleId as string));
|
||||
|
||||
mutate(PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string));
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
workspaceSlug,
|
||||
cycleId,
|
||||
moduleId,
|
||||
mutateState,
|
||||
groupedByIssues,
|
||||
projectId,
|
||||
selectedGroup,
|
||||
states,
|
||||
]
|
||||
[workspaceSlug, cycleId, moduleId, groupedByIssues, projectId, selectedGroup, states]
|
||||
);
|
||||
|
||||
const addIssueToState = (groupTitle: string, stateId: string | null) => {
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -101,7 +101,6 @@ export const SingleList: React.FC<Props> = ({
|
||||
type={type}
|
||||
issue={issue}
|
||||
properties={properties}
|
||||
members={members}
|
||||
editIssue={() => handleEditIssue(issue)}
|
||||
handleDeleteIssue={handleDeleteIssue}
|
||||
removeIssue={() => {
|
||||
|
@ -1,48 +0,0 @@
|
||||
// ui
|
||||
import { CustomDatePicker } from "components/ui";
|
||||
// helpers
|
||||
import { findHowManyDaysLeft, renderShortNumericDateFormat } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
partialUpdateIssue: (formData: Partial<IIssue>) => void;
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const DueDateSelect: React.FC<Props> = ({ issue, partialUpdateIssue, isNotAllowed }) => (
|
||||
<div
|
||||
className={`group relative ${
|
||||
issue.target_date === null
|
||||
? ""
|
||||
: issue.target_date < new Date().toISOString()
|
||||
? "text-red-600"
|
||||
: findHowManyDaysLeft(issue.target_date) <= 3 && "text-orange-400"
|
||||
}`}
|
||||
>
|
||||
<CustomDatePicker
|
||||
placeholder="N/A"
|
||||
value={issue?.target_date}
|
||||
onChange={(val) =>
|
||||
partialUpdateIssue({
|
||||
target_date: val,
|
||||
})
|
||||
}
|
||||
className={issue?.target_date ? "w-[6.5rem]" : "w-[3rem] text-center"}
|
||||
/>
|
||||
<div className="absolute bottom-full right-0 z-10 mb-2 hidden whitespace-nowrap rounded-md bg-white p-2 shadow-md group-hover:block">
|
||||
<h5 className="mb-1 font-medium text-gray-900">Due date</h5>
|
||||
<div>{renderShortNumericDateFormat(issue.target_date ?? "")}</div>
|
||||
<div>
|
||||
{issue.target_date
|
||||
? issue.target_date < new Date().toISOString()
|
||||
? `Due date has passed by ${findHowManyDaysLeft(issue.target_date)} days`
|
||||
: findHowManyDaysLeft(issue.target_date) <= 3
|
||||
? `Due date is in ${findHowManyDaysLeft(issue.target_date)} days`
|
||||
: "Due date"
|
||||
: "N/A"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
@ -1,97 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
// ui
|
||||
import { Listbox, Transition } from "@headlessui/react";
|
||||
// types
|
||||
import { IIssue, IState } from "types";
|
||||
// constants
|
||||
import { getPriorityIcon } from "constants/global";
|
||||
import { PRIORITIES } from "constants/";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
partialUpdateIssue: (formData: Partial<IIssue>) => void;
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const PrioritySelect: React.FC<Props> = ({ issue, partialUpdateIssue, isNotAllowed }) => (
|
||||
<Listbox
|
||||
as="div"
|
||||
value={issue.priority}
|
||||
onChange={(data: string) => {
|
||||
partialUpdateIssue({ priority: data });
|
||||
}}
|
||||
className="group relative flex-shrink-0"
|
||||
disabled={isNotAllowed}
|
||||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<div>
|
||||
<Listbox.Button
|
||||
className={`flex ${
|
||||
isNotAllowed ? "cursor-not-allowed" : "cursor-pointer"
|
||||
} items-center gap-x-2 rounded px-2 py-0.5 capitalize shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 ${
|
||||
issue.priority === "urgent"
|
||||
? "bg-red-100 text-red-600"
|
||||
: issue.priority === "high"
|
||||
? "bg-orange-100 text-orange-500"
|
||||
: issue.priority === "medium"
|
||||
? "bg-yellow-100 text-yellow-500"
|
||||
: issue.priority === "low"
|
||||
? "bg-green-100 text-green-500"
|
||||
: "bg-gray-100"
|
||||
}`}
|
||||
>
|
||||
{getPriorityIcon(
|
||||
issue.priority && issue.priority !== "" ? issue.priority ?? "" : "None",
|
||||
"text-sm"
|
||||
)}
|
||||
</Listbox.Button>
|
||||
|
||||
<Transition
|
||||
show={open}
|
||||
as={React.Fragment}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options className="absolute right-0 z-10 mt-1 max-h-48 w-36 overflow-auto rounded-md bg-white py-1 text-xs shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||
{PRIORITIES?.map((priority) => (
|
||||
<Listbox.Option
|
||||
key={priority}
|
||||
className={({ active }) =>
|
||||
`flex cursor-pointer select-none items-center gap-x-2 px-3 py-2 capitalize ${
|
||||
active ? "bg-indigo-50" : "bg-white"
|
||||
}`
|
||||
}
|
||||
value={priority}
|
||||
>
|
||||
{getPriorityIcon(priority, "text-sm")}
|
||||
{priority ?? "None"}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
<div className="absolute bottom-full right-0 z-10 mb-2 hidden whitespace-nowrap rounded-md bg-white p-2 shadow-md group-hover:block">
|
||||
<h5 className="mb-1 font-medium text-gray-900">Priority</h5>
|
||||
<div
|
||||
className={`capitalize ${
|
||||
issue.priority === "urgent"
|
||||
? "text-red-600"
|
||||
: issue.priority === "high"
|
||||
? "text-orange-500"
|
||||
: issue.priority === "medium"
|
||||
? "text-yellow-500"
|
||||
: issue.priority === "low"
|
||||
? "text-green-500"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{issue.priority ?? "None"}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Listbox>
|
||||
);
|
@ -1,55 +0,0 @@
|
||||
// ui
|
||||
import { CustomSelect } from "components/ui";
|
||||
// helpers
|
||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||
// types
|
||||
import { IIssue, IState } from "types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
states: IState[] | undefined;
|
||||
partialUpdateIssue: (formData: Partial<IIssue>) => void;
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const StateSelect: React.FC<Props> = ({
|
||||
issue,
|
||||
states,
|
||||
partialUpdateIssue,
|
||||
isNotAllowed,
|
||||
}) => (
|
||||
<CustomSelect
|
||||
label={
|
||||
<>
|
||||
<span
|
||||
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: states?.find((s) => s.id === issue.state)?.color,
|
||||
}}
|
||||
/>
|
||||
{addSpaceIfCamelCase(states?.find((s) => s.id === issue.state)?.name ?? "")}
|
||||
</>
|
||||
}
|
||||
value={issue.state}
|
||||
onChange={(data: string) => {
|
||||
partialUpdateIssue({ state: data });
|
||||
}}
|
||||
maxHeight="md"
|
||||
noChevron
|
||||
disabled={isNotAllowed}
|
||||
>
|
||||
{states?.map((state) => (
|
||||
<CustomSelect.Option key={state.id} value={state.id}>
|
||||
<>
|
||||
<span
|
||||
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: state.color,
|
||||
}}
|
||||
/>
|
||||
{addSpaceIfCamelCase(state.name)}
|
||||
</>
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
</CustomSelect>
|
||||
);
|
@ -7,7 +7,7 @@ import { Props } from "./types";
|
||||
import emojis from "./emojis.json";
|
||||
// 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";
|
||||
|
||||
|
@ -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 ? (
|
||||
|
@ -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";
|
||||
|
@ -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}
|
||||
|
@ -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 =
|
||||
|
@ -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;
|
||||
|
@ -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>;
|
||||
|
@ -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
|
||||
|
@ -1,41 +1,59 @@
|
||||
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,
|
||||
}) => (
|
||||
<Listbox
|
||||
as="div"
|
||||
value={issue.assignees}
|
||||
onChange={(data: any) => {
|
||||
const newData = issue.assignees ?? [];
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
if (newData.includes(data)) newData.splice(newData.indexOf(data), 1);
|
||||
else newData.push(data);
|
||||
const { data: members } = useSWR(
|
||||
projectId ? PROJECT_MEMBERS(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => projectService.projectMembers(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
);
|
||||
|
||||
partialUpdateIssue({ assignees_list: newData });
|
||||
}}
|
||||
className="group relative flex-shrink-0"
|
||||
disabled={isNotAllowed}
|
||||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
return (
|
||||
<Listbox
|
||||
as="div"
|
||||
value={issue.assignees}
|
||||
onChange={(data: any) => {
|
||||
const newData = issue.assignees ?? [];
|
||||
|
||||
if (newData.includes(data)) newData.splice(newData.indexOf(data), 1);
|
||||
else newData.push(data);
|
||||
|
||||
partialUpdateIssue({ assignees_list: newData });
|
||||
}}
|
||||
className="group relative flex-shrink-0"
|
||||
disabled={isNotAllowed}
|
||||
>
|
||||
{({ open }) => (
|
||||
<div>
|
||||
<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>
|
||||
{member.member.first_name && member.member.first_name !== ""
|
||||
? member.member.first_name
|
||||
: member.member.email}
|
||||
</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>
|
||||
);
|
||||
)}
|
||||
</Listbox>
|
||||
);
|
||||
};
|
36
apps/app/components/issues/view-select/due-date.tsx
Normal file
36
apps/app/components/issues/view-select/due-date.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
// ui
|
||||
import { CustomDatePicker } from "components/ui";
|
||||
// helpers
|
||||
import { findHowManyDaysLeft } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
partialUpdateIssue: (formData: Partial<IIssue>) => void;
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const ViewDueDateSelect: React.FC<Props> = ({ issue, partialUpdateIssue, isNotAllowed }) => (
|
||||
<div
|
||||
className={`group relative ${
|
||||
issue.target_date === null
|
||||
? ""
|
||||
: issue.target_date < new Date().toISOString()
|
||||
? "text-red-600"
|
||||
: findHowManyDaysLeft(issue.target_date) <= 3 && "text-orange-400"
|
||||
}`}
|
||||
>
|
||||
<CustomDatePicker
|
||||
placeholder="N/A"
|
||||
value={issue?.target_date}
|
||||
onChange={(val) =>
|
||||
partialUpdateIssue({
|
||||
target_date: val,
|
||||
})
|
||||
}
|
||||
className={issue?.target_date ? "w-[6.5rem]" : "w-[3rem] text-center"}
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
</div>
|
||||
);
|
88
apps/app/components/issues/view-select/priority.tsx
Normal file
88
apps/app/components/issues/view-select/priority.tsx
Normal file
@ -0,0 +1,88 @@
|
||||
import React from "react";
|
||||
|
||||
// ui
|
||||
import { Listbox, Transition } from "@headlessui/react";
|
||||
// icons
|
||||
import { getPriorityIcon } from "components/icons/priority-icon";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
// constants
|
||||
import { PRIORITIES } from "constants/project";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
partialUpdateIssue: (formData: Partial<IIssue>) => void;
|
||||
position?: "left" | "right";
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const ViewPrioritySelect: React.FC<Props> = ({
|
||||
issue,
|
||||
partialUpdateIssue,
|
||||
position = "right",
|
||||
isNotAllowed,
|
||||
}) => (
|
||||
<Listbox
|
||||
as="div"
|
||||
value={issue.priority}
|
||||
onChange={(data: string) => {
|
||||
partialUpdateIssue({ priority: data });
|
||||
}}
|
||||
className="group relative flex-shrink-0"
|
||||
disabled={isNotAllowed}
|
||||
>
|
||||
{({ open }) => (
|
||||
<div>
|
||||
<Listbox.Button
|
||||
className={`flex ${
|
||||
isNotAllowed ? "cursor-not-allowed" : "cursor-pointer"
|
||||
} items-center gap-x-2 rounded px-2 py-0.5 capitalize shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 ${
|
||||
issue.priority === "urgent"
|
||||
? "bg-red-100 text-red-600"
|
||||
: issue.priority === "high"
|
||||
? "bg-orange-100 text-orange-500"
|
||||
: issue.priority === "medium"
|
||||
? "bg-yellow-100 text-yellow-500"
|
||||
: issue.priority === "low"
|
||||
? "bg-green-100 text-green-500"
|
||||
: "bg-gray-100"
|
||||
}`}
|
||||
>
|
||||
{getPriorityIcon(
|
||||
issue.priority && issue.priority !== "" ? issue.priority ?? "" : "None",
|
||||
"text-sm"
|
||||
)}
|
||||
</Listbox.Button>
|
||||
|
||||
<Transition
|
||||
show={open}
|
||||
as={React.Fragment}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options
|
||||
className={`absolute z-10 mt-1 max-h-48 w-36 overflow-auto rounded-md bg-white py-1 text-xs shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none ${
|
||||
position === "left" ? "left-0" : "right-0"
|
||||
}`}
|
||||
>
|
||||
{PRIORITIES?.map((priority) => (
|
||||
<Listbox.Option
|
||||
key={priority}
|
||||
className={({ active }) =>
|
||||
`flex cursor-pointer select-none items-center gap-x-2 px-3 py-2 capitalize ${
|
||||
active ? "bg-indigo-50" : "bg-white"
|
||||
}`
|
||||
}
|
||||
value={priority}
|
||||
>
|
||||
{getPriorityIcon(priority, "text-sm")}
|
||||
{priority ?? "None"}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
)}
|
||||
</Listbox>
|
||||
);
|
75
apps/app/components/issues/view-select/state.tsx
Normal file
75
apps/app/components/issues/view-select/state.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// services
|
||||
import stateService from "services/state.service";
|
||||
// ui
|
||||
import { CustomSelect } from "components/ui";
|
||||
// helpers
|
||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||
// types
|
||||
import { IIssue, IState } from "types";
|
||||
// fetch-keys
|
||||
import { STATE_LIST } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
partialUpdateIssue: (formData: Partial<IIssue>) => void;
|
||||
position?: "left" | "right";
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const ViewStateSelect: React.FC<Props> = ({
|
||||
issue,
|
||||
partialUpdateIssue,
|
||||
position,
|
||||
isNotAllowed,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { data: states } = useSWR<IState[]>(
|
||||
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
|
||||
workspaceSlug
|
||||
? () => stateService.getStates(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
);
|
||||
|
||||
return (
|
||||
<CustomSelect
|
||||
label={
|
||||
<>
|
||||
<span
|
||||
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: states?.find((s) => s.id === issue.state)?.color,
|
||||
}}
|
||||
/>
|
||||
{addSpaceIfCamelCase(states?.find((s) => s.id === issue.state)?.name ?? "")}
|
||||
</>
|
||||
}
|
||||
value={issue.state}
|
||||
onChange={(data: string) => {
|
||||
partialUpdateIssue({ state: data });
|
||||
}}
|
||||
maxHeight="md"
|
||||
noChevron
|
||||
disabled={isNotAllowed}
|
||||
>
|
||||
{states?.map((state) => (
|
||||
<CustomSelect.Option key={state.id} value={state.id}>
|
||||
<>
|
||||
<span
|
||||
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: state.color,
|
||||
}}
|
||||
/>
|
||||
{addSpaceIfCamelCase(state.name)}
|
||||
</>
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
</CustomSelect>
|
||||
);
|
||||
};
|
@ -9,7 +9,7 @@ import { Squares2X2Icon } from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import type { IModule } from "types";
|
||||
// constants
|
||||
import { MODULE_STATUS } from "constants/";
|
||||
import { MODULE_STATUS } from "constants/module";
|
||||
|
||||
type Props = {
|
||||
control: Control<IModule, any>;
|
||||
|
@ -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>;
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -143,27 +143,34 @@ export const ProjectSidebarList: FC = () => {
|
||||
sidebarCollapse ? "" : "ml-[2.25rem]"
|
||||
} flex flex-col gap-y-1`}
|
||||
>
|
||||
{navigation(workspaceSlug as string, project?.id).map((item) => (
|
||||
<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 ${
|
||||
item.href === router.asPath
|
||||
? "bg-gray-200 text-gray-900"
|
||||
: "text-gray-500 hover:bg-gray-100 hover:text-gray-900 focus:bg-gray-100 focus:text-gray-900"
|
||||
} ${sidebarCollapse ? "justify-center" : ""}`}
|
||||
>
|
||||
<item.icon
|
||||
className={`h-4 w-4 flex-shrink-0 ${
|
||||
{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 ${
|
||||
item.href === router.asPath
|
||||
? "text-gray-900"
|
||||
: "text-gray-500 group-hover:text-gray-900"
|
||||
} ${!sidebarCollapse ? "mr-3" : ""}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{!sidebarCollapse && item.name}
|
||||
</a>
|
||||
</Link>
|
||||
))}
|
||||
? "bg-gray-200 text-gray-900"
|
||||
: "text-gray-500 hover:bg-gray-100 hover:text-gray-900 focus:bg-gray-100 focus:text-gray-900"
|
||||
} ${sidebarCollapse ? "justify-center" : ""}`}
|
||||
>
|
||||
<item.icon
|
||||
className={`h-4 w-4 flex-shrink-0 ${
|
||||
item.href === router.asPath
|
||||
? "text-gray-900"
|
||||
: "text-gray-500 group-hover:text-gray-900"
|
||||
} ${!sidebarCollapse ? "mr-3" : ""}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{!sidebarCollapse && item.name}
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
|
@ -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]
|
||||
);
|
||||
|
@ -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;
|
||||
|
@ -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 = {
|
||||
|
@ -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,
|
||||
|
@ -1,69 +0,0 @@
|
||||
import type { IIssue, NestedKeyOf } from "types";
|
||||
|
||||
export const PRIORITIES = ["urgent", "high", "medium", "low", null];
|
||||
|
||||
export const ROLE = {
|
||||
5: "Guest",
|
||||
10: "Viewer",
|
||||
15: "Member",
|
||||
20: "Admin",
|
||||
};
|
||||
|
||||
export const NETWORK_CHOICES = { "0": "Secret", "2": "Public" };
|
||||
|
||||
export const GROUP_CHOICES = {
|
||||
backlog: "Backlog",
|
||||
unstarted: "Unstarted",
|
||||
started: "Started",
|
||||
completed: "Completed",
|
||||
cancelled: "Cancelled",
|
||||
};
|
||||
|
||||
export const MODULE_STATUS = [
|
||||
{ label: "Backlog", value: "backlog", color: "#5e6ad2" },
|
||||
{ label: "Planned", value: "planned", color: "#26b5ce" },
|
||||
{ label: "In Progress", value: "in-progress", color: "#f2c94c" },
|
||||
{ label: "Paused", value: "paused", color: "#ff6900" },
|
||||
{ label: "Completed", value: "completed", color: "#4cb782" },
|
||||
{ label: "Cancelled", value: "cancelled", color: "#cc1d10" },
|
||||
];
|
||||
|
||||
export const groupByOptions: Array<{ name: string; key: NestedKeyOf<IIssue> | null }> = [
|
||||
{ name: "State", key: "state_detail.name" },
|
||||
{ name: "Priority", key: "priority" },
|
||||
{ name: "Created By", key: "created_by" },
|
||||
{ name: "None", key: null },
|
||||
];
|
||||
|
||||
export const orderByOptions: Array<{ name: string; key: NestedKeyOf<IIssue> | "manual" | null }> = [
|
||||
// { name: "Manual", key: "manual" },
|
||||
{ name: "Last created", key: "created_at" },
|
||||
{ name: "Last updated", key: "updated_at" },
|
||||
{ name: "Priority", key: "priority" },
|
||||
{ name: "None", key: null },
|
||||
];
|
||||
|
||||
export const filterIssueOptions: Array<{
|
||||
name: string;
|
||||
key: "activeIssue" | "backlogIssue" | null;
|
||||
}> = [
|
||||
{
|
||||
name: "All",
|
||||
key: null,
|
||||
},
|
||||
{
|
||||
name: "Active Issues",
|
||||
key: "activeIssue",
|
||||
},
|
||||
{
|
||||
name: "Backlog Issues",
|
||||
key: "backlogIssue",
|
||||
},
|
||||
];
|
||||
|
||||
export const companySize = [
|
||||
{ value: 5, label: "5" },
|
||||
{ value: 10, label: "10" },
|
||||
{ value: 25, label: "25" },
|
||||
{ value: 50, label: "50" },
|
||||
];
|
36
apps/app/constants/issue.ts
Normal file
36
apps/app/constants/issue.ts
Normal file
@ -0,0 +1,36 @@
|
||||
// types
|
||||
import { IIssue, NestedKeyOf } from "types";
|
||||
|
||||
export const GROUP_BY_OPTIONS: Array<{ name: string; key: NestedKeyOf<IIssue> | null }> = [
|
||||
{ name: "State", key: "state_detail.name" },
|
||||
{ name: "Priority", key: "priority" },
|
||||
{ name: "Created By", key: "created_by" },
|
||||
{ name: "None", key: null },
|
||||
];
|
||||
|
||||
export const ORDER_BY_OPTIONS: Array<{ name: string; key: NestedKeyOf<IIssue> | "manual" | null }> =
|
||||
[
|
||||
// { name: "Manual", key: "manual" },
|
||||
{ name: "Last created", key: "created_at" },
|
||||
{ name: "Last updated", key: "updated_at" },
|
||||
{ name: "Priority", key: "priority" },
|
||||
// { name: "None", key: null },
|
||||
];
|
||||
|
||||
export const FILTER_ISSUE_OPTIONS: Array<{
|
||||
name: string;
|
||||
key: "activeIssue" | "backlogIssue" | null;
|
||||
}> = [
|
||||
{
|
||||
name: "All",
|
||||
key: null,
|
||||
},
|
||||
{
|
||||
name: "Active Issues",
|
||||
key: "activeIssue",
|
||||
},
|
||||
{
|
||||
name: "Backlog Issues",
|
||||
key: "backlogIssue",
|
||||
},
|
||||
];
|
8
apps/app/constants/module.ts
Normal file
8
apps/app/constants/module.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export const MODULE_STATUS = [
|
||||
{ label: "Backlog", value: "backlog", color: "#5e6ad2" },
|
||||
{ label: "Planned", value: "planned", color: "#26b5ce" },
|
||||
{ label: "In Progress", value: "in-progress", color: "#f2c94c" },
|
||||
{ label: "Paused", value: "paused", color: "#ff6900" },
|
||||
{ label: "Completed", value: "completed", color: "#4cb782" },
|
||||
{ label: "Cancelled", value: "cancelled", color: "#cc1d10" },
|
||||
];
|
11
apps/app/constants/project.ts
Normal file
11
apps/app/constants/project.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export const NETWORK_CHOICES = { "0": "Secret", "2": "Public" };
|
||||
|
||||
export const GROUP_CHOICES = {
|
||||
backlog: "Backlog",
|
||||
unstarted: "Unstarted",
|
||||
started: "Started",
|
||||
completed: "Completed",
|
||||
cancelled: "Cancelled",
|
||||
};
|
||||
|
||||
export const PRIORITIES = ["urgent", "high", "medium", "low", null];
|
13
apps/app/constants/workspace.ts
Normal file
13
apps/app/constants/workspace.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export const ROLE = {
|
||||
5: "Guest",
|
||||
10: "Viewer",
|
||||
15: "Member",
|
||||
20: "Admin",
|
||||
};
|
||||
|
||||
export const COMPANY_SIZE = [
|
||||
{ value: 5, label: "5" },
|
||||
{ value: 10, label: "10" },
|
||||
{ value: 25, label: "25" },
|
||||
{ value: 50, label: "50" },
|
||||
];
|
@ -51,6 +51,7 @@ const useIssuesProperties = (workspaceSlug?: string, projectId?: string) => {
|
||||
const updateIssueProperties = useCallback(
|
||||
(key: keyof Properties) => {
|
||||
if (!workspaceSlug || !user) return;
|
||||
|
||||
setProperties((prev) => ({ ...prev, [key]: !prev[key] }));
|
||||
if (issueProperties && projectId) {
|
||||
mutateIssueProperties(
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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,32 +149,71 @@ const AppLayout: FC<AppLayoutProps> = ({
|
||||
<CommandPalette />
|
||||
<div className="flex h-screen w-full overflow-x-hidden">
|
||||
<AppSidebar toggleSidebar={toggleSidebar} setToggleSidebar={setToggleSidebar} />
|
||||
<main className="flex h-screen w-full min-w-0 flex-col overflow-y-auto">
|
||||
{!noHeader && (
|
||||
<AppHeader
|
||||
breadcrumbs={breadcrumbs}
|
||||
left={left}
|
||||
right={right}
|
||||
setToggleSidebar={setToggleSidebar}
|
||||
/>
|
||||
)}
|
||||
|
||||
{projectId && !projectMembers ? (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
) : isMember ? (
|
||||
<div
|
||||
className={`w-full flex-grow ${noPadding ? "" : "p-5"} ${
|
||||
bg === "primary" ? "bg-primary" : bg === "secondary" ? "bg-secondary" : "bg-primary"
|
||||
}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
) : (
|
||||
<JoinProject isJoiningProject={isJoiningProject} handleJoin={handleJoin} />
|
||||
)}
|
||||
</main>
|
||||
{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
|
||||
breadcrumbs={breadcrumbs}
|
||||
left={left}
|
||||
right={right}
|
||||
setToggleSidebar={setToggleSidebar}
|
||||
/>
|
||||
)}
|
||||
{projectId && !projectMembers ? (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
) : isMember ? (
|
||||
<div
|
||||
className={`w-full flex-grow ${
|
||||
noPadding ? "" : settingsLayout ? "p-5 pb-5 lg:px-16 lg:pt-10" : "p-5"
|
||||
} ${
|
||||
bg === "primary"
|
||||
? "bg-primary"
|
||||
: bg === "secondary"
|
||||
? "bg-secondary"
|
||||
: "bg-primary"
|
||||
}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
) : (
|
||||
<JoinProject isJoiningProject={isJoiningProject} handleJoin={handleJoin} />
|
||||
)}
|
||||
</main>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
|
@ -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";
|
||||
|
@ -1,14 +0,0 @@
|
||||
// layouts
|
||||
import Container from "layouts/container";
|
||||
// types
|
||||
import type { Props } from "./types";
|
||||
|
||||
const DefaultLayout: React.FC<Props> = ({ meta, children }) => (
|
||||
<Container meta={meta}>
|
||||
<div className="w-full h-screen overflow-auto bg-gray-50">
|
||||
<>{children}</>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
|
||||
export default DefaultLayout;
|
30
apps/app/layouts/default-layout/index.tsx
Normal file
30
apps/app/layouts/default-layout/index.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
// layouts
|
||||
import Container from "layouts/container";
|
||||
|
||||
type Meta = {
|
||||
title?: string | null;
|
||||
description?: string | null;
|
||||
image?: string | null;
|
||||
url?: string | null;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
meta?: Meta;
|
||||
children: React.ReactNode;
|
||||
noPadding?: boolean;
|
||||
bg?: "primary" | "secondary";
|
||||
noHeader?: boolean;
|
||||
breadcrumbs?: JSX.Element;
|
||||
left?: JSX.Element;
|
||||
right?: JSX.Element;
|
||||
};
|
||||
|
||||
const DefaultLayout: React.FC<Props> = ({ meta, children }) => (
|
||||
<Container meta={meta}>
|
||||
<div className="w-full h-screen overflow-auto bg-gray-50">
|
||||
<>{children}</>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
|
||||
export default DefaultLayout;
|
@ -1,37 +0,0 @@
|
||||
// ui
|
||||
import { Button } from "components/ui";
|
||||
// icons
|
||||
import { Bars3Icon } from "@heroicons/react/24/outline";
|
||||
|
||||
type Props = {
|
||||
breadcrumbs?: JSX.Element;
|
||||
left?: JSX.Element;
|
||||
right?: JSX.Element;
|
||||
setToggleSidebar: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
const Header: React.FC<Props> = ({ breadcrumbs, left, right, setToggleSidebar }) => {
|
||||
return (
|
||||
<>
|
||||
<div className="flex w-full flex-col gap-y-4 border-b border-gray-200 bg-gray-50 px-5 py-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="block md:hidden">
|
||||
<Button
|
||||
type="button"
|
||||
theme="secondary"
|
||||
className="h-8 w-8"
|
||||
onClick={() => setToggleSidebar((prevData) => !prevData)}
|
||||
>
|
||||
<Bars3Icon className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
{breadcrumbs}
|
||||
{left}
|
||||
</div>
|
||||
{right}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
@ -1,188 +0,0 @@
|
||||
import { useRef, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { Transition } from "@headlessui/react";
|
||||
// hooks
|
||||
import useTheme from "hooks/use-theme";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// components
|
||||
import ProjectsList from "components/sidebar/projects-list";
|
||||
import WorkspaceOptions from "components/sidebar/workspace-options";
|
||||
// icons
|
||||
import {
|
||||
Cog6ToothIcon,
|
||||
RectangleStackIcon,
|
||||
ArrowLongLeftIcon,
|
||||
RectangleGroupIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import {
|
||||
CyclesIcon,
|
||||
QuestionMarkCircleIcon,
|
||||
BoltIcon,
|
||||
DocumentIcon,
|
||||
DiscordIcon,
|
||||
GithubIcon,
|
||||
CommentIcon,
|
||||
} from "components/icons";
|
||||
|
||||
type Props = {
|
||||
toggleSidebar: boolean;
|
||||
setToggleSidebar: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
const helpOptions = [
|
||||
{
|
||||
name: "Documentation",
|
||||
href: "https://docs.plane.so/",
|
||||
Icon: DocumentIcon,
|
||||
},
|
||||
{
|
||||
name: "Join our Discord",
|
||||
href: "https://discord.com/invite/A92xrEGCge",
|
||||
Icon: DiscordIcon,
|
||||
},
|
||||
{
|
||||
name: "Report a bug",
|
||||
href: "https://github.com/makeplane/plane/issues/new/choose",
|
||||
Icon: GithubIcon,
|
||||
},
|
||||
{
|
||||
name: "Chat with us",
|
||||
href: "mailto:hello@plane.so",
|
||||
Icon: CommentIcon,
|
||||
},
|
||||
];
|
||||
|
||||
const navigation = (workspaceSlug: string, projectId: string) => [
|
||||
{
|
||||
name: "Issues",
|
||||
href: `/${workspaceSlug}/projects/${projectId}/issues`,
|
||||
icon: RectangleStackIcon,
|
||||
},
|
||||
{
|
||||
name: "Cycles",
|
||||
href: `/${workspaceSlug}/projects/${projectId}/cycles`,
|
||||
icon: CyclesIcon,
|
||||
},
|
||||
{
|
||||
name: "Modules",
|
||||
href: `/${workspaceSlug}/projects/${projectId}/modules`,
|
||||
icon: RectangleGroupIcon,
|
||||
},
|
||||
{
|
||||
name: "Settings",
|
||||
href: `/${workspaceSlug}/projects/${projectId}/settings`,
|
||||
icon: Cog6ToothIcon,
|
||||
},
|
||||
];
|
||||
|
||||
const Sidebar: React.FC<Props> = ({ toggleSidebar, setToggleSidebar }) => {
|
||||
const helpOptionsRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const { collapsed: sidebarCollapse, toggleCollapsed } = useTheme();
|
||||
|
||||
useOutsideClickDetector(helpOptionsRef, () => setIsNeedHelpOpen(false));
|
||||
|
||||
const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false);
|
||||
|
||||
const helpOptionMode = sidebarCollapse ? "left-full" : "left-[-75px]";
|
||||
|
||||
return (
|
||||
<nav className="relative z-20 h-screen">
|
||||
<div
|
||||
className={`${sidebarCollapse ? "" : "w-auto md:w-60"} fixed inset-y-0 top-0 ${
|
||||
toggleSidebar ? "left-0" : "-left-60 md:left-0"
|
||||
} flex h-full flex-col bg-white duration-300 md:relative`}
|
||||
>
|
||||
<div className="flex h-full flex-1 flex-col border-r border-gray-200">
|
||||
<div className="flex h-full flex-1 flex-col pt-2">
|
||||
<WorkspaceOptions sidebarCollapse={sidebarCollapse} />
|
||||
<ProjectsList navigation={navigation} sidebarCollapse={sidebarCollapse} />
|
||||
<div
|
||||
className={`flex w-full items-center justify-between self-baseline bg-primary px-2 py-2 ${
|
||||
sidebarCollapse ? "flex-col-reverse" : ""
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={`hidden items-center gap-3 rounded-md px-2 py-2 text-xs font-medium text-gray-500 outline-none hover:bg-gray-100 hover:text-gray-900 md:flex ${
|
||||
sidebarCollapse ? "w-full justify-center" : ""
|
||||
}`}
|
||||
onClick={() => toggleCollapsed()}
|
||||
>
|
||||
<ArrowLongLeftIcon
|
||||
className={`h-4 w-4 flex-shrink-0 text-gray-500 duration-300 group-hover:text-gray-900 ${
|
||||
sidebarCollapse ? "rotate-180" : ""
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-3 rounded-md px-2 py-2 text-xs font-medium text-gray-500 outline-none hover:bg-gray-100 hover:text-gray-900 md:hidden"
|
||||
onClick={() => setToggleSidebar(false)}
|
||||
>
|
||||
<ArrowLongLeftIcon className="h-4 w-4 flex-shrink-0 text-gray-500 group-hover:text-gray-900" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`flex items-center gap-x-1 rounded-md px-2 py-2 text-xs font-medium text-gray-500 outline-none hover:bg-gray-100 hover:text-gray-900 ${
|
||||
sidebarCollapse ? "w-full justify-center" : ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
const e = new KeyboardEvent("keydown", {
|
||||
key: "h",
|
||||
});
|
||||
document.dispatchEvent(e);
|
||||
}}
|
||||
title="Shortcuts"
|
||||
>
|
||||
<BoltIcon className="h-4 w-4 text-gray-500" />
|
||||
{!sidebarCollapse && <span>Shortcuts</span>}
|
||||
</button>
|
||||
<div className="relative">
|
||||
<Transition
|
||||
show={isNeedHelpOpen}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<div
|
||||
className={`absolute bottom-2 ${helpOptionMode} space-y-2 rounded-sm bg-white py-3 shadow-md`}
|
||||
ref={helpOptionsRef}
|
||||
>
|
||||
{helpOptions.map(({ name, Icon, href }) => (
|
||||
<Link href={href} key={name}>
|
||||
<a
|
||||
target="_blank"
|
||||
className="mx-3 flex items-center gap-x-2 rounded-md whitespace-nowrap px-2 py-2 text-xs hover:bg-gray-100"
|
||||
>
|
||||
<Icon className="h-5 w-5 text-gray-500" />
|
||||
<span className="text-sm">{name}</span>
|
||||
</a>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</Transition>
|
||||
<button
|
||||
type="button"
|
||||
className={`flex items-center gap-x-1 rounded-md px-2 py-2 text-xs font-medium text-gray-500 outline-none hover:bg-gray-100 hover:text-gray-900 ${
|
||||
sidebarCollapse ? "w-full justify-center" : ""
|
||||
}`}
|
||||
onClick={() => setIsNeedHelpOpen((prev) => !prev)}
|
||||
title="Help"
|
||||
>
|
||||
<QuestionMarkCircleIcon className="h-4 w-4 text-gray-500" />
|
||||
{!sidebarCollapse && <span>Help?</span>}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
@ -1,177 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// layouts
|
||||
import Container from "layouts/container";
|
||||
import Header from "layouts/navbar/header";
|
||||
import Sidebar from "layouts/navbar/main-sidebar";
|
||||
import SettingsSidebar from "layouts/navbar/settings-sidebar";
|
||||
// components
|
||||
import { NotAuthorizedView } from "components/core";
|
||||
import CommandPalette from "components/command-palette";
|
||||
// ui
|
||||
import { Button } from "components/ui";
|
||||
// types
|
||||
import { Meta } from "./types";
|
||||
import AppSidebar from "./app-layout/app-sidebar";
|
||||
|
||||
type Props = {
|
||||
meta?: Meta;
|
||||
children: React.ReactNode;
|
||||
noPadding?: boolean;
|
||||
bg?: "primary" | "secondary";
|
||||
noHeader?: boolean;
|
||||
breadcrumbs?: JSX.Element;
|
||||
left?: JSX.Element;
|
||||
right?: JSX.Element;
|
||||
type: "workspace" | "project";
|
||||
memberType?: {
|
||||
isMember: boolean;
|
||||
isOwner: boolean;
|
||||
isViewer: boolean;
|
||||
isGuest: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
const workspaceLinks: (wSlug: string) => Array<{
|
||||
label: string;
|
||||
href: string;
|
||||
}> = (workspaceSlug) => [
|
||||
{
|
||||
label: "General",
|
||||
href: `/${workspaceSlug}/settings`,
|
||||
},
|
||||
{
|
||||
label: "Members",
|
||||
href: `/${workspaceSlug}/settings/members`,
|
||||
},
|
||||
{
|
||||
label: "Features",
|
||||
href: `/${workspaceSlug}/settings/features`,
|
||||
},
|
||||
{
|
||||
label: "Billing & Plans",
|
||||
href: `/${workspaceSlug}/settings/billing`,
|
||||
},
|
||||
];
|
||||
|
||||
const sidebarLinks: (
|
||||
wSlug?: string,
|
||||
pId?: string
|
||||
) => Array<{
|
||||
label: string;
|
||||
href: string;
|
||||
}> = (workspaceSlug, projectId) => [
|
||||
{
|
||||
label: "General",
|
||||
href: `/${workspaceSlug}/projects/${projectId}/settings`,
|
||||
},
|
||||
{
|
||||
label: "Control",
|
||||
href: `/${workspaceSlug}/projects/${projectId}/settings/control`,
|
||||
},
|
||||
{
|
||||
label: "Members",
|
||||
href: `/${workspaceSlug}/projects/${projectId}/settings/members`,
|
||||
},
|
||||
{
|
||||
label: "States",
|
||||
href: `/${workspaceSlug}/projects/${projectId}/settings/states`,
|
||||
},
|
||||
{
|
||||
label: "Labels",
|
||||
href: `/${workspaceSlug}/projects/${projectId}/settings/labels`,
|
||||
},
|
||||
];
|
||||
|
||||
const SettingsLayout: React.FC<Props> = ({
|
||||
meta,
|
||||
children,
|
||||
noPadding,
|
||||
bg,
|
||||
noHeader,
|
||||
breadcrumbs,
|
||||
left,
|
||||
right,
|
||||
type,
|
||||
memberType,
|
||||
}) => {
|
||||
const [toggleSidebar, setToggleSidebar] = useState(false);
|
||||
|
||||
const { isMember, isOwner, isViewer, isGuest } = memberType ?? {
|
||||
isMember: false,
|
||||
isOwner: false,
|
||||
isViewer: false,
|
||||
isGuest: false,
|
||||
};
|
||||
|
||||
const {
|
||||
query: { workspaceSlug, projectId },
|
||||
} = useRouter();
|
||||
|
||||
return (
|
||||
<Container meta={meta}>
|
||||
<div className="flex h-screen w-full overflow-x-hidden">
|
||||
<AppSidebar toggleSidebar={toggleSidebar} setToggleSidebar={setToggleSidebar} />
|
||||
<CommandPalette />
|
||||
{isMember || isOwner ? (
|
||||
<>
|
||||
<SettingsSidebar
|
||||
links={
|
||||
type === "workspace"
|
||||
? workspaceLinks(workspaceSlug as string)
|
||||
: sidebarLinks(workspaceSlug as string, projectId as string)
|
||||
}
|
||||
/>
|
||||
<main className="flex h-screen w-full min-w-0 flex-col overflow-y-auto">
|
||||
{noHeader ? null : (
|
||||
<Header
|
||||
breadcrumbs={breadcrumbs}
|
||||
left={left}
|
||||
right={right}
|
||||
setToggleSidebar={setToggleSidebar}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={`w-full flex-grow ${noPadding ? "" : "p-5 pb-5 lg:px-16 lg:pt-10"} ${
|
||||
bg === "primary"
|
||||
? "bg-primary"
|
||||
: bg === "secondary"
|
||||
? "bg-secondary"
|
||||
: "bg-primary"
|
||||
}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
) : (
|
||||
<NotAuthorizedView
|
||||
actionButton={
|
||||
(isViewer || isGuest) && projectId ? (
|
||||
<Link href={`/${workspaceSlug}/projects/${projectId}/issues`}>
|
||||
<Button size="sm" theme="secondary">
|
||||
Go to Issues
|
||||
</Button>
|
||||
</Link>
|
||||
) : (
|
||||
(isViewer || isGuest) &&
|
||||
workspaceSlug && (
|
||||
<Link href={`/${workspaceSlug}`}>
|
||||
<Button size="sm" theme="secondary">
|
||||
Go to workspace
|
||||
</Button>
|
||||
</Link>
|
||||
)
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsLayout;
|
@ -1,5 +1,4 @@
|
||||
// next
|
||||
import { ArrowLeftIcon } from "@heroicons/react/24/outline";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
17
apps/app/layouts/types.d.ts
vendored
17
apps/app/layouts/types.d.ts
vendored
@ -1,17 +0,0 @@
|
||||
export type Meta = {
|
||||
title?: string | null;
|
||||
description?: string | null;
|
||||
image?: string | null;
|
||||
url?: string | null;
|
||||
};
|
||||
|
||||
export type Props = {
|
||||
meta?: Meta;
|
||||
children: React.ReactNode;
|
||||
noPadding?: boolean;
|
||||
bg?: "primary" | "secondary";
|
||||
noHeader?: boolean;
|
||||
breadcrumbs?: JSX.Element;
|
||||
left?: JSX.Element;
|
||||
right?: JSX.Element;
|
||||
};
|
@ -1,34 +1,34 @@
|
||||
import React from "react";
|
||||
|
||||
import 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
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -0,0 +1,187 @@
|
||||
import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR, { mutate } from "swr";
|
||||
|
||||
// services
|
||||
import projectService from "services/project.service";
|
||||
// lib
|
||||
import { requiredAdmin } from "lib/auth";
|
||||
// layouts
|
||||
import AppLayout from "layouts/app-layout";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// ui
|
||||
import { Button } from "components/ui";
|
||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||
// types
|
||||
import { IProject, UserAuth } from "types";
|
||||
import type { NextPage, NextPageContext } from "next";
|
||||
// fetch-keys
|
||||
import { PROJECTS_LIST, PROJECT_DETAILS } from "constants/fetch-keys";
|
||||
|
||||
const FeaturesSettings: NextPage<UserAuth> = (props) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { data: projectDetails } = useSWR(
|
||||
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => projectService.getProject(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
);
|
||||
|
||||
const handleSubmit = async (formData: Partial<IProject>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
mutate<IProject>(
|
||||
PROJECT_DETAILS(projectId as string),
|
||||
(prevData) => ({ ...(prevData as IProject), ...formData }),
|
||||
false
|
||||
);
|
||||
|
||||
mutate<IProject[]>(
|
||||
PROJECTS_LIST(workspaceSlug as string),
|
||||
(prevData) =>
|
||||
prevData?.map((p) => {
|
||||
if (p.id === projectId)
|
||||
return {
|
||||
...p,
|
||||
...formData,
|
||||
};
|
||||
|
||||
return p;
|
||||
}),
|
||||
false
|
||||
);
|
||||
|
||||
await projectService
|
||||
.updateProject(workspaceSlug as string, projectId as string, formData)
|
||||
.then((res) => {
|
||||
mutate(PROJECT_DETAILS(projectId as string));
|
||||
mutate(PROJECTS_LIST(workspaceSlug as string));
|
||||
|
||||
setToastAlert({
|
||||
title: "Success!",
|
||||
type: "success",
|
||||
message: "Project features updated successfully.",
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<AppLayout
|
||||
settingsLayout="project"
|
||||
memberType={props}
|
||||
breadcrumbs={
|
||||
<Breadcrumbs>
|
||||
<BreadcrumbItem
|
||||
title={`${projectDetails?.name ?? "Project"}`}
|
||||
link={`/${workspaceSlug}/projects/${projectDetails?.id}/issues`}
|
||||
/>
|
||||
<BreadcrumbItem title="Features Settings" />
|
||||
</Breadcrumbs>
|
||||
}
|
||||
>
|
||||
<section className="space-y-8">
|
||||
<div>
|
||||
<h3 className="text-3xl font-bold leading-6 text-gray-900">Project Features</h3>
|
||||
</div>
|
||||
<div className="space-y-8 md:w-2/3">
|
||||
<div className="flex items-center justify-between gap-x-10 gap-y-2">
|
||||
<div>
|
||||
<h4 className="text-md mb-1 leading-6 text-gray-900">Use cycles</h4>
|
||||
<p className="mb-3 text-sm text-gray-500">
|
||||
Cycles are enabled for all the projects in this workspace. Access it from the
|
||||
navigation bar.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
|
||||
projectDetails?.cycle_view ? "bg-indigo-500" : "bg-gray-200"
|
||||
}`}
|
||||
role="switch"
|
||||
aria-checked={projectDetails?.cycle_view}
|
||||
onClick={() => handleSubmit({ cycle_view: !projectDetails?.cycle_view })}
|
||||
>
|
||||
<span className="sr-only">Use cycles</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
|
||||
projectDetails?.cycle_view ? "translate-x-5" : "translate-x-0"
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-x-10 gap-y-2">
|
||||
<div>
|
||||
<h4 className="text-md mb-1 leading-6 text-gray-900">Use modules</h4>
|
||||
<p className="mb-3 text-sm text-gray-500">
|
||||
Modules are enabled for all the projects in this workspace. Access it from the
|
||||
navigation bar.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
|
||||
projectDetails?.module_view ? "bg-indigo-500" : "bg-gray-200"
|
||||
}`}
|
||||
role="switch"
|
||||
aria-checked={projectDetails?.module_view}
|
||||
onClick={() => handleSubmit({ module_view: !projectDetails?.module_view })}
|
||||
>
|
||||
<span className="sr-only">Use cycles</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
|
||||
projectDetails?.module_view ? "translate-x-5" : "translate-x-0"
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<a href="https://plane.so/" target="_blank" rel="noreferrer">
|
||||
<Button theme="secondary" size="rg" className="text-xs">
|
||||
Plane is open-source, view Roadmap
|
||||
</Button>
|
||||
</a>
|
||||
<a href="https://github.com/makeplane/plane" target="_blank" rel="noreferrer">
|
||||
<Button theme="secondary" size="rg" className="text-xs">
|
||||
Star us on GitHub
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</AppLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export const getServerSideProps = async (ctx: NextPageContext) => {
|
||||
const projectId = ctx.query.projectId as string;
|
||||
const workspaceSlug = ctx.query.workspaceSlug as string;
|
||||
|
||||
const memberDetail = await requiredAdmin(workspaceSlug, projectId, ctx.req?.headers.cookie);
|
||||
|
||||
return {
|
||||
props: {
|
||||
isOwner: memberDetail?.role === 20,
|
||||
isMember: memberDetail?.role === 15,
|
||||
isViewer: memberDetail?.role === 10,
|
||||
isGuest: memberDetail?.role === 5,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default FeaturesSettings;
|
@ -6,11 +6,11 @@ import useSWR, { mutate } from "swr";
|
||||
|
||||
// react-hook-form
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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,13 +178,13 @@ 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",
|
||||
}}
|
||||
/>
|
||||
/>
|
||||
)}
|
||||
</Popover.Button>
|
||||
|
||||
@ -191,7 +199,7 @@ const LabelsSettings: NextPage<TLabelSettingsProps> = (props) => {
|
||||
>
|
||||
<Popover.Panel className="absolute top-full left-0 z-20 mt-3 w-screen max-w-xs px-2 sm:px-0">
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,173 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
import useSWR from "swr";
|
||||
|
||||
// lib
|
||||
import type { GetServerSideProps, NextPage } from "next";
|
||||
import { requiredWorkspaceAdmin } from "lib/auth";
|
||||
// constants
|
||||
// services
|
||||
import workspaceService from "services/workspace.service";
|
||||
// layouts
|
||||
import SettingsLayout from "layouts/settings-layout";
|
||||
// ui
|
||||
import { Button } from "components/ui";
|
||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||
import { WORKSPACE_DETAILS } from "constants/fetch-keys";
|
||||
|
||||
type TFeatureSettingsProps = {
|
||||
isOwner: boolean;
|
||||
isMember: boolean;
|
||||
isViewer: boolean;
|
||||
isGuest: boolean;
|
||||
};
|
||||
|
||||
const FeaturesSettings: NextPage<TFeatureSettingsProps> = (props) => {
|
||||
const {
|
||||
query: { workspaceSlug },
|
||||
} = useRouter();
|
||||
|
||||
const { data: activeWorkspace } = useSWR(
|
||||
workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug as string) : null,
|
||||
() => (workspaceSlug ? workspaceService.getWorkspace(workspaceSlug as string) : null)
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsLayout
|
||||
memberType={{
|
||||
...props,
|
||||
}}
|
||||
type="workspace"
|
||||
breadcrumbs={
|
||||
<Breadcrumbs>
|
||||
<BreadcrumbItem
|
||||
title={`${activeWorkspace?.name ?? "Workspace"}`}
|
||||
link={`/${workspaceSlug}`}
|
||||
/>
|
||||
<BreadcrumbItem title="Members Settings" />
|
||||
</Breadcrumbs>
|
||||
}
|
||||
>
|
||||
<section className="space-y-8">
|
||||
<div>
|
||||
<h3 className="text-3xl font-bold leading-6 text-gray-900">Workspace Features</h3>
|
||||
</div>
|
||||
<div className="space-y-8 md:w-2/3">
|
||||
<div className="flex items-center gap-x-10 gap-y-2">
|
||||
<div>
|
||||
<h4 className="text-md mb-1 leading-6 text-gray-900">Use modules</h4>
|
||||
<p className="mb-3 text-sm text-gray-500">
|
||||
Modules are enabled for all the projects in this workspace. Access it from the
|
||||
navigation bar.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
{/* Disabled- bg-gray-200, translate-x-0 */}
|
||||
<button
|
||||
type="button"
|
||||
className="pointer-events-none relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent bg-indigo-500 transition-colors duration-200 ease-in-out focus:outline-none"
|
||||
role="switch"
|
||||
aria-checked="false"
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none inline-block h-5 w-5 translate-x-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-x-10 gap-y-2">
|
||||
<div>
|
||||
<h4 className="text-md mb-1 leading-6 text-gray-900">Use cycles</h4>
|
||||
<p className="mb-3 text-sm text-gray-500">
|
||||
Cycles are enabled for all the projects in this workspace. Access it from the
|
||||
navigation bar.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
{/* Disabled- bg-gray-200, translate-x-0 */}
|
||||
<button
|
||||
type="button"
|
||||
className="pointer-events-none relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent bg-indigo-500 transition-colors duration-200 ease-in-out focus:outline-none"
|
||||
role="switch"
|
||||
aria-checked="false"
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none inline-block h-5 w-5 translate-x-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-x-10 gap-y-2">
|
||||
<div>
|
||||
<h4 className="text-md mb-1 leading-6 text-gray-900">Use backlogs</h4>
|
||||
<p className="mb-3 text-sm text-gray-500">
|
||||
Backlog are enabled for all the projects in this workspace. Access it from the
|
||||
navigation bar.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
{/* Disabled- bg-gray-200, translate-x-0 */}
|
||||
<button
|
||||
type="button"
|
||||
className="pointer-events-none relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent bg-indigo-500 transition-colors duration-200 ease-in-out focus:outline-none"
|
||||
role="switch"
|
||||
aria-checked="false"
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none inline-block h-5 w-5 translate-x-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<a href="https://plane.so/" target="_blank" rel="noreferrer">
|
||||
<Button theme="secondary" size="rg" className="text-xs">
|
||||
Plane is open-source, view Roadmap
|
||||
</Button>
|
||||
</a>
|
||||
<a href="https://github.com/makeplane/plane" target="_blank" rel="noreferrer">
|
||||
<Button theme="secondary" size="rg" className="text-xs">
|
||||
Star us on GitHub
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</SettingsLayout>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
||||
const workspaceSlug = ctx.params?.workspaceSlug as string;
|
||||
|
||||
const memberDetail = await requiredWorkspaceAdmin(workspaceSlug, ctx.req.headers.cookie);
|
||||
|
||||
if (memberDetail === null) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
isOwner: memberDetail?.role === 20,
|
||||
isMember: memberDetail?.role === 15,
|
||||
isViewer: memberDetail?.role === 10,
|
||||
isGuest: memberDetail?.role === 5,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default FeaturesSettings;
|
@ -17,7 +17,7 @@ import { requiredWorkspaceAdmin } from "lib/auth";
|
||||
import workspaceService from "services/workspace.service";
|
||||
import 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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
|
@ -1,21 +1,21 @@
|
||||
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
|
||||
meta={{
|
||||
title: "Plane - An error occurred",
|
||||
description: "We were unable to get this page for you.",
|
||||
}}
|
||||
>
|
||||
<div className="h-full w-full">
|
||||
<h2 className="text-3xl">Error!</h2>
|
||||
</div>
|
||||
</DefaultLayout>
|
||||
);
|
||||
<DefaultLayout
|
||||
meta={{
|
||||
title: "Plane - An error occurred",
|
||||
description: "We were unable to get this page for you.",
|
||||
}}
|
||||
>
|
||||
<div className="h-full w-full">
|
||||
<h2 className="text-3xl">Error!</h2>
|
||||
</div>
|
||||
</DefaultLayout>
|
||||
);
|
||||
|
||||
export default ErrorPage;
|
||||
|
@ -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 = () => {
|
||||
|
@ -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);
|
||||
|
@ -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";
|
||||
|
||||
|
2
apps/app/types/issues.d.ts
vendored
2
apps/app/types/issues.d.ts
vendored
@ -171,7 +171,7 @@ export interface IIssueLabels {
|
||||
updated_at: Date;
|
||||
name: string;
|
||||
description: string;
|
||||
colour: string;
|
||||
color: string;
|
||||
created_by: string;
|
||||
updated_by: string;
|
||||
project: string;
|
||||
|
24
apps/app/types/projects.d.ts
vendored
24
apps/app/types/projects.d.ts
vendored
@ -1,20 +1,22 @@
|
||||
import type { IUserLite, IWorkspace } from "./";
|
||||
|
||||
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 = {
|
||||
|
Loading…
Reference in New Issue
Block a user