forked from github/plane
Merge pull request #33 from makeplane/stage-release
dev: promote staging to master
This commit is contained in:
commit
43a256f15d
4
apps/app/.env.example
Normal file
4
apps/app/.env.example
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
NEXT_PUBLIC_API_BASE_URL = "<-- endpoint goes here -->"
|
||||||
|
NEXT_PUBLIC_GOOGLE_CLIENTID = "<-- google client id goes here -->"
|
||||||
|
NEXT_PUBLIC_GITHUB_ID = "<-- github id goes here -->"
|
||||||
|
NEXT_PUBLIC_APP_ENVIRONMENT=development
|
@ -7,7 +7,7 @@ import { useForm } from "react-hook-form";
|
|||||||
// headless ui
|
// headless ui
|
||||||
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
||||||
// services
|
// services
|
||||||
import issuesServices from "lib/services/issues.services";
|
import issuesServices from "lib/services/issues.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useUser from "lib/hooks/useUser";
|
import useUser from "lib/hooks/useUser";
|
||||||
// icons
|
// icons
|
||||||
|
@ -8,7 +8,7 @@ import { SubmitHandler, useForm } from "react-hook-form";
|
|||||||
// headless ui
|
// headless ui
|
||||||
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
||||||
// services
|
// services
|
||||||
import issuesServices from "lib/services/issues.services";
|
import issuesServices from "lib/services/issues.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useUser from "lib/hooks/useUser";
|
import useUser from "lib/hooks/useUser";
|
||||||
import useTheme from "lib/hooks/useTheme";
|
import useTheme from "lib/hooks/useTheme";
|
||||||
@ -22,7 +22,7 @@ import {
|
|||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
// components
|
// components
|
||||||
import ShortcutsModal from "components/command-palette/shortcuts";
|
import ShortcutsModal from "components/command-palette/shortcuts";
|
||||||
import CreateProjectModal from "components/project/CreateProjectModal";
|
import CreateProjectModal from "components/project/create-project-modal";
|
||||||
import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal";
|
import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal";
|
||||||
import CreateUpdateCycleModal from "components/project/cycles/CreateUpdateCyclesModal";
|
import CreateUpdateCycleModal from "components/project/cycles/CreateUpdateCyclesModal";
|
||||||
// ui
|
// ui
|
||||||
@ -278,15 +278,15 @@ const CommandPalette: React.FC = () => {
|
|||||||
value={issue.id}
|
value={issue.id}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
className={`h-1.5 w-1.5 block rounded-full`}
|
className="flex-shrink-0 h-1.5 w-1.5 block rounded-full"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: issue.state_detail.color,
|
backgroundColor: issue.state_detail.color,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span className="text-xs text-gray-500">
|
<span className="flex-shrink-0 text-xs text-gray-500">
|
||||||
{activeProject?.identifier}-{issue.sequence_id}
|
{activeProject?.identifier}-{issue.sequence_id}
|
||||||
</span>
|
</span>
|
||||||
{issue.name}
|
<span>{issue.name}</span>
|
||||||
</div>
|
</div>
|
||||||
{active && (
|
{active && (
|
||||||
<button
|
<button
|
||||||
@ -297,10 +297,9 @@ const CommandPalette: React.FC = () => {
|
|||||||
);
|
);
|
||||||
handleCommandPaletteClose();
|
handleCommandPaletteClose();
|
||||||
}}
|
}}
|
||||||
|
className="flex-shrink-0 text-gray-500"
|
||||||
>
|
>
|
||||||
<span className="justify-self-end flex-none text-gray-500">
|
Jump to...
|
||||||
Jump to...
|
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -20,7 +20,7 @@ import { Button, Select, TextArea } from "ui";
|
|||||||
import { ChevronDownIcon, CheckIcon } from "@heroicons/react/20/solid";
|
import { ChevronDownIcon, CheckIcon } from "@heroicons/react/20/solid";
|
||||||
|
|
||||||
// types
|
// types
|
||||||
import { ProjectMember, WorkspaceMember } from "types";
|
import { IProjectMemberInvitation } from "types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -28,6 +28,11 @@ type Props = {
|
|||||||
members: any[];
|
members: any[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ProjectMember = IProjectMemberInvitation & {
|
||||||
|
member_id: string;
|
||||||
|
user_id: string;
|
||||||
|
};
|
||||||
|
|
||||||
const defaultValues: Partial<ProjectMember> = {
|
const defaultValues: Partial<ProjectMember> = {
|
||||||
email: "",
|
email: "",
|
||||||
message: "",
|
message: "",
|
||||||
@ -49,9 +54,15 @@ const SendProjectInvitationModal: React.FC<Props> = ({ isOpen, setIsOpen, member
|
|||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const { data: people } = useSWR<WorkspaceMember[]>(
|
const { data: people } = useSWR(
|
||||||
activeWorkspace ? WORKSPACE_MEMBERS : null,
|
activeWorkspace ? WORKSPACE_MEMBERS(activeWorkspace.slug) : null,
|
||||||
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null
|
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null,
|
||||||
|
{
|
||||||
|
onErrorRetry(err, _, __, revalidate, revalidateOpts) {
|
||||||
|
if (err?.status === 403) return;
|
||||||
|
setTimeout(() => revalidate(revalidateOpts), 5000);
|
||||||
|
},
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -9,19 +9,26 @@ import useToast from "lib/hooks/useToast";
|
|||||||
// icons
|
// icons
|
||||||
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
||||||
// ui
|
// ui
|
||||||
import { Button } from "ui";
|
import { Button, Input } from "ui";
|
||||||
// types
|
// types
|
||||||
import type { IProject } from "types";
|
import type { IProject } from "types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
onClose: () => void;
|
||||||
data?: IProject;
|
data: IProject | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ConfirmProjectDeletion: React.FC<Props> = ({ isOpen, setIsOpen, data }) => {
|
const ConfirmProjectDeletion: React.FC<Props> = ({ isOpen, data, onClose }) => {
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
|
const [selectedProject, setSelectedProject] = useState<IProject | null>(null);
|
||||||
|
|
||||||
|
const [confirmProjectName, setConfirmProjectName] = useState("");
|
||||||
|
const [confirmDeleteMyProject, setConfirmDeleteMyProject] = useState(false);
|
||||||
|
|
||||||
|
const canDelete = confirmProjectName === data?.name && confirmDeleteMyProject;
|
||||||
|
|
||||||
const { activeWorkspace, mutateProjects } = useUser();
|
const { activeWorkspace, mutateProjects } = useUser();
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
@ -29,13 +36,18 @@ const ConfirmProjectDeletion: React.FC<Props> = ({ isOpen, setIsOpen, data }) =>
|
|||||||
const cancelButtonRef = useRef(null);
|
const cancelButtonRef = useRef(null);
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setIsOpen(false);
|
|
||||||
setIsDeleteLoading(false);
|
setIsDeleteLoading(false);
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setConfirmProjectName("");
|
||||||
|
setConfirmDeleteMyProject(false);
|
||||||
|
clearTimeout(timer);
|
||||||
|
}, 350);
|
||||||
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeletion = async () => {
|
const handleDeletion = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
if (!data || !activeWorkspace) return;
|
if (!data || !activeWorkspace || !canDelete) return;
|
||||||
await projectService
|
await projectService
|
||||||
.deleteProject(activeWorkspace.slug, data.id)
|
.deleteProject(activeWorkspace.slug, data.id)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@ -54,8 +66,14 @@ const ConfirmProjectDeletion: React.FC<Props> = ({ isOpen, setIsOpen, data }) =>
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
data && setIsOpen(true);
|
if (data) setSelectedProject(data);
|
||||||
}, [data, setIsOpen]);
|
else {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setSelectedProject(null);
|
||||||
|
clearTimeout(timer);
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition.Root show={isOpen} as={React.Fragment}>
|
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||||
@ -104,11 +122,48 @@ const ConfirmProjectDeletion: React.FC<Props> = ({ isOpen, setIsOpen, data }) =>
|
|||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
Are you sure you want to delete project - {`"`}
|
Are you sure you want to delete project - {`"`}
|
||||||
<span className="italic">{data?.name}</span>
|
<span className="italic">{selectedProject?.name}</span>
|
||||||
{`"`} ? All of the data related to the project will be permanently
|
{`"`} ? All of the data related to the project will be permanently
|
||||||
removed. This action cannot be undone.
|
removed. This action cannot be undone.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="h-0.5 bg-gray-200 my-3" />
|
||||||
|
<div className="mt-3">
|
||||||
|
<p className="text-sm">
|
||||||
|
Enter the project name{" "}
|
||||||
|
<span className="font-semibold">{selectedProject?.name}</span> to
|
||||||
|
continue:
|
||||||
|
</p>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Project name"
|
||||||
|
className="mt-2"
|
||||||
|
value={confirmProjectName}
|
||||||
|
onChange={(e) => {
|
||||||
|
setConfirmProjectName(e.target.value);
|
||||||
|
}}
|
||||||
|
name="projectName"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3">
|
||||||
|
<p className="text-sm">
|
||||||
|
To confirm, type <span className="font-semibold">delete my project</span>{" "}
|
||||||
|
below:
|
||||||
|
</p>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter 'delete my project'"
|
||||||
|
className="mt-2"
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e.target.value === "delete my project") {
|
||||||
|
setConfirmDeleteMyProject(true);
|
||||||
|
} else {
|
||||||
|
setConfirmDeleteMyProject(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
name="typeDelete"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -117,7 +172,7 @@ const ConfirmProjectDeletion: React.FC<Props> = ({ isOpen, setIsOpen, data }) =>
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={handleDeletion}
|
onClick={handleDeletion}
|
||||||
theme="danger"
|
theme="danger"
|
||||||
disabled={isDeleteLoading}
|
disabled={isDeleteLoading || !canDelete}
|
||||||
className="inline-flex sm:ml-3"
|
className="inline-flex sm:ml-3"
|
||||||
>
|
>
|
||||||
{isDeleteLoading ? "Deleting..." : "Delete"}
|
{isDeleteLoading ? "Deleting..." : "Delete"}
|
@ -1,21 +1,24 @@
|
|||||||
import React, { useState, useEffect, useCallback } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
// swr
|
// swr
|
||||||
import { mutate } from "swr";
|
import useSWR, { mutate } from "swr";
|
||||||
// react hook form
|
// react hook form
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
// headless
|
// headless
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
// services
|
// services
|
||||||
import projectServices from "lib/services/project.service";
|
import projectServices from "lib/services/project.service";
|
||||||
|
import workspaceService from "lib/services/workspace.service";
|
||||||
|
// common
|
||||||
|
import { createSimilarString } from "constants/common";
|
||||||
|
// constants
|
||||||
|
import { NETWORK_CHOICES } from "constants/";
|
||||||
// fetch keys
|
// fetch keys
|
||||||
import { PROJECTS_LIST } from "constants/fetch-keys";
|
import { PROJECTS_LIST, WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
||||||
// hooks
|
// hooks
|
||||||
import useUser from "lib/hooks/useUser";
|
import useUser from "lib/hooks/useUser";
|
||||||
import useToast from "lib/hooks/useToast";
|
import useToast from "lib/hooks/useToast";
|
||||||
// ui
|
// ui
|
||||||
import { Button, Input, TextArea, Select } from "ui";
|
import { Button, Input, TextArea, Select } from "ui";
|
||||||
// common
|
|
||||||
import { debounce } from "constants/common";
|
|
||||||
// types
|
// types
|
||||||
import { IProject } from "types";
|
import { IProject } from "types";
|
||||||
|
|
||||||
@ -24,11 +27,28 @@ type Props = {
|
|||||||
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const NETWORK_CHOICES = { "0": "Secret", "2": "Public" };
|
|
||||||
|
|
||||||
const defaultValues: Partial<IProject> = {
|
const defaultValues: Partial<IProject> = {
|
||||||
name: "",
|
name: "",
|
||||||
|
identifier: "",
|
||||||
description: "",
|
description: "",
|
||||||
|
network: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const IsGuestCondition: React.FC<{
|
||||||
|
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
}> = ({ setIsOpen }) => {
|
||||||
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
setToastAlert({
|
||||||
|
title: "Error",
|
||||||
|
type: "error",
|
||||||
|
message: "You don't have permission to create project.",
|
||||||
|
});
|
||||||
|
}, [setIsOpen, setToastAlert]);
|
||||||
|
|
||||||
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const CreateProjectModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
const CreateProjectModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
||||||
@ -40,7 +60,17 @@ const CreateProjectModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
|||||||
}, 500);
|
}, 500);
|
||||||
};
|
};
|
||||||
|
|
||||||
const { activeWorkspace } = useUser();
|
const { activeWorkspace, user } = useUser();
|
||||||
|
|
||||||
|
const { data: workspaceMembers } = useSWR(
|
||||||
|
activeWorkspace ? WORKSPACE_MEMBERS(activeWorkspace.slug) : null,
|
||||||
|
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null,
|
||||||
|
{
|
||||||
|
shouldRetryOnError: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const [recommendedIdentifier, setRecommendedIdentifier] = useState<string[]>([]);
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
@ -52,6 +82,7 @@ const CreateProjectModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
reset,
|
||||||
setError,
|
setError,
|
||||||
|
clearErrors,
|
||||||
watch,
|
watch,
|
||||||
setValue,
|
setValue,
|
||||||
} = useForm<IProject>({
|
} = useForm<IProject>({
|
||||||
@ -77,6 +108,16 @@ const CreateProjectModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
|||||||
handleClose();
|
handleClose();
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
|
if (err.status === 403) {
|
||||||
|
setToastAlert({
|
||||||
|
title: "Error",
|
||||||
|
type: "error",
|
||||||
|
message: "You don't have permission to create project.",
|
||||||
|
});
|
||||||
|
handleClose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
err = err.data;
|
||||||
Object.keys(err).map((key) => {
|
Object.keys(err).map((key) => {
|
||||||
const errorMessages = err[key];
|
const errorMessages = err[key];
|
||||||
setError(key as keyof IProject, {
|
setError(key as keyof IProject, {
|
||||||
@ -89,22 +130,39 @@ const CreateProjectModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
|||||||
const projectName = watch("name") ?? "";
|
const projectName = watch("name") ?? "";
|
||||||
const projectIdentifier = watch("identifier") ?? "";
|
const projectIdentifier = watch("identifier") ?? "";
|
||||||
|
|
||||||
const checkIdentifier = (slug: string, value: string) => {
|
if (workspaceMembers) {
|
||||||
projectServices.checkProjectIdentifierAvailability(slug, value).then((response) => {
|
const isMember = workspaceMembers.find((member) => member.member.id === user?.id);
|
||||||
console.log(response);
|
const isGuest = workspaceMembers.find(
|
||||||
if (response.exists) setError("identifier", { message: "Identifier already exists" });
|
(member) => member.member.id === user?.id && member.role === 5
|
||||||
});
|
);
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
if ((!isMember || isGuest) && isOpen) return <IsGuestCondition setIsOpen={setIsOpen} />;
|
||||||
const checkIdentifierAvailability = useCallback(debounce(checkIdentifier, 1500), []);
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (projectName && isChangeIdentifierRequired) {
|
if (projectName && isChangeIdentifierRequired) {
|
||||||
setValue("identifier", projectName.replace(/ /g, "-").toUpperCase().substring(0, 3));
|
setValue("identifier", projectName.replace(/ /g, "").toUpperCase().substring(0, 3));
|
||||||
}
|
}
|
||||||
}, [projectName, projectIdentifier, setValue, isChangeIdentifierRequired]);
|
}, [projectName, projectIdentifier, setValue, isChangeIdentifierRequired]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!projectName) return;
|
||||||
|
const suggestedIdentifier = createSimilarString(
|
||||||
|
projectName.replace(/ /g, "").toUpperCase().substring(0, 3)
|
||||||
|
);
|
||||||
|
|
||||||
|
setRecommendedIdentifier([
|
||||||
|
suggestedIdentifier + Math.floor(Math.random() * 101),
|
||||||
|
suggestedIdentifier + Math.floor(Math.random() * 101),
|
||||||
|
projectIdentifier.toUpperCase().substring(0, 3) + Math.floor(Math.random() * 101),
|
||||||
|
projectIdentifier.toUpperCase().substring(0, 3) + Math.floor(Math.random() * 101),
|
||||||
|
]);
|
||||||
|
}, [errors.identifier]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => setIsChangeIdentifierRequired(true);
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition.Root show={isOpen} as={React.Fragment}>
|
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||||
<Dialog as="div" className="relative z-10" onClose={handleClose}>
|
<Dialog as="div" className="relative z-10" onClose={handleClose}>
|
||||||
@ -191,11 +249,7 @@ const CreateProjectModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
|||||||
placeholder="Enter Project Identifier"
|
placeholder="Enter Project Identifier"
|
||||||
error={errors.identifier}
|
error={errors.identifier}
|
||||||
register={register}
|
register={register}
|
||||||
onChange={(e: any) => {
|
onChange={() => setIsChangeIdentifierRequired(false)}
|
||||||
setIsChangeIdentifierRequired(false);
|
|
||||||
if (!activeWorkspace || !e.target.value) return;
|
|
||||||
checkIdentifierAvailability(activeWorkspace.slug, e.target.value);
|
|
||||||
}}
|
|
||||||
validations={{
|
validations={{
|
||||||
required: "Identifier is required",
|
required: "Identifier is required",
|
||||||
minLength: {
|
minLength: {
|
||||||
@ -203,11 +257,32 @@ const CreateProjectModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
|||||||
message: "Identifier must at least be of 1 character",
|
message: "Identifier must at least be of 1 character",
|
||||||
},
|
},
|
||||||
maxLength: {
|
maxLength: {
|
||||||
value: 9,
|
value: 5,
|
||||||
message: "Identifier must at most be of 9 characters",
|
message: "Identifier must at most be of 5 characters",
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{errors.identifier && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<p>Ops! Identifier is already taken. Try one of the following:</p>
|
||||||
|
<div className="flex gap-x-2">
|
||||||
|
{recommendedIdentifier.map((identifier) => (
|
||||||
|
<button
|
||||||
|
key={identifier}
|
||||||
|
type="button"
|
||||||
|
className="text-sm text-gray-500 hover:text-gray-700 border p-2 py-0.5 rounded"
|
||||||
|
onClick={() => {
|
||||||
|
clearErrors("identifier");
|
||||||
|
setValue("identifier", identifier);
|
||||||
|
setIsChangeIdentifierRequired(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{identifier}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@ -4,7 +4,7 @@ import { mutate } from "swr";
|
|||||||
// headless ui
|
// headless ui
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
// services
|
// services
|
||||||
import cycleService from "lib/services/cycles.services";
|
import cycleService from "lib/services/cycles.service";
|
||||||
// fetch api
|
// fetch api
|
||||||
import { CYCLE_LIST } from "constants/fetch-keys";
|
import { CYCLE_LIST } from "constants/fetch-keys";
|
||||||
// hooks
|
// hooks
|
||||||
|
@ -6,7 +6,7 @@ import { useForm } from "react-hook-form";
|
|||||||
// headless
|
// headless
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
// services
|
// services
|
||||||
import cycleService from "lib/services/cycles.services";
|
import cycleService from "lib/services/cycles.service";
|
||||||
// fetch keys
|
// fetch keys
|
||||||
import { CYCLE_LIST } from "constants/fetch-keys";
|
import { CYCLE_LIST } from "constants/fetch-keys";
|
||||||
// hooks
|
// hooks
|
||||||
|
@ -7,7 +7,7 @@ import { Combobox, Dialog, Transition } from "@headlessui/react";
|
|||||||
// ui
|
// ui
|
||||||
import { Button } from "ui";
|
import { Button } from "ui";
|
||||||
// services
|
// services
|
||||||
import issuesServices from "lib/services/issues.services";
|
import issuesServices from "lib/services/issues.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useUser from "lib/hooks/useUser";
|
import useUser from "lib/hooks/useUser";
|
||||||
import useToast from "lib/hooks/useToast";
|
import useToast from "lib/hooks/useToast";
|
||||||
|
@ -7,7 +7,7 @@ import useSWR, { mutate } from "swr";
|
|||||||
// headless ui
|
// headless ui
|
||||||
import { Disclosure, Transition, Menu } from "@headlessui/react";
|
import { Disclosure, Transition, Menu } from "@headlessui/react";
|
||||||
// services
|
// services
|
||||||
import cycleServices from "lib/services/cycles.services";
|
import cycleServices from "lib/services/cycles.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useUser from "lib/hooks/useUser";
|
import useUser from "lib/hooks/useUser";
|
||||||
// components
|
// components
|
||||||
@ -22,7 +22,7 @@ import type { CycleViewProps as Props, CycleIssueResponse, IssueResponse } from
|
|||||||
import { CYCLE_ISSUES } from "constants/fetch-keys";
|
import { CYCLE_ISSUES } from "constants/fetch-keys";
|
||||||
// constants
|
// constants
|
||||||
import { renderShortNumericDateFormat } from "constants/common";
|
import { renderShortNumericDateFormat } from "constants/common";
|
||||||
import issuesServices from "lib/services/issues.services";
|
import issuesServices from "lib/services/issues.service";
|
||||||
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
||||||
import { Draggable } from "react-beautiful-dnd";
|
import { Draggable } from "react-beautiful-dnd";
|
||||||
|
|
||||||
|
@ -18,11 +18,10 @@ import {
|
|||||||
ArrowsPointingOutIcon,
|
ArrowsPointingOutIcon,
|
||||||
CalendarDaysIcon,
|
CalendarDaysIcon,
|
||||||
EllipsisHorizontalIcon,
|
EllipsisHorizontalIcon,
|
||||||
PencilIcon,
|
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { divide } from "lodash";
|
import { getPriorityIcon } from "constants/global";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
selectedGroup: NestedKeyOf<IIssue> | null;
|
selectedGroup: NestedKeyOf<IIssue> | null;
|
||||||
@ -42,8 +41,8 @@ type Props = {
|
|||||||
>
|
>
|
||||||
>;
|
>;
|
||||||
bgColor?: string;
|
bgColor?: string;
|
||||||
stateId?: string;
|
stateId: string | null;
|
||||||
createdBy?: string;
|
createdBy: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SingleBoard: React.FC<Props> = ({
|
const SingleBoard: React.FC<Props> = ({
|
||||||
@ -109,7 +108,7 @@ const SingleBoard: React.FC<Props> = ({
|
|||||||
<span
|
<span
|
||||||
className={`w-3 h-3 block rounded-full ${!show ? "" : "mr-1"}`}
|
className={`w-3 h-3 block rounded-full ${!show ? "" : "mr-1"}`}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: bgColor,
|
backgroundColor: Boolean(bgColor) ? bgColor : undefined,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<h2
|
<h2
|
||||||
@ -151,7 +150,7 @@ const SingleBoard: React.FC<Props> = ({
|
|||||||
setIsIssueOpen(true);
|
setIsIssueOpen(true);
|
||||||
if (selectedGroup !== null)
|
if (selectedGroup !== null)
|
||||||
setPreloadedData({
|
setPreloadedData({
|
||||||
state: stateId,
|
state: stateId !== null ? stateId : undefined,
|
||||||
[selectedGroup]: groupTitle,
|
[selectedGroup]: groupTitle,
|
||||||
actionType: "createIssue",
|
actionType: "createIssue",
|
||||||
});
|
});
|
||||||
@ -159,17 +158,6 @@ const SingleBoard: React.FC<Props> = ({
|
|||||||
>
|
>
|
||||||
<PlusIcon className="h-4 w-4" />
|
<PlusIcon className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-200 duration-300 outline-none"
|
|
||||||
onClick={() =>
|
|
||||||
setPreloadedData({
|
|
||||||
actionType: "edit",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<PencilIcon className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<StrictModeDroppable key={groupTitle} droppableId={groupTitle}>
|
<StrictModeDroppable key={groupTitle} droppableId={groupTitle}>
|
||||||
@ -192,131 +180,116 @@ const SingleBoard: React.FC<Props> = ({
|
|||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
{...provided.draggableProps}
|
{...provided.draggableProps}
|
||||||
>
|
>
|
||||||
<div
|
<div className="p-2 select-none" {...provided.dragHandleProps}>
|
||||||
className="px-2 py-3 space-y-1.5 select-none"
|
{properties.key && (
|
||||||
{...provided.dragHandleProps}
|
<div className="text-xs font-medium text-gray-500 mb-2">
|
||||||
>
|
{childIssue.project_detail?.identifier}-{childIssue.sequence_id}
|
||||||
<span className="group-hover:text-theme text-sm break-all">
|
</div>
|
||||||
|
)}
|
||||||
|
<h5 className="group-hover:text-theme text-sm break-all mb-3">
|
||||||
{childIssue.name}
|
{childIssue.name}
|
||||||
</span>
|
</h5>
|
||||||
{Object.keys(properties).map(
|
<div className="flex items-center gap-x-1 gap-y-2 text-xs flex-wrap">
|
||||||
(key) =>
|
{properties.priority && (
|
||||||
properties[key as keyof Properties] &&
|
<div
|
||||||
!Array.isArray(childIssue[key as keyof IIssue]) && (
|
className={`rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 capitalize ${
|
||||||
<div
|
childIssue.priority === "high"
|
||||||
key={key}
|
? "bg-red-100 text-red-600"
|
||||||
className={`${
|
: childIssue.priority === "medium"
|
||||||
key === "name"
|
? "bg-orange-100 text-orange-500"
|
||||||
? "text-sm mb-2"
|
: childIssue.priority === "low"
|
||||||
: key === "description"
|
? "bg-green-100 text-green-500"
|
||||||
? "text-xs text-black"
|
: "hidden"
|
||||||
: key === "priority"
|
}`}
|
||||||
? `text-xs bg-gray-200 px-2 py-1 mt-2 flex items-center gap-x-1 rounded w-min whitespace-nowrap capitalize font-medium ${
|
>
|
||||||
childIssue.priority === "high"
|
{/* {getPriorityIcon(childIssue.priority ?? "")} */}
|
||||||
? "bg-red-100 text-red-600"
|
{childIssue.priority}
|
||||||
: childIssue.priority === "medium"
|
</div>
|
||||||
? "bg-orange-100 text-orange-500"
|
)}
|
||||||
: childIssue.priority === "low"
|
{properties.state && (
|
||||||
? "bg-green-100 text-green-500"
|
<div className="flex-shrink-0 flex items-center gap-1 hover:bg-gray-100 border rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300">
|
||||||
: "hidden"
|
<span
|
||||||
}`
|
className="flex-shrink-0 h-1.5 w-1.5 rounded-full"
|
||||||
: key === "target_date"
|
style={{ backgroundColor: childIssue.state_detail.color }}
|
||||||
? "text-xs bg-indigo-50 px-2 py-1 mt-2 flex items-center gap-x-1 rounded w-min whitespace-nowrap"
|
></span>
|
||||||
: "text-sm text-gray-500"
|
{addSpaceIfCamelCase(childIssue.state_detail.name)}
|
||||||
} gap-1 relative
|
</div>
|
||||||
`}
|
)}
|
||||||
>
|
{properties.start_date && (
|
||||||
{key === "start_date" && childIssue.start_date !== null && (
|
<div className="flex-shrink-0 flex items-center gap-1 hover:bg-gray-100 border rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300">
|
||||||
<span className="text-sm">
|
<CalendarDaysIcon className="h-4 w-4" />
|
||||||
<CalendarDaysIcon className="h-4 w-4" />
|
{childIssue.start_date
|
||||||
{renderShortNumericDateFormat(childIssue.start_date)} -
|
? renderShortNumericDateFormat(childIssue.start_date)
|
||||||
{childIssue.target_date
|
: "N/A"}
|
||||||
? renderShortNumericDateFormat(childIssue.target_date)
|
</div>
|
||||||
: "None"}
|
)}
|
||||||
</span>
|
{properties.target_date && (
|
||||||
)}
|
<div
|
||||||
{key === "target_date" && (
|
className={`flex-shrink-0 group flex items-center gap-1 hover:bg-gray-100 border rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 ${
|
||||||
<>
|
childIssue.target_date === null
|
||||||
<span
|
? ""
|
||||||
className={`flex items-center gap-x-1 group ${
|
: childIssue.target_date < new Date().toISOString()
|
||||||
childIssue.target_date === null
|
? "text-red-600"
|
||||||
? ""
|
: findHowManyDaysLeft(childIssue.target_date) <= 3 &&
|
||||||
: childIssue.target_date < new Date().toISOString()
|
"text-orange-400"
|
||||||
? "text-red-600"
|
}`}
|
||||||
: findHowManyDaysLeft(childIssue.target_date) <=
|
>
|
||||||
3 && "text-orange-400"
|
<CalendarDaysIcon className="h-4 w-4" />
|
||||||
|
{childIssue.target_date
|
||||||
|
? renderShortNumericDateFormat(childIssue.target_date)
|
||||||
|
: "N/A"}
|
||||||
|
{childIssue.target_date && (
|
||||||
|
<span className="absolute -top-full mb-2 left-4 border transition-opacity opacity-0 group-hover:opacity-100 bg-white rounded px-2 py-1">
|
||||||
|
{childIssue.target_date < new Date().toISOString()
|
||||||
|
? `Target date has passed by ${findHowManyDaysLeft(
|
||||||
|
childIssue.target_date
|
||||||
|
)} days`
|
||||||
|
: findHowManyDaysLeft(childIssue.target_date) <= 3
|
||||||
|
? `Target date is in ${findHowManyDaysLeft(
|
||||||
|
childIssue.target_date
|
||||||
|
)} days`
|
||||||
|
: "Target date"}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{properties.assignee && (
|
||||||
|
<div className="justify-end w-full flex items-center gap-1 text-xs">
|
||||||
|
{childIssue?.assignee_details?.length > 0 ? (
|
||||||
|
childIssue?.assignee_details?.map(
|
||||||
|
(assignee, index: number) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={`relative z-[1] h-5 w-5 rounded-full ${
|
||||||
|
index !== 0 ? "-ml-2.5" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<CalendarDaysIcon className="h-4 w-4" />
|
{assignee.avatar && assignee.avatar !== "" ? (
|
||||||
{childIssue.target_date
|
<div className="h-5 w-5 border-2 bg-white border-white rounded-full">
|
||||||
? renderShortNumericDateFormat(childIssue.target_date)
|
<Image
|
||||||
: "N/A"}
|
src={assignee.avatar}
|
||||||
{childIssue.target_date && (
|
height="100%"
|
||||||
<span className="absolute -top-full mb-2 left-4 border transition-opacity opacity-0 group-hover:opacity-100 bg-white rounded px-2 py-1">
|
width="100%"
|
||||||
{childIssue.target_date < new Date().toISOString()
|
className="rounded-full"
|
||||||
? `Target date has passed by ${findHowManyDaysLeft(
|
alt={assignee.name}
|
||||||
childIssue.target_date
|
/>
|
||||||
)} days`
|
</div>
|
||||||
: findHowManyDaysLeft(childIssue.target_date) <= 3
|
) : (
|
||||||
? `Target date is in ${findHowManyDaysLeft(
|
<div
|
||||||
childIssue.target_date
|
className={`h-5 w-5 bg-gray-700 text-white border-2 border-white grid place-items-center rounded-full`}
|
||||||
)} days`
|
>
|
||||||
: "Target date"}
|
{assignee.first_name.charAt(0)}
|
||||||
</span>
|
</div>
|
||||||
)}
|
)}
|
||||||
</span>
|
</div>
|
||||||
</>
|
)
|
||||||
)}
|
)
|
||||||
{key === "key" && (
|
) : (
|
||||||
<span className="text-xs">
|
<span>No assignee.</span>
|
||||||
{childIssue.project_detail?.identifier}-
|
)}
|
||||||
{childIssue.sequence_id}
|
</div>
|
||||||
</span>
|
)}
|
||||||
)}
|
</div>
|
||||||
{key === "state" && (
|
|
||||||
<>{addSpaceIfCamelCase(childIssue["state_detail"].name)}</>
|
|
||||||
)}
|
|
||||||
{key === "priority" && <>{childIssue.priority}</>}
|
|
||||||
{/* {key === "description" && <>{childIssue.description}</>} */}
|
|
||||||
{key === "assignee" ? (
|
|
||||||
<div className="flex items-center gap-1 text-xs">
|
|
||||||
{childIssue?.assignee_details?.length > 0 ? (
|
|
||||||
childIssue?.assignee_details?.map(
|
|
||||||
(assignee, index: number) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className={`relative z-[1] h-5 w-5 rounded-full ${
|
|
||||||
index !== 0 ? "-ml-2.5" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{assignee.avatar && assignee.avatar !== "" ? (
|
|
||||||
<div className="h-5 w-5 border-2 bg-white border-white rounded-full">
|
|
||||||
<Image
|
|
||||||
src={assignee.avatar}
|
|
||||||
height="100%"
|
|
||||||
width="100%"
|
|
||||||
className="rounded-full"
|
|
||||||
alt={assignee.name}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className={`h-5 w-5 bg-gray-700 text-white border-2 border-white grid place-items-center rounded-full`}
|
|
||||||
>
|
|
||||||
{assignee.first_name.charAt(0)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<span>No assignee.</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
@ -331,7 +304,7 @@ const SingleBoard: React.FC<Props> = ({
|
|||||||
setIsIssueOpen(true);
|
setIsIssueOpen(true);
|
||||||
if (selectedGroup !== null) {
|
if (selectedGroup !== null) {
|
||||||
setPreloadedData({
|
setPreloadedData({
|
||||||
state: stateId,
|
state: stateId !== null ? stateId : undefined,
|
||||||
[selectedGroup]: groupTitle,
|
[selectedGroup]: groupTitle,
|
||||||
actionType: "createIssue",
|
actionType: "createIssue",
|
||||||
});
|
});
|
||||||
|
@ -7,8 +7,8 @@ import useSWR from "swr";
|
|||||||
import type { DropResult } from "react-beautiful-dnd";
|
import type { DropResult } from "react-beautiful-dnd";
|
||||||
import { DragDropContext } from "react-beautiful-dnd";
|
import { DragDropContext } from "react-beautiful-dnd";
|
||||||
// services
|
// services
|
||||||
import stateServices from "lib/services/state.services";
|
import stateServices from "lib/services/state.service";
|
||||||
import issuesServices from "lib/services/issues.services";
|
import issuesServices from "lib/services/issues.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useUser from "lib/hooks/useUser";
|
import useUser from "lib/hooks/useUser";
|
||||||
// fetching keys
|
// fetching keys
|
||||||
@ -20,7 +20,7 @@ import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssue
|
|||||||
// ui
|
// ui
|
||||||
import { Spinner } from "ui";
|
import { Spinner } from "ui";
|
||||||
// types
|
// types
|
||||||
import type { IState, IIssue, Properties, NestedKeyOf, ProjectMember } from "types";
|
import type { IState, IIssue, Properties, NestedKeyOf, IProjectMember } from "types";
|
||||||
import ConfirmIssueDeletion from "../ConfirmIssueDeletion";
|
import ConfirmIssueDeletion from "../ConfirmIssueDeletion";
|
||||||
import { TrashIcon } from "@heroicons/react/24/outline";
|
import { TrashIcon } from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
@ -30,7 +30,7 @@ type Props = {
|
|||||||
groupedByIssues: {
|
groupedByIssues: {
|
||||||
[key: string]: IIssue[];
|
[key: string]: IIssue[];
|
||||||
};
|
};
|
||||||
members: ProjectMember[] | undefined;
|
members: IProjectMember[] | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const BoardView: React.FC<Props> = ({ properties, selectedGroup, groupedByIssues, members }) => {
|
const BoardView: React.FC<Props> = ({ properties, selectedGroup, groupedByIssues, members }) => {
|
||||||
@ -197,9 +197,10 @@ const BoardView: React.FC<Props> = ({ properties, selectedGroup, groupedByIssues
|
|||||||
selectedGroup={selectedGroup}
|
selectedGroup={selectedGroup}
|
||||||
groupTitle={singleGroup}
|
groupTitle={singleGroup}
|
||||||
createdBy={
|
createdBy={
|
||||||
members
|
selectedGroup === "created_by"
|
||||||
? members?.find((m) => m.member.id === singleGroup)?.member.first_name
|
? members?.find((m) => m.member.id === singleGroup)?.member
|
||||||
: undefined
|
.first_name ?? "loading..."
|
||||||
|
: null
|
||||||
}
|
}
|
||||||
groupedByIssues={groupedByIssues}
|
groupedByIssues={groupedByIssues}
|
||||||
index={index}
|
index={index}
|
||||||
@ -208,8 +209,8 @@ const BoardView: React.FC<Props> = ({ properties, selectedGroup, groupedByIssues
|
|||||||
setPreloadedData={setPreloadedData}
|
setPreloadedData={setPreloadedData}
|
||||||
stateId={
|
stateId={
|
||||||
selectedGroup === "state_detail.name"
|
selectedGroup === "state_detail.name"
|
||||||
? states?.find((s) => s.name === singleGroup)?.id
|
? states?.find((s) => s.name === singleGroup)?.id ?? null
|
||||||
: undefined
|
: null
|
||||||
}
|
}
|
||||||
bgColor={
|
bgColor={
|
||||||
selectedGroup === "state_detail.name"
|
selectedGroup === "state_detail.name"
|
||||||
|
@ -4,7 +4,7 @@ import { mutate } from "swr";
|
|||||||
// headless ui
|
// headless ui
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
// services
|
// services
|
||||||
import stateServices from "lib/services/state.services";
|
import stateServices from "lib/services/state.service";
|
||||||
// fetch api
|
// fetch api
|
||||||
import { STATE_LIST } from "constants/fetch-keys";
|
import { STATE_LIST } from "constants/fetch-keys";
|
||||||
// hooks
|
// hooks
|
||||||
@ -43,7 +43,7 @@ const ConfirmStateDeletion: React.FC<Props> = ({ isOpen, setIsOpen, data }) => {
|
|||||||
mutate<IState[]>(
|
mutate<IState[]>(
|
||||||
STATE_LIST(data.project),
|
STATE_LIST(data.project),
|
||||||
(prevData) => prevData?.filter((state) => state.id !== data?.id),
|
(prevData) => prevData?.filter((state) => state.id !== data?.id),
|
||||||
false,
|
false
|
||||||
);
|
);
|
||||||
handleClose();
|
handleClose();
|
||||||
})
|
})
|
||||||
@ -98,18 +98,15 @@ const ConfirmStateDeletion: React.FC<Props> = ({ isOpen, setIsOpen, data }) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||||
<Dialog.Title
|
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900">
|
||||||
as="h3"
|
|
||||||
className="text-lg font-medium leading-6 text-gray-900"
|
|
||||||
>
|
|
||||||
Delete State
|
Delete State
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
Are you sure you want to delete state - {`"`}
|
Are you sure you want to delete state - {`"`}
|
||||||
<span className="italic">{data?.name}</span>
|
<span className="italic">{data?.name}</span>
|
||||||
{`"`} ? All of the data related to the state will be
|
{`"`} ? All of the data related to the state will be permanently removed.
|
||||||
permanently removed. This action cannot be undone.
|
This action cannot be undone.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -8,7 +8,7 @@ import { TwitterPicker } from "react-color";
|
|||||||
// headless
|
// headless
|
||||||
import { Dialog, Popover, Transition } from "@headlessui/react";
|
import { Dialog, Popover, Transition } from "@headlessui/react";
|
||||||
// services
|
// services
|
||||||
import stateService from "lib/services/state.services";
|
import stateService from "lib/services/state.service";
|
||||||
// fetch keys
|
// fetch keys
|
||||||
import { STATE_LIST } from "constants/fetch-keys";
|
import { STATE_LIST } from "constants/fetch-keys";
|
||||||
// hooks
|
// hooks
|
||||||
|
@ -6,7 +6,7 @@ import { Dialog, Transition } from "@headlessui/react";
|
|||||||
// fetching keys
|
// fetching keys
|
||||||
import { PROJECT_ISSUES_LIST } from "constants/fetch-keys";
|
import { PROJECT_ISSUES_LIST } from "constants/fetch-keys";
|
||||||
// services
|
// services
|
||||||
import issueServices from "lib/services/issues.services";
|
import issueServices from "lib/services/issues.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useUser from "lib/hooks/useUser";
|
import useUser from "lib/hooks/useUser";
|
||||||
import useToast from "lib/hooks/useToast";
|
import useToast from "lib/hooks/useToast";
|
||||||
|
@ -11,7 +11,7 @@ import useUser from "lib/hooks/useUser";
|
|||||||
import { PROJECT_MEMBERS } from "constants/fetch-keys";
|
import { PROJECT_MEMBERS } from "constants/fetch-keys";
|
||||||
// types
|
// types
|
||||||
import type { Control } from "react-hook-form";
|
import type { Control } from "react-hook-form";
|
||||||
import type { IIssue, WorkspaceMember } from "types";
|
import type { IIssue } from "types";
|
||||||
import { UserIcon } from "@heroicons/react/24/outline";
|
import { UserIcon } from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
import { SearchListbox } from "ui";
|
import { SearchListbox } from "ui";
|
||||||
@ -23,7 +23,7 @@ type Props = {
|
|||||||
const SelectAssignee: React.FC<Props> = ({ control }) => {
|
const SelectAssignee: React.FC<Props> = ({ control }) => {
|
||||||
const { activeWorkspace, activeProject } = useUser();
|
const { activeWorkspace, activeProject } = useUser();
|
||||||
|
|
||||||
const { data: people } = useSWR<WorkspaceMember[]>(
|
const { data: people } = useSWR(
|
||||||
activeWorkspace && activeProject ? PROJECT_MEMBERS(activeProject.id) : null,
|
activeWorkspace && activeProject ? PROJECT_MEMBERS(activeProject.id) : null,
|
||||||
activeWorkspace && activeProject
|
activeWorkspace && activeProject
|
||||||
? () => projectServices.projectMembers(activeWorkspace.slug, activeProject.id)
|
? () => projectServices.projectMembers(activeWorkspace.slug, activeProject.id)
|
||||||
|
@ -6,7 +6,7 @@ import { useForm, Controller } from "react-hook-form";
|
|||||||
// headless ui
|
// headless ui
|
||||||
import { Listbox, Transition } from "@headlessui/react";
|
import { Listbox, Transition } from "@headlessui/react";
|
||||||
// services
|
// services
|
||||||
import issuesServices from "lib/services/issues.services";
|
import issuesServices from "lib/services/issues.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useUser from "lib/hooks/useUser";
|
import useUser from "lib/hooks/useUser";
|
||||||
// fetching keys
|
// fetching keys
|
||||||
|
@ -14,7 +14,7 @@ type Props = {
|
|||||||
control: Control<IIssue, any>;
|
control: Control<IIssue, any>;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
issues: IssueResponse | undefined;
|
issues: IIssue[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const SelectParent: React.FC<Props> = ({ control, isOpen, setIsOpen, issues }) => {
|
const SelectParent: React.FC<Props> = ({ control, isOpen, setIsOpen, issues }) => {
|
||||||
|
@ -16,7 +16,7 @@ import {
|
|||||||
// headless
|
// headless
|
||||||
import { Dialog, Menu, Transition } from "@headlessui/react";
|
import { Dialog, Menu, Transition } from "@headlessui/react";
|
||||||
// services
|
// services
|
||||||
import issuesServices from "lib/services/issues.services";
|
import issuesServices from "lib/services/issues.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useUser from "lib/hooks/useUser";
|
import useUser from "lib/hooks/useUser";
|
||||||
import useToast from "lib/hooks/useToast";
|
import useToast from "lib/hooks/useToast";
|
||||||
@ -392,16 +392,16 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
|
|||||||
/> */}
|
/> */}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Input
|
{/* <Input
|
||||||
id="target_date"
|
id="target_date"
|
||||||
label="Due Date"
|
label="Target Date"
|
||||||
name="target_date"
|
name="target_date"
|
||||||
type="date"
|
type="date"
|
||||||
placeholder="Enter name"
|
placeholder="Enter name"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
error={errors.target_date}
|
error={errors.target_date}
|
||||||
register={register}
|
register={register}
|
||||||
/>
|
/> */}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center flex-wrap gap-2">
|
<div className="flex items-center flex-wrap gap-2">
|
||||||
<SelectState control={control} setIsOpen={setIsStateModalOpen} />
|
<SelectState control={control} setIsOpen={setIsStateModalOpen} />
|
||||||
@ -409,11 +409,25 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
|
|||||||
<SelectPriority control={control} />
|
<SelectPriority control={control} />
|
||||||
<SelectAssignee control={control} />
|
<SelectAssignee control={control} />
|
||||||
<SelectLabels control={control} />
|
<SelectLabels control={control} />
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="target_date"
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={value ?? ""}
|
||||||
|
onChange={(e: any) => {
|
||||||
|
onChange(e.target.value);
|
||||||
|
}}
|
||||||
|
className="hover:bg-gray-100 border rounded-md shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<SelectParent
|
<SelectParent
|
||||||
control={control}
|
control={control}
|
||||||
isOpen={parentIssueListModalOpen}
|
isOpen={parentIssueListModalOpen}
|
||||||
setIsOpen={setParentIssueListModalOpen}
|
setIsOpen={setParentIssueListModalOpen}
|
||||||
issues={issues}
|
issues={issues?.results ?? []}
|
||||||
/>
|
/>
|
||||||
<Menu as="div" className="relative inline-block">
|
<Menu as="div" className="relative inline-block">
|
||||||
<Menu.Button className="grid place-items-center p-1 hover:bg-gray-100 border rounded-md shadow-sm cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm duration-300">
|
<Menu.Button className="grid place-items-center p-1 hover:bg-gray-100 border rounded-md shadow-sm cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm duration-300">
|
||||||
|
@ -2,35 +2,49 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
// headless ui
|
// headless ui
|
||||||
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
||||||
|
// ui
|
||||||
|
import { Button } from "ui";
|
||||||
// icons
|
// icons
|
||||||
import { MagnifyingGlassIcon, RectangleStackIcon } from "@heroicons/react/24/outline";
|
import { MagnifyingGlassIcon, RectangleStackIcon } from "@heroicons/react/24/outline";
|
||||||
// types
|
// types
|
||||||
import { IIssue, IssueResponse } from "types";
|
import { IIssue } from "types";
|
||||||
import { classNames } from "constants/common";
|
import { classNames } from "constants/common";
|
||||||
import useUser from "lib/hooks/useUser";
|
import useUser from "lib/hooks/useUser";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
handleClose: () => void;
|
handleClose: () => void;
|
||||||
|
value?: any;
|
||||||
onChange: (...event: any[]) => void;
|
onChange: (...event: any[]) => void;
|
||||||
issues: IssueResponse | undefined;
|
issues: IIssue[];
|
||||||
|
title?: string;
|
||||||
|
multiple?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const IssuesListModal: React.FC<Props> = ({ isOpen, handleClose: onClose, onChange, issues }) => {
|
const IssuesListModal: React.FC<Props> = ({
|
||||||
|
isOpen,
|
||||||
|
handleClose: onClose,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
issues,
|
||||||
|
title = "Issues",
|
||||||
|
multiple = false,
|
||||||
|
}) => {
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
|
const [values, setValues] = useState<string[]>([]);
|
||||||
|
|
||||||
const { activeProject } = useUser();
|
const { activeProject } = useUser();
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
onClose();
|
onClose();
|
||||||
setQuery("");
|
setQuery("");
|
||||||
|
setValues([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredIssues: IIssue[] =
|
const filteredIssues: IIssue[] =
|
||||||
query === ""
|
query === ""
|
||||||
? issues?.results ?? []
|
? issues ?? []
|
||||||
: issues?.results.filter((issue) => issue.name.toLowerCase().includes(query.toLowerCase())) ??
|
: issues?.filter((issue) => issue.name.toLowerCase().includes(query.toLowerCase())) ?? [];
|
||||||
[];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -59,7 +73,14 @@ const IssuesListModal: React.FC<Props> = ({ isOpen, handleClose: onClose, onChan
|
|||||||
leaveTo="opacity-0 scale-95"
|
leaveTo="opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-gray-500 divide-opacity-10 rounded-xl bg-white bg-opacity-80 shadow-2xl ring-1 ring-black ring-opacity-5 backdrop-blur backdrop-filter transition-all">
|
<Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-gray-500 divide-opacity-10 rounded-xl bg-white bg-opacity-80 shadow-2xl ring-1 ring-black ring-opacity-5 backdrop-blur backdrop-filter transition-all">
|
||||||
<Combobox onChange={onChange}>
|
<Combobox
|
||||||
|
value={value}
|
||||||
|
onChange={(val) => {
|
||||||
|
if (multiple) setValues(val);
|
||||||
|
else onChange(val);
|
||||||
|
}}
|
||||||
|
// multiple={multiple}
|
||||||
|
>
|
||||||
<div className="relative m-1">
|
<div className="relative m-1">
|
||||||
<MagnifyingGlassIcon
|
<MagnifyingGlassIcon
|
||||||
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-gray-900 text-opacity-40"
|
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-gray-900 text-opacity-40"
|
||||||
@ -80,7 +101,7 @@ const IssuesListModal: React.FC<Props> = ({ isOpen, handleClose: onClose, onChan
|
|||||||
<li className="p-2">
|
<li className="p-2">
|
||||||
{query === "" && (
|
{query === "" && (
|
||||||
<h2 className="mt-4 mb-2 px-3 text-xs font-semibold text-gray-900">
|
<h2 className="mt-4 mb-2 px-3 text-xs font-semibold text-gray-900">
|
||||||
Issues
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
)}
|
)}
|
||||||
<ul className="text-sm text-gray-700">
|
<ul className="text-sm text-gray-700">
|
||||||
@ -95,20 +116,26 @@ const IssuesListModal: React.FC<Props> = ({ isOpen, handleClose: onClose, onChan
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// setIssueIdFromList(issue.id);
|
if (!multiple) handleClose();
|
||||||
handleClose();
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span
|
{({ selected }) => (
|
||||||
className={`h-1.5 w-1.5 block rounded-full`}
|
<>
|
||||||
style={{
|
{multiple ? (
|
||||||
backgroundColor: issue.state_detail.color,
|
<input type="checkbox" checked={selected} readOnly />
|
||||||
}}
|
) : null}
|
||||||
/>
|
<span
|
||||||
<span className="text-xs text-gray-500">
|
className="flex-shrink-0 h-1.5 w-1.5 block rounded-full"
|
||||||
{activeProject?.identifier}-{issue.sequence_id}
|
style={{
|
||||||
</span>{" "}
|
backgroundColor: issue.state_detail.color,
|
||||||
{issue.name}
|
}}
|
||||||
|
/>
|
||||||
|
<span className="flex-shrink-0 text-xs text-gray-500">
|
||||||
|
{activeProject?.identifier}-{issue.sequence_id}
|
||||||
|
</span>{" "}
|
||||||
|
{issue.name}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Combobox.Option>
|
</Combobox.Option>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
@ -128,6 +155,16 @@ const IssuesListModal: React.FC<Props> = ({ isOpen, handleClose: onClose, onChan
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Combobox>
|
</Combobox>
|
||||||
|
{multiple ? (
|
||||||
|
<div className="flex justify-end items-center gap-2 p-3">
|
||||||
|
<Button type="button" theme="danger" size="sm" onClick={handleClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="button" size="sm" onClick={() => onChange(values)}>
|
||||||
|
Add to Cycle
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</Dialog.Panel>
|
</Dialog.Panel>
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
// react
|
// react
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useState } from "react";
|
||||||
// next
|
// next
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
@ -10,23 +10,17 @@ import { Listbox, Transition } from "@headlessui/react";
|
|||||||
// icons
|
// icons
|
||||||
import { PencilIcon, TrashIcon } from "@heroicons/react/24/outline";
|
import { PencilIcon, TrashIcon } from "@heroicons/react/24/outline";
|
||||||
// types
|
// types
|
||||||
import { IIssue, IssueResponse, IState, NestedKeyOf, Properties, WorkspaceMember } from "types";
|
import { IIssue, IssueResponse, NestedKeyOf, Properties } from "types";
|
||||||
// hooks
|
// hooks
|
||||||
import useUser from "lib/hooks/useUser";
|
import useUser from "lib/hooks/useUser";
|
||||||
// fetch keys
|
// fetch keys
|
||||||
import { PRIORITIES } from "constants/";
|
import { PRIORITIES } from "constants/";
|
||||||
import { PROJECT_ISSUES_LIST, WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
import { PROJECT_ISSUES_LIST, WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
||||||
// services
|
// services
|
||||||
import issuesServices from "lib/services/issues.services";
|
import issuesServices from "lib/services/issues.service";
|
||||||
import workspaceService from "lib/services/workspace.service";
|
import workspaceService from "lib/services/workspace.service";
|
||||||
// constants
|
// constants
|
||||||
import {
|
import { addSpaceIfCamelCase, classNames, renderShortNumericDateFormat } from "constants/common";
|
||||||
addSpaceIfCamelCase,
|
|
||||||
classNames,
|
|
||||||
renderShortNumericDateFormat,
|
|
||||||
replaceUnderscoreIfSnakeCase,
|
|
||||||
} from "constants/common";
|
|
||||||
import IssuePreviewModal from "../PreviewModal";
|
|
||||||
|
|
||||||
// types
|
// types
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -44,9 +38,6 @@ const ListView: React.FC<Props> = ({
|
|||||||
setSelectedIssue,
|
setSelectedIssue,
|
||||||
handleDeleteIssue,
|
handleDeleteIssue,
|
||||||
}) => {
|
}) => {
|
||||||
const [issuePreviewModal, setIssuePreviewModal] = useState(false);
|
|
||||||
const [previewModalIssueId, setPreviewModalIssueId] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const { activeWorkspace, activeProject, states } = useUser();
|
const { activeWorkspace, activeProject, states } = useUser();
|
||||||
|
|
||||||
const partialUpdateIssue = (formData: Partial<IIssue>, issueId: string) => {
|
const partialUpdateIssue = (formData: Partial<IIssue>, issueId: string) => {
|
||||||
@ -69,375 +60,355 @@ const ListView: React.FC<Props> = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const { data: people } = useSWR<WorkspaceMember[]>(
|
const { data: people } = useSWR(
|
||||||
activeWorkspace ? WORKSPACE_MEMBERS : null,
|
activeWorkspace ? WORKSPACE_MEMBERS : null,
|
||||||
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null
|
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleHover = (issueId: string) => {
|
|
||||||
document.addEventListener("keydown", (e) => {
|
|
||||||
// if (e.code === "Space") {
|
|
||||||
// e.preventDefault();
|
|
||||||
// setPreviewModalIssueId(issueId);
|
|
||||||
// setIssuePreviewModal(true);
|
|
||||||
// }
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-4 flex flex-col">
|
<div className="mt-4 flex flex-col space-y-5">
|
||||||
<IssuePreviewModal
|
{Object.keys(groupedByIssues).map((singleGroup) => (
|
||||||
isOpen={issuePreviewModal}
|
<div key={singleGroup} className="overflow-x-auto">
|
||||||
setIsOpen={setIssuePreviewModal}
|
<div className="inline-block min-w-full p-0.5 align-middle">
|
||||||
issueId={previewModalIssueId}
|
<div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
|
||||||
/>
|
<table className="min-w-full">
|
||||||
<div className="overflow-x-auto">
|
{selectedGroup !== null ? (
|
||||||
<div className="inline-block min-w-full p-0.5 align-middle">
|
<thead className="bg-gray-100">
|
||||||
<div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
|
<tr>
|
||||||
<table className="min-w-full">
|
<th
|
||||||
<thead className="bg-gray-100">
|
colSpan={14}
|
||||||
<tr>
|
scope="col"
|
||||||
<th
|
className="px-3 py-3.5 text-left uppercase text-sm font-semibold text-gray-900"
|
||||||
scope="col"
|
>
|
||||||
className="px-3 py-3.5 text-left uppercase text-sm font-semibold text-gray-900"
|
<div className="flex items-center gap-2">
|
||||||
>
|
{selectedGroup === "state_detail.name" ? (
|
||||||
NAME
|
<span
|
||||||
</th>
|
className="flex-shrink-0 h-1.5 w-1.5 block rounded-full"
|
||||||
{Object.keys(properties).map(
|
style={{
|
||||||
(key) =>
|
backgroundColor: states?.find((s) => s.name === singleGroup)?.color,
|
||||||
properties[key as keyof Properties] && (
|
}}
|
||||||
<th
|
></span>
|
||||||
key={key}
|
) : null}
|
||||||
scope="col"
|
|
||||||
className="px-3 py-3.5 text-left uppercase text-sm font-semibold text-gray-900"
|
|
||||||
>
|
|
||||||
{replaceUnderscoreIfSnakeCase(key)}
|
|
||||||
</th>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
<th
|
|
||||||
scope="col"
|
|
||||||
className="px-3 py-3.5 text-right text-sm font-semibold text-gray-900"
|
|
||||||
>
|
|
||||||
ACTIONS
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="bg-white">
|
|
||||||
{Object.keys(groupedByIssues).map((singleGroup) => (
|
|
||||||
<React.Fragment key={singleGroup}>
|
|
||||||
{selectedGroup !== null ? (
|
|
||||||
<tr className="border-t border-gray-200">
|
|
||||||
<th
|
|
||||||
colSpan={14}
|
|
||||||
scope="colgroup"
|
|
||||||
className="bg-gray-50 px-4 py-2 text-left font-medium text-gray-900 capitalize"
|
|
||||||
>
|
|
||||||
{singleGroup === null || singleGroup === "null"
|
{singleGroup === null || singleGroup === "null"
|
||||||
? selectedGroup === "priority" && "No priority"
|
? selectedGroup === "priority" && "No priority"
|
||||||
: addSpaceIfCamelCase(singleGroup)}
|
: addSpaceIfCamelCase(singleGroup)}
|
||||||
<span className="ml-2 text-gray-500 font-normal text-sm">
|
<span className="ml-2 text-gray-500 font-normal text-sm">
|
||||||
{groupedByIssues[singleGroup as keyof IIssue].length}
|
{groupedByIssues[singleGroup as keyof IIssue].length}
|
||||||
</span>
|
</span>
|
||||||
</th>
|
</div>
|
||||||
</tr>
|
</th>
|
||||||
) : null}
|
</tr>
|
||||||
{groupedByIssues[singleGroup].length > 0
|
</thead>
|
||||||
? groupedByIssues[singleGroup].map((issue: IIssue, index: number) => {
|
) : (
|
||||||
const assignees = [
|
<thead className="bg-gray-100">
|
||||||
...(issue?.assignees_list ?? []),
|
<tr>
|
||||||
...(issue?.assignees ?? []),
|
<th
|
||||||
]?.map(
|
colSpan={14}
|
||||||
(assignee) =>
|
scope="col"
|
||||||
people?.find((p) => p.member.id === assignee)?.member.email
|
className="px-3 py-3.5 text-left uppercase text-sm font-semibold text-gray-900"
|
||||||
);
|
>
|
||||||
|
ALL ISSUES
|
||||||
|
<span className="ml-2 text-gray-500 font-normal text-sm">
|
||||||
|
{groupedByIssues[singleGroup as keyof IIssue].length}
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
)}
|
||||||
|
<tbody className="bg-white">
|
||||||
|
{groupedByIssues[singleGroup].length > 0
|
||||||
|
? groupedByIssues[singleGroup].map((issue: IIssue, index: number) => {
|
||||||
|
const assignees = [
|
||||||
|
...(issue?.assignees_list ?? []),
|
||||||
|
...(issue?.assignees ?? []),
|
||||||
|
]?.map(
|
||||||
|
(assignee) => people?.find((p) => p.member.id === assignee)?.member.email
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
key={issue.id}
|
key={issue.id}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
index === 0 ? "border-gray-300" : "border-gray-200",
|
index === 0 ? "border-gray-300" : "border-gray-200",
|
||||||
"border-t"
|
"border-t"
|
||||||
)}
|
)}
|
||||||
onMouseEnter={() => handleHover(issue.id)}
|
>
|
||||||
>
|
<td className="px-3 py-4 text-sm font-medium text-gray-900 w-[15rem]">
|
||||||
<td className="px-3 py-4 text-sm font-medium text-gray-900 w-[15rem]">
|
<Link href={`/projects/${issue.project}/issues/${issue.id}`}>
|
||||||
<Link href={`/projects/${issue.project}/issues/${issue.id}`}>
|
<a className="hover:text-theme duration-300">{issue.name}</a>
|
||||||
<a className="hover:text-theme duration-300">{issue.name}</a>
|
</Link>
|
||||||
</Link>
|
</td>
|
||||||
</td>
|
{Object.keys(properties).map(
|
||||||
{Object.keys(properties).map(
|
(key) =>
|
||||||
(key) =>
|
properties[key as keyof Properties] && (
|
||||||
properties[key as keyof Properties] && (
|
<React.Fragment key={key}>
|
||||||
<React.Fragment key={key}>
|
{(key as keyof Properties) === "key" ? (
|
||||||
{(key as keyof Properties) === "key" ? (
|
<td className="px-3 py-4 font-medium text-gray-900 text-xs whitespace-nowrap">
|
||||||
<td className="px-3 py-4 font-medium text-gray-900 text-xs whitespace-nowrap">
|
{activeProject?.identifier}-{issue.sequence_id}
|
||||||
{activeProject?.identifier}-{issue.sequence_id}
|
</td>
|
||||||
</td>
|
) : (key as keyof Properties) === "priority" ? (
|
||||||
) : (key as keyof Properties) === "priority" ? (
|
<td className="px-3 py-4 text-sm font-medium text-gray-900 relative">
|
||||||
<td className="px-3 py-4 text-sm font-medium text-gray-900 relative">
|
<Listbox
|
||||||
<Listbox
|
as="div"
|
||||||
as="div"
|
value={issue.priority}
|
||||||
value={issue.priority}
|
onChange={(data: string) => {
|
||||||
onChange={(data: string) => {
|
partialUpdateIssue({ priority: data }, issue.id);
|
||||||
partialUpdateIssue({ priority: data }, issue.id);
|
}}
|
||||||
}}
|
className="flex-shrink-0"
|
||||||
className="flex-shrink-0"
|
>
|
||||||
>
|
{({ open }) => (
|
||||||
{({ open }) => (
|
<>
|
||||||
<>
|
<div className="">
|
||||||
<div className="">
|
<Listbox.Button className="inline-flex items-center whitespace-nowrap rounded-full bg-gray-50 py-1 px-0.5 text-xs font-medium text-gray-500 hover:bg-gray-100 border">
|
||||||
<Listbox.Button className="inline-flex items-center whitespace-nowrap rounded-full bg-gray-50 py-1 px-0.5 text-xs font-medium text-gray-500 hover:bg-gray-100 border">
|
<span
|
||||||
<span
|
className={classNames(
|
||||||
className={classNames(
|
issue.priority ? "" : "text-gray-900",
|
||||||
issue.priority ? "" : "text-gray-900",
|
"hidden truncate capitalize sm:block w-16"
|
||||||
"hidden truncate capitalize sm:block w-16"
|
)}
|
||||||
)}
|
|
||||||
>
|
|
||||||
{issue.priority ?? "None"}
|
|
||||||
</span>
|
|
||||||
</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 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
|
{issue.priority ?? "None"}
|
||||||
{PRIORITIES?.map((priority) => (
|
</span>
|
||||||
<Listbox.Option
|
</Listbox.Button>
|
||||||
key={priority}
|
|
||||||
className={({ active }) =>
|
<Transition
|
||||||
classNames(
|
show={open}
|
||||||
active ? "bg-indigo-50" : "bg-white",
|
as={React.Fragment}
|
||||||
"cursor-pointer capitalize select-none px-3 py-2"
|
leave="transition ease-in duration-100"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<Listbox.Options className="absolute z-10 mt-1 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
|
||||||
|
{PRIORITIES?.map((priority) => (
|
||||||
|
<Listbox.Option
|
||||||
|
key={priority}
|
||||||
|
className={({ active }) =>
|
||||||
|
classNames(
|
||||||
|
active ? "bg-indigo-50" : "bg-white",
|
||||||
|
"cursor-pointer capitalize select-none px-3 py-2"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
value={priority}
|
||||||
|
>
|
||||||
|
{priority}
|
||||||
|
</Listbox.Option>
|
||||||
|
))}
|
||||||
|
</Listbox.Options>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Listbox>
|
||||||
|
</td>
|
||||||
|
) : (key as keyof Properties) === "assignee" ? (
|
||||||
|
<td className="px-3 py-4 text-sm font-medium text-gray-900 relative">
|
||||||
|
<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 },
|
||||||
|
issue.id
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
className="flex-shrink-0"
|
||||||
|
>
|
||||||
|
{({ open }) => (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<Listbox.Button className="rounded-full bg-gray-50 px-5 py-1 text-xs text-gray-500 hover:bg-gray-100 border">
|
||||||
|
{() => {
|
||||||
|
if (assignees.length > 0)
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{assignees.map((assignee, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={
|
||||||
|
"hidden truncate sm:block text-left"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{assignee}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
else return <span>None</span>;
|
||||||
|
}}
|
||||||
|
</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 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
|
||||||
|
{people?.map((person) => (
|
||||||
|
<Listbox.Option
|
||||||
|
key={person.id}
|
||||||
|
className={({ active }) =>
|
||||||
|
classNames(
|
||||||
|
active ? "bg-indigo-50" : "bg-white",
|
||||||
|
"cursor-pointer select-none px-3 py-2"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
value={person.member.id}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`flex items-center gap-x-1 ${
|
||||||
|
assignees.includes(
|
||||||
|
person.member.first_name
|
||||||
)
|
)
|
||||||
}
|
? "font-medium"
|
||||||
value={priority}
|
: "font-normal"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{priority}
|
{person.member.avatar &&
|
||||||
</Listbox.Option>
|
person.member.avatar !== "" ? (
|
||||||
))}
|
<div className="relative w-4 h-4">
|
||||||
</Listbox.Options>
|
<Image
|
||||||
</Transition>
|
src={person.member.avatar}
|
||||||
</div>
|
alt="avatar"
|
||||||
</>
|
className="rounded-full"
|
||||||
)}
|
layout="fill"
|
||||||
</Listbox>
|
objectFit="cover"
|
||||||
</td>
|
/>
|
||||||
) : (key as keyof Properties) === "assignee" ? (
|
</div>
|
||||||
<td className="px-3 py-4 text-sm font-medium text-gray-900 relative">
|
) : (
|
||||||
<Listbox
|
<p>
|
||||||
as="div"
|
{person.member.first_name.charAt(0)}
|
||||||
value={issue.assignees}
|
</p>
|
||||||
onChange={(data: any) => {
|
)}
|
||||||
const newData = issue.assignees ?? [];
|
<p>{person.member.first_name}</p>
|
||||||
if (newData.includes(data)) {
|
</div>
|
||||||
newData.splice(newData.indexOf(data), 1);
|
</Listbox.Option>
|
||||||
} else {
|
))}
|
||||||
newData.push(data);
|
</Listbox.Options>
|
||||||
}
|
</Transition>
|
||||||
partialUpdateIssue(
|
</div>
|
||||||
{ assignees_list: newData },
|
</>
|
||||||
issue.id
|
)}
|
||||||
);
|
</Listbox>
|
||||||
}}
|
</td>
|
||||||
className="flex-shrink-0"
|
) : (key as keyof Properties) === "state" ? (
|
||||||
>
|
<td className="px-3 py-4 text-sm font-medium text-gray-900 relative">
|
||||||
{({ open }) => (
|
<Listbox
|
||||||
<>
|
as="div"
|
||||||
<div>
|
value={issue.state}
|
||||||
<Listbox.Button className="rounded-full bg-gray-50 px-5 py-1 text-xs text-gray-500 hover:bg-gray-100 border">
|
onChange={(data: string) => {
|
||||||
{() => {
|
partialUpdateIssue({ state: data }, issue.id);
|
||||||
if (assignees.length > 0)
|
}}
|
||||||
return (
|
className="flex-shrink-0"
|
||||||
<>
|
>
|
||||||
{assignees.map((assignee, index) => (
|
{({ open }) => (
|
||||||
<div
|
<>
|
||||||
key={index}
|
<div>
|
||||||
className={
|
<Listbox.Button
|
||||||
"hidden truncate sm:block text-left"
|
className="inline-flex items-center whitespace-nowrap rounded-full px-2 py-1 text-xs font-medium text-gray-500 hover:bg-gray-100 border"
|
||||||
}
|
style={{
|
||||||
>
|
border: `2px solid ${issue.state_detail.color}`,
|
||||||
{assignee}
|
backgroundColor: `${issue.state_detail.color}20`,
|
||||||
</div>
|
}}
|
||||||
))}
|
>
|
||||||
</>
|
<span
|
||||||
);
|
className={classNames(
|
||||||
else return <span>None</span>;
|
issue.state ? "" : "text-gray-900",
|
||||||
}}
|
"hidden capitalize sm:block w-16"
|
||||||
</Listbox.Button>
|
)}
|
||||||
|
>
|
||||||
|
{addSpaceIfCamelCase(issue.state_detail.name)}
|
||||||
|
</span>
|
||||||
|
</Listbox.Button>
|
||||||
|
|
||||||
<Transition
|
<Transition
|
||||||
show={open}
|
show={open}
|
||||||
as={React.Fragment}
|
as={React.Fragment}
|
||||||
leave="transition ease-in duration-100"
|
leave="transition ease-in duration-100"
|
||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<Listbox.Options className="absolute z-10 mt-1 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
|
<Listbox.Options className="absolute z-10 mt-1 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
|
||||||
{people?.map((person) => (
|
{states?.map((state) => (
|
||||||
<Listbox.Option
|
<Listbox.Option
|
||||||
key={person.id}
|
key={state.id}
|
||||||
className={({ active }) =>
|
className={({ active }) =>
|
||||||
classNames(
|
classNames(
|
||||||
active ? "bg-indigo-50" : "bg-white",
|
active ? "bg-indigo-50" : "bg-white",
|
||||||
"cursor-pointer select-none px-3 py-2"
|
"cursor-pointer select-none px-3 py-2"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
value={person.member.id}
|
value={state.id}
|
||||||
>
|
>
|
||||||
<div
|
{addSpaceIfCamelCase(state.name)}
|
||||||
className={`flex items-center gap-x-1 ${
|
</Listbox.Option>
|
||||||
assignees.includes(
|
))}
|
||||||
person.member.first_name
|
</Listbox.Options>
|
||||||
)
|
</Transition>
|
||||||
? "font-medium"
|
</div>
|
||||||
: "font-normal"
|
</>
|
||||||
}`}
|
)}
|
||||||
>
|
</Listbox>
|
||||||
{person.member.avatar &&
|
</td>
|
||||||
person.member.avatar !== "" ? (
|
) : (key as keyof Properties) === "target_date" ? (
|
||||||
<div className="relative w-4 h-4">
|
<td className="px-3 py-4 text-sm font-medium text-gray-900 whitespace-nowrap">
|
||||||
<Image
|
{issue.target_date
|
||||||
src={person.member.avatar}
|
? renderShortNumericDateFormat(issue.target_date)
|
||||||
alt="avatar"
|
: "-"}
|
||||||
className="rounded-full"
|
</td>
|
||||||
layout="fill"
|
) : (
|
||||||
objectFit="cover"
|
<td className="px-3 py-4 text-sm font-medium text-gray-900 relative capitalize">
|
||||||
/>
|
{issue[key as keyof IIssue] ??
|
||||||
</div>
|
(issue[key as keyof IIssue] as any)?.name ??
|
||||||
) : (
|
"None"}
|
||||||
<p>
|
</td>
|
||||||
{person.member.first_name.charAt(0)}
|
)}
|
||||||
</p>
|
</React.Fragment>
|
||||||
)}
|
)
|
||||||
<p>{person.member.first_name}</p>
|
)}
|
||||||
</div>
|
<td className="px-3">
|
||||||
</Listbox.Option>
|
<div className="flex justify-end items-center gap-2">
|
||||||
))}
|
<button
|
||||||
</Listbox.Options>
|
type="button"
|
||||||
</Transition>
|
className="flex items-center bg-blue-100 text-blue-600 hover:bg-blue-200 duration-300 font-medium px-2 py-1 rounded-md text-sm outline-none"
|
||||||
</div>
|
onClick={() => {
|
||||||
</>
|
setSelectedIssue({
|
||||||
)}
|
...issue,
|
||||||
</Listbox>
|
actionType: "edit",
|
||||||
</td>
|
});
|
||||||
) : (key as keyof Properties) === "state" ? (
|
}}
|
||||||
<td className="px-3 py-4 text-sm font-medium text-gray-900 relative">
|
>
|
||||||
<Listbox
|
<PencilIcon className="h-3 w-3" />
|
||||||
as="div"
|
</button>
|
||||||
value={issue.state}
|
<button
|
||||||
onChange={(data: string) => {
|
type="button"
|
||||||
partialUpdateIssue({ state: data }, issue.id);
|
className="flex items-center bg-red-100 text-red-600 hover:bg-red-200 duration-300 font-medium px-2 py-1 rounded-md text-sm outline-none"
|
||||||
}}
|
onClick={() => {
|
||||||
className="flex-shrink-0"
|
handleDeleteIssue(issue.id);
|
||||||
>
|
}}
|
||||||
{({ open }) => (
|
>
|
||||||
<>
|
<TrashIcon className="h-3 w-3" />
|
||||||
<div>
|
</button>
|
||||||
<Listbox.Button
|
</div>
|
||||||
className="inline-flex items-center whitespace-nowrap rounded-full px-2 py-1 text-xs font-medium text-gray-500 hover:bg-gray-100 border"
|
</td>
|
||||||
style={{
|
</tr>
|
||||||
border: `2px solid ${issue.state_detail.color}`,
|
);
|
||||||
backgroundColor: `${issue.state_detail.color}20`,
|
})
|
||||||
}}
|
: null}
|
||||||
>
|
</tbody>
|
||||||
<span
|
</table>
|
||||||
className={classNames(
|
</div>
|
||||||
issue.state ? "" : "text-gray-900",
|
|
||||||
"hidden capitalize sm:block w-16"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{addSpaceIfCamelCase(issue.state_detail.name)}
|
|
||||||
</span>
|
|
||||||
</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 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
|
|
||||||
{states?.map((state) => (
|
|
||||||
<Listbox.Option
|
|
||||||
key={state.id}
|
|
||||||
className={({ active }) =>
|
|
||||||
classNames(
|
|
||||||
active ? "bg-indigo-50" : "bg-white",
|
|
||||||
"cursor-pointer select-none px-3 py-2"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
value={state.id}
|
|
||||||
>
|
|
||||||
{addSpaceIfCamelCase(state.name)}
|
|
||||||
</Listbox.Option>
|
|
||||||
))}
|
|
||||||
</Listbox.Options>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Listbox>
|
|
||||||
</td>
|
|
||||||
) : (key as keyof Properties) === "target_date" ? (
|
|
||||||
<td className="px-3 py-4 text-sm font-medium text-gray-900 whitespace-nowrap">
|
|
||||||
{issue.target_date
|
|
||||||
? renderShortNumericDateFormat(issue.target_date)
|
|
||||||
: "-"}
|
|
||||||
</td>
|
|
||||||
) : (
|
|
||||||
<td className="px-3 py-4 text-sm font-medium text-gray-900 relative capitalize">
|
|
||||||
{issue[key as keyof IIssue] ??
|
|
||||||
(issue[key as keyof IIssue] as any)?.name ??
|
|
||||||
"None"}
|
|
||||||
</td>
|
|
||||||
)}
|
|
||||||
</React.Fragment>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
<td className="px-3">
|
|
||||||
<div className="flex justify-end items-center gap-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="flex items-center bg-blue-100 text-blue-600 hover:bg-blue-200 duration-300 font-medium px-2 py-1 rounded-md text-sm outline-none"
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedIssue({
|
|
||||||
...issue,
|
|
||||||
actionType: "edit",
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<PencilIcon className="h-3 w-3" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="flex items-center bg-red-100 text-red-600 hover:bg-red-200 duration-300 font-medium px-2 py-1 rounded-md text-sm outline-none"
|
|
||||||
onClick={() => {
|
|
||||||
handleDeleteIssue(issue.id);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TrashIcon className="h-3 w-3" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
: null}
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ListView;
|
export default ListView;
|
||||||
|
@ -1,138 +0,0 @@
|
|||||||
// next
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
// react
|
|
||||||
import { Fragment } from "react";
|
|
||||||
// headless ui
|
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
|
||||||
// hooks
|
|
||||||
import useUser from "lib/hooks/useUser";
|
|
||||||
// services
|
|
||||||
import issuesServices from "lib/services/issues.services";
|
|
||||||
import projectService from "lib/services/project.service";
|
|
||||||
// swr
|
|
||||||
import useSWR from "swr";
|
|
||||||
// types
|
|
||||||
import { IIssue, ProjectMember } from "types";
|
|
||||||
// constants
|
|
||||||
import { PROJECT_ISSUES_DETAILS, PROJECT_MEMBERS } from "constants/fetch-keys";
|
|
||||||
import { Button } from "ui";
|
|
||||||
import { ChartBarIcon, Squares2X2Icon, TagIcon, UserIcon } from "@heroicons/react/24/outline";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
isOpen: boolean;
|
|
||||||
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
issueId: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const IssuePreviewModal = ({ isOpen, setIsOpen, issueId }: Props) => {
|
|
||||||
const closeModal = () => {
|
|
||||||
setIsOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const { activeWorkspace, activeProject } = useUser();
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const { data: issueDetails } = useSWR<IIssue | null>(
|
|
||||||
activeWorkspace && activeProject && issueId ? PROJECT_ISSUES_DETAILS(issueId) : null,
|
|
||||||
activeWorkspace && activeProject && issueId
|
|
||||||
? () => issuesServices.getIssue(activeWorkspace.slug, activeProject.id, issueId)
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: users } = useSWR<ProjectMember[] | null>(
|
|
||||||
activeWorkspace && activeProject ? PROJECT_MEMBERS(activeProject.id) : null,
|
|
||||||
activeWorkspace && activeProject
|
|
||||||
? () => projectService.projectMembers(activeWorkspace.slug, activeProject.id)
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Transition.Root appear show={isOpen} as={Fragment}>
|
|
||||||
<Dialog as="div" className="relative z-10" onClose={closeModal}>
|
|
||||||
<Transition.Child
|
|
||||||
as={Fragment}
|
|
||||||
enter="ease-out duration-300"
|
|
||||||
enterFrom="opacity-0"
|
|
||||||
enterTo="opacity-100"
|
|
||||||
leave="ease-in duration-200"
|
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0"
|
|
||||||
>
|
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-25" />
|
|
||||||
</Transition.Child>
|
|
||||||
|
|
||||||
<div className="fixed inset-0 overflow-y-auto">
|
|
||||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
|
||||||
<Transition.Child
|
|
||||||
as={Fragment}
|
|
||||||
enter="ease-out duration-300"
|
|
||||||
enterFrom="opacity-0 scale-95"
|
|
||||||
enterTo="opacity-100 scale-100"
|
|
||||||
leave="ease-in duration-200"
|
|
||||||
leaveFrom="opacity-100 scale-100"
|
|
||||||
leaveTo="opacity-0 scale-95"
|
|
||||||
>
|
|
||||||
<Dialog.Panel className="w-full max-w-3xl transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
|
|
||||||
<Dialog.Title
|
|
||||||
as="h3"
|
|
||||||
className="text-xl flex flex-col gap-1 font-medium leading-6 text-gray-900"
|
|
||||||
>
|
|
||||||
{issueDetails?.project_detail.identifier}-{issueDetails?.sequence_id}{" "}
|
|
||||||
{issueDetails?.name}
|
|
||||||
<span className="text-sm text-gray-500 font-normal">
|
|
||||||
Created by{" "}
|
|
||||||
{users?.find((u) => u.id === issueDetails?.created_by)?.member.first_name}
|
|
||||||
</span>
|
|
||||||
</Dialog.Title>
|
|
||||||
<div className="mt-4">
|
|
||||||
<p className="text-sm text-gray-500">{issueDetails?.description}</p>
|
|
||||||
</div>
|
|
||||||
<div className="mt-4 flex flex-wrap gap-2">
|
|
||||||
<span className="flex items-center gap-1 hover:bg-gray-100 border rounded-md shadow-sm px-2 py-1 sm:text-sm">
|
|
||||||
<Squares2X2Icon className="h-3 w-3" />
|
|
||||||
{issueDetails?.state_detail.name}
|
|
||||||
</span>
|
|
||||||
<span className="flex items-center gap-1 hover:bg-gray-100 border rounded-md shadow-sm px-2 py-1 capitalize sm:text-sm">
|
|
||||||
<ChartBarIcon className="h-3 w-3" />
|
|
||||||
{issueDetails?.priority}
|
|
||||||
</span>
|
|
||||||
<span className="flex items-center gap-1 hover:bg-gray-100 border rounded-md shadow-sm px-2 py-1 capitalize sm:text-sm">
|
|
||||||
<TagIcon className="h-3 w-3" />
|
|
||||||
{issueDetails?.label_details && issueDetails.label_details.length > 0
|
|
||||||
? issueDetails.label_details.map((label) => (
|
|
||||||
<span key={label.id}>{label.name}</span>
|
|
||||||
))
|
|
||||||
: "None"}
|
|
||||||
</span>
|
|
||||||
<span className="flex items-center gap-1 hover:bg-gray-100 border rounded-md shadow-sm px-2 py-1 capitalize sm:text-sm">
|
|
||||||
<UserIcon className="h-3 w-3" />
|
|
||||||
{issueDetails?.assignee_details && issueDetails.assignee_details.length > 0
|
|
||||||
? issueDetails.assignee_details.map((assignee) => (
|
|
||||||
<span key={assignee.id}>{assignee.first_name}</span>
|
|
||||||
))
|
|
||||||
: "None"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="mt-4 flex gap-3 justify-end">
|
|
||||||
<Button
|
|
||||||
onClick={() =>
|
|
||||||
router.push(`/projects/${activeProject?.id}/issues/${issueId}`)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
View in Detail
|
|
||||||
</Button>
|
|
||||||
<Button onClick={closeModal}>Close</Button>
|
|
||||||
</div>
|
|
||||||
</Dialog.Panel>
|
|
||||||
</Transition.Child>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
</Transition.Root>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default IssuePreviewModal;
|
|
@ -4,13 +4,14 @@ import useSWR from "swr";
|
|||||||
// headless ui
|
// headless ui
|
||||||
import { Listbox, Transition } from "@headlessui/react";
|
import { Listbox, Transition } from "@headlessui/react";
|
||||||
// react hook form
|
// react hook form
|
||||||
import { useForm, Controller } from "react-hook-form";
|
import { useForm, Controller, UseFormWatch } from "react-hook-form";
|
||||||
// services
|
// services
|
||||||
import stateServices from "lib/services/state.services";
|
import stateServices from "lib/services/state.service";
|
||||||
import issuesServices from "lib/services/issues.services";
|
import issuesServices from "lib/services/issues.service";
|
||||||
import workspaceService from "lib/services/workspace.service";
|
import workspaceService from "lib/services/workspace.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useUser from "lib/hooks/useUser";
|
import useUser from "lib/hooks/useUser";
|
||||||
|
import useToast from "lib/hooks/useToast";
|
||||||
// fetching keys
|
// fetching keys
|
||||||
import {
|
import {
|
||||||
PROJECT_ISSUES_LIST,
|
PROJECT_ISSUES_LIST,
|
||||||
@ -20,6 +21,7 @@ import {
|
|||||||
} from "constants/fetch-keys";
|
} from "constants/fetch-keys";
|
||||||
// commons
|
// commons
|
||||||
import { classNames, copyTextToClipboard } from "constants/common";
|
import { classNames, copyTextToClipboard } from "constants/common";
|
||||||
|
import { PRIORITIES } from "constants/";
|
||||||
// ui
|
// ui
|
||||||
import { Input, Button, Spinner } from "ui";
|
import { Input, Button, Spinner } from "ui";
|
||||||
import { Popover } from "@headlessui/react";
|
import { Popover } from "@headlessui/react";
|
||||||
@ -38,25 +40,33 @@ import {
|
|||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
// types
|
// types
|
||||||
import type { Control } from "react-hook-form";
|
import type { Control } from "react-hook-form";
|
||||||
import type { IIssue, IIssueLabels, IssueResponse, IState, WorkspaceMember } from "types";
|
import type { IIssue, IIssueLabels, IssueResponse, IState, NestedKeyOf } from "types";
|
||||||
import { TwitterPicker } from "react-color";
|
import { TwitterPicker } from "react-color";
|
||||||
import useToast from "lib/hooks/useToast";
|
import IssuesListModal from "components/project/issues/IssuesListModal";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
control: Control<IIssue, any>;
|
control: Control<IIssue, any>;
|
||||||
submitChanges: (formData: Partial<IIssue>) => void;
|
submitChanges: (formData: Partial<IIssue>) => void;
|
||||||
issueDetail: IIssue | undefined;
|
issueDetail: IIssue | undefined;
|
||||||
|
watch: UseFormWatch<IIssue>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const PRIORITIES = ["high", "medium", "low"];
|
|
||||||
|
|
||||||
const defaultValues: Partial<IIssueLabels> = {
|
const defaultValues: Partial<IIssueLabels> = {
|
||||||
name: "",
|
name: "",
|
||||||
colour: "#ff0000",
|
colour: "#ff0000",
|
||||||
};
|
};
|
||||||
|
|
||||||
const IssueDetailSidebar: React.FC<Props> = ({ control, submitChanges, issueDetail }) => {
|
const IssueDetailSidebar: React.FC<Props> = ({
|
||||||
const { activeWorkspace, activeProject, cycles } = useUser();
|
control,
|
||||||
|
watch: watchIssue,
|
||||||
|
submitChanges,
|
||||||
|
issueDetail,
|
||||||
|
}) => {
|
||||||
|
const [isBlockerModalOpen, setIsBlockerModalOpen] = useState(false);
|
||||||
|
const [isBlockedModalOpen, setIsBlockedModalOpen] = useState(false);
|
||||||
|
const [isParentModalOpen, setIsParentModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const { activeWorkspace, activeProject, cycles, issues } = useUser();
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
@ -67,20 +77,11 @@ const IssueDetailSidebar: React.FC<Props> = ({ control, submitChanges, issueDeta
|
|||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: people } = useSWR<WorkspaceMember[]>(
|
const { data: people } = useSWR(
|
||||||
activeWorkspace ? WORKSPACE_MEMBERS : null,
|
activeWorkspace ? WORKSPACE_MEMBERS(activeWorkspace.slug) : null,
|
||||||
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null
|
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: projectIssues } = useSWR<IssueResponse>(
|
|
||||||
activeProject && activeWorkspace
|
|
||||||
? PROJECT_ISSUES_LIST(activeWorkspace.slug, activeProject.id)
|
|
||||||
: null,
|
|
||||||
activeProject && activeWorkspace
|
|
||||||
? () => issuesServices.getIssues(activeWorkspace.slug, activeProject.id)
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: issueLabels, mutate: issueLabelMutate } = useSWR<IIssueLabels[]>(
|
const { data: issueLabels, mutate: issueLabelMutate } = useSWR<IIssueLabels[]>(
|
||||||
activeProject && activeWorkspace ? PROJECT_ISSUE_LABELS(activeProject.id) : null,
|
activeProject && activeWorkspace ? PROJECT_ISSUE_LABELS(activeProject.id) : null,
|
||||||
activeProject && activeWorkspace
|
activeProject && activeWorkspace
|
||||||
@ -110,7 +111,19 @@ const IssueDetailSidebar: React.FC<Props> = ({ control, submitChanges, issueDeta
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const sidebarSections = [
|
const sidebarSections: Array<
|
||||||
|
Array<{
|
||||||
|
label: string;
|
||||||
|
name: NestedKeyOf<IIssue>;
|
||||||
|
canSelectMultipleOptions: boolean;
|
||||||
|
icon: (props: any) => JSX.Element;
|
||||||
|
options?: Array<{ label: string; value: any }>;
|
||||||
|
modal: boolean;
|
||||||
|
issuesList?: Array<IIssue>;
|
||||||
|
isOpen?: boolean;
|
||||||
|
setIsOpen?: (arg: boolean) => void;
|
||||||
|
}>
|
||||||
|
> = [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
label: "Status",
|
label: "Status",
|
||||||
@ -121,6 +134,7 @@ const IssueDetailSidebar: React.FC<Props> = ({ control, submitChanges, issueDeta
|
|||||||
label: state.name,
|
label: state.name,
|
||||||
value: state.id,
|
value: state.id,
|
||||||
})),
|
})),
|
||||||
|
modal: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Assignees",
|
label: "Assignees",
|
||||||
@ -131,6 +145,7 @@ const IssueDetailSidebar: React.FC<Props> = ({ control, submitChanges, issueDeta
|
|||||||
label: person.member.first_name,
|
label: person.member.first_name,
|
||||||
value: person.member.id,
|
value: person.member.id,
|
||||||
})),
|
})),
|
||||||
|
modal: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Priority",
|
label: "Priority",
|
||||||
@ -141,34 +156,52 @@ const IssueDetailSidebar: React.FC<Props> = ({ control, submitChanges, issueDeta
|
|||||||
label: property,
|
label: property,
|
||||||
value: property,
|
value: property,
|
||||||
})),
|
})),
|
||||||
|
modal: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
label: "Blocker",
|
label: "Parent",
|
||||||
name: "blockers_list",
|
name: "parent",
|
||||||
canSelectMultipleOptions: true,
|
canSelectMultipleOptions: false,
|
||||||
icon: UserIcon,
|
icon: UserIcon,
|
||||||
options: projectIssues?.results?.map((issue) => ({
|
issuesList:
|
||||||
label: issue.name,
|
issues?.results.filter(
|
||||||
value: issue.id,
|
(i) =>
|
||||||
})),
|
i.id !== issueDetail?.id &&
|
||||||
|
i.id !== issueDetail?.parent &&
|
||||||
|
i.parent !== issueDetail?.id
|
||||||
|
) ?? [],
|
||||||
|
modal: true,
|
||||||
|
isOpen: isParentModalOpen,
|
||||||
|
setIsOpen: setIsParentModalOpen,
|
||||||
},
|
},
|
||||||
|
// {
|
||||||
|
// label: "Blocker",
|
||||||
|
// name: "blockers_list",
|
||||||
|
// canSelectMultipleOptions: true,
|
||||||
|
// icon: UserIcon,
|
||||||
|
// issuesList: issues?.results.filter((i) => i.id !== issueDetail?.id) ?? [],
|
||||||
|
// modal: true,
|
||||||
|
// isOpen: isBlockerModalOpen,
|
||||||
|
// setIsOpen: setIsBlockerModalOpen,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// label: "Blocked",
|
||||||
|
// name: "blocked_list",
|
||||||
|
// canSelectMultipleOptions: true,
|
||||||
|
// icon: UserIcon,
|
||||||
|
// issuesList: issues?.results.filter((i) => i.id !== issueDetail?.id) ?? [],
|
||||||
|
// modal: true,
|
||||||
|
// isOpen: isBlockedModalOpen,
|
||||||
|
// setIsOpen: setIsBlockedModalOpen,
|
||||||
|
// },
|
||||||
{
|
{
|
||||||
label: "Blocked",
|
label: "Target Date",
|
||||||
name: "blocked_list",
|
|
||||||
canSelectMultipleOptions: true,
|
|
||||||
icon: UserIcon,
|
|
||||||
options: projectIssues?.results?.map((issue) => ({
|
|
||||||
label: issue.name,
|
|
||||||
value: issue.id,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Due Date",
|
|
||||||
name: "target_date",
|
name: "target_date",
|
||||||
canSelectMultipleOptions: true,
|
canSelectMultipleOptions: true,
|
||||||
icon: CalendarDaysIcon,
|
icon: CalendarDaysIcon,
|
||||||
|
modal: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
@ -181,6 +214,7 @@ const IssueDetailSidebar: React.FC<Props> = ({ control, submitChanges, issueDeta
|
|||||||
label: cycle.name,
|
label: cycle.name,
|
||||||
value: cycle.id,
|
value: cycle.id,
|
||||||
})),
|
})),
|
||||||
|
modal: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
@ -249,12 +283,12 @@ const IssueDetailSidebar: React.FC<Props> = ({ control, submitChanges, issueDeta
|
|||||||
{sidebarSections.map((section, index) => (
|
{sidebarSections.map((section, index) => (
|
||||||
<div key={index} className="py-1">
|
<div key={index} className="py-1">
|
||||||
{section.map((item) => (
|
{section.map((item) => (
|
||||||
<div key={item.label} className="flex justify-between items-center gap-x-2 py-2">
|
<div key={item.label} className="flex items-center py-2 flex-wrap">
|
||||||
<div className="flex items-center gap-x-2 text-sm">
|
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
|
||||||
<item.icon className="h-4 w-4" />
|
<item.icon className="flex-shrink-0 h-4 w-4" />
|
||||||
<p>{item.label}</p>
|
<p>{item.label}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="sm:basis-1/2">
|
||||||
{item.name === "target_date" ? (
|
{item.name === "target_date" ? (
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
@ -262,15 +296,52 @@ const IssueDetailSidebar: React.FC<Props> = ({ control, submitChanges, issueDeta
|
|||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
value={""}
|
value={value ?? ""}
|
||||||
onChange={(e: any) => {
|
onChange={(e: any) => {
|
||||||
submitChanges({ target_date: e.target.value });
|
submitChanges({ target_date: e.target.value });
|
||||||
onChange(e.target.value);
|
onChange(e.target.value);
|
||||||
}}
|
}}
|
||||||
className="hover:bg-gray-100 border rounded-md shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300"
|
className="hover:bg-gray-100 border rounded-md shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 w-full"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
) : item.modal ? (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={item.name as keyof IIssue}
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<>
|
||||||
|
<IssuesListModal
|
||||||
|
isOpen={Boolean(item?.isOpen)}
|
||||||
|
handleClose={() => item.setIsOpen && item.setIsOpen(false)}
|
||||||
|
onChange={(val) => {
|
||||||
|
console.log(val);
|
||||||
|
// submitChanges({ [item.name]: val });
|
||||||
|
onChange(val);
|
||||||
|
}}
|
||||||
|
issues={item?.issuesList ?? []}
|
||||||
|
title={`Select ${item.label}`}
|
||||||
|
multiple={item.canSelectMultipleOptions}
|
||||||
|
value={value}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex justify-between items-center gap-1 hover:bg-gray-100 border rounded-md shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 w-full"
|
||||||
|
onClick={() => item.setIsOpen && item.setIsOpen(true)}
|
||||||
|
>
|
||||||
|
{watchIssue(`${item.name as keyof IIssue}`) &&
|
||||||
|
watchIssue(`${item.name as keyof IIssue}`) !== ""
|
||||||
|
? `${activeProject?.identifier}-
|
||||||
|
${
|
||||||
|
issues?.results.find(
|
||||||
|
(i) => i.id === watchIssue(`${item.name as keyof IIssue}`)
|
||||||
|
)?.sequence_id
|
||||||
|
}`
|
||||||
|
: `Select ${item.label}`}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
@ -288,11 +359,11 @@ const IssueDetailSidebar: React.FC<Props> = ({ control, submitChanges, issueDeta
|
|||||||
>
|
>
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Listbox.Button className="relative flex justify-between items-center gap-1 hover:bg-gray-100 border rounded-md shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-sm duration-300">
|
<Listbox.Button className="flex justify-between items-center gap-1 hover:bg-gray-100 border rounded-md shadow-sm px-2 w-full py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300">
|
||||||
<span
|
<span
|
||||||
className={classNames(
|
className={classNames(
|
||||||
value ? "" : "text-gray-900",
|
value ? "" : "text-gray-900",
|
||||||
"hidden truncate sm:block w-16 text-left",
|
"hidden truncate sm:block text-left",
|
||||||
item.label === "Priority" ? "capitalize" : ""
|
item.label === "Priority" ? "capitalize" : ""
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@ -419,12 +490,12 @@ const IssueDetailSidebar: React.FC<Props> = ({ control, submitChanges, issueDeta
|
|||||||
+
|
+
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
<div className="flex justify-between items-center gap-x-2">
|
<div className="flex justify-between items-center">
|
||||||
<div className="flex items-center gap-x-2 text-sm">
|
<div className="flex items-center gap-x-2 text-sm basis-1/2">
|
||||||
<TagIcon className="w-4 h-4" />
|
<TagIcon className="w-4 h-4" />
|
||||||
<p>Label</p>
|
<p>Label</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="basis-1/2">
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="labels_list"
|
name="labels_list"
|
||||||
@ -440,11 +511,11 @@ const IssueDetailSidebar: React.FC<Props> = ({ control, submitChanges, issueDeta
|
|||||||
<>
|
<>
|
||||||
<Listbox.Label className="sr-only">Label</Listbox.Label>
|
<Listbox.Label className="sr-only">Label</Listbox.Label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Listbox.Button className="relative flex justify-between items-center gap-1 hover:bg-gray-100 border rounded-md shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-sm duration-300">
|
<Listbox.Button className="flex justify-between items-center gap-1 hover:bg-gray-100 border rounded-md shadow-sm px-2 w-full py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300">
|
||||||
<span
|
<span
|
||||||
className={classNames(
|
className={classNames(
|
||||||
value ? "" : "text-gray-900",
|
value ? "" : "text-gray-900",
|
||||||
"hidden truncate capitalize sm:block w-16 text-left"
|
"hidden truncate capitalize sm:block text-left"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{value && value.length > 0
|
{value && value.length > 0
|
||||||
|
@ -64,14 +64,14 @@ const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentDeletion
|
|||||||
<div key={comment.id}>
|
<div key={comment.id}>
|
||||||
<div className="w-full h-full flex justify-between">
|
<div className="w-full h-full flex justify-between">
|
||||||
<div className="flex gap-x-2 w-full">
|
<div className="flex gap-x-2 w-full">
|
||||||
<div className="flex-shrink-0 -ml-1.5">
|
<div className="flex-shrink-0">
|
||||||
{comment.actor_detail.avatar && comment.actor_detail.avatar !== "" ? (
|
{comment.actor_detail.avatar && comment.actor_detail.avatar !== "" ? (
|
||||||
<Image
|
<Image
|
||||||
src={comment.actor_detail.avatar}
|
src={comment.actor_detail.avatar}
|
||||||
alt={comment.actor_detail.name}
|
alt={comment.actor_detail.name}
|
||||||
height={30}
|
height={30}
|
||||||
width={30}
|
width={30}
|
||||||
className="rounded-full"
|
className="rounded"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
@ -82,11 +82,7 @@ const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentDeletion
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<p>
|
<div>
|
||||||
{comment.actor_detail.first_name} {comment.actor_detail.last_name}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-gray-500">{timeAgo(comment.created_at)}</p>
|
|
||||||
<div className="w-full mt-2">
|
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<form className="flex flex-col gap-2" onSubmit={handleSubmit(onEnter)}>
|
<form className="flex flex-col gap-2" onSubmit={handleSubmit(onEnter)}>
|
||||||
<TextArea
|
<TextArea
|
||||||
@ -120,13 +116,19 @@ const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentDeletion
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{comment.comment.split("\n").map((item, index) => (
|
{comment.comment.split("\n").map((item, index) => (
|
||||||
<p key={index} className="text-sm text-gray-600">
|
<p key={index} className="text-sm">
|
||||||
{item}
|
{item}
|
||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<p className="text-xs text-gray-500 flex items-center gap-2 mt-1">
|
||||||
|
<span>
|
||||||
|
{comment.actor_detail.first_name} {comment.actor_detail.last_name}
|
||||||
|
</span>
|
||||||
|
<span>{timeAgo(comment.created_at)}</span>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{user?.id === comment.actor && (
|
{user?.id === comment.actor && (
|
||||||
|
@ -4,7 +4,7 @@ import { mutate } from "swr";
|
|||||||
// react hook form
|
// react hook form
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
// services
|
// services
|
||||||
import issuesServices from "lib/services/issues.services";
|
import issuesServices from "lib/services/issues.service";
|
||||||
// fetch keys
|
// fetch keys
|
||||||
import { PROJECT_ISSUES_COMMENTS } from "constants/fetch-keys";
|
import { PROJECT_ISSUES_COMMENTS } from "constants/fetch-keys";
|
||||||
// components
|
// components
|
||||||
@ -72,7 +72,7 @@ const IssueCommentSection: React.FC<Props> = ({ comments, issueId, projectId, wo
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<div className="p-2 bg-indigo-50 rounded-md">
|
<div className="bg-gray-100 rounded-md">
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<TextArea
|
<TextArea
|
||||||
id="comment"
|
id="comment"
|
||||||
|
@ -8,7 +8,7 @@ import useUser from "lib/hooks/useUser";
|
|||||||
import { addSpaceIfCamelCase, classNames } from "constants/common";
|
import { addSpaceIfCamelCase, classNames } from "constants/common";
|
||||||
import { STATE_LIST } from "constants/fetch-keys";
|
import { STATE_LIST } from "constants/fetch-keys";
|
||||||
// services
|
// services
|
||||||
import stateServices from "lib/services/state.services";
|
import stateServices from "lib/services/state.service";
|
||||||
// ui
|
// ui
|
||||||
import { Listbox, Transition } from "@headlessui/react";
|
import { Listbox, Transition } from "@headlessui/react";
|
||||||
// types
|
// types
|
||||||
|
@ -3,10 +3,14 @@ import React, { useState } from "react";
|
|||||||
// next
|
// next
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import _ from "lodash";
|
// hooks
|
||||||
import useUser from "lib/hooks/useUser";
|
import useUser from "lib/hooks/useUser";
|
||||||
// Services
|
// Services
|
||||||
import projectService from "lib/services/project.service";
|
import projectService from "lib/services/project.service";
|
||||||
|
// fetch keys
|
||||||
|
import { PROJECT_MEMBERS } from "constants/fetch-keys";
|
||||||
|
// commons
|
||||||
|
import { renderShortNumericDateFormat } from "constants/common";
|
||||||
// icons
|
// icons
|
||||||
import {
|
import {
|
||||||
CalendarDaysIcon,
|
CalendarDaysIcon,
|
||||||
@ -17,25 +21,41 @@ import {
|
|||||||
PlusIcon,
|
PlusIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
import { renderShortNumericDateFormat } from "constants/common";
|
// types
|
||||||
|
import type { IProject } from "types";
|
||||||
|
type Props = {
|
||||||
|
project: IProject;
|
||||||
|
slug: string;
|
||||||
|
invitationsRespond: string[];
|
||||||
|
handleInvitation: (project_invitation: any, action: "accepted" | "withdraw") => void;
|
||||||
|
setDeleteProject: (id: string | null) => void;
|
||||||
|
};
|
||||||
|
|
||||||
const ProjectMemberInvitations = ({
|
const ProjectMemberInvitations: React.FC<Props> = ({
|
||||||
project,
|
project,
|
||||||
slug,
|
slug,
|
||||||
invitationsRespond,
|
invitationsRespond,
|
||||||
handleInvitation,
|
handleInvitation,
|
||||||
setDeleteProject,
|
setDeleteProject,
|
||||||
}: any) => {
|
}) => {
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const { data: members } = useSWR("PROJECT_MEMBERS", () =>
|
|
||||||
|
const { data: members } = useSWR<any[]>(PROJECT_MEMBERS(project.id), () =>
|
||||||
projectService.projectMembers(slug, project.id)
|
projectService.projectMembers(slug, project.id)
|
||||||
);
|
);
|
||||||
|
|
||||||
const isMember =
|
const isMember = members?.some((item: any) => item.member.id === (user as any)?.id);
|
||||||
_.filter(members, (item: any) => item.member.id === (user as any).id).length === 1;
|
|
||||||
|
|
||||||
const [selected, setSelected] = useState<any>(false);
|
const [selected, setSelected] = useState<any>(false);
|
||||||
|
|
||||||
|
if (!members) {
|
||||||
|
return (
|
||||||
|
<div className="w-full h-36 flex flex-col px-4 py-3 rounded-md bg-white">
|
||||||
|
<div className="w-full h-full bg-gray-50 animate-pulse" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
@ -80,7 +100,7 @@ const ProjectMemberInvitations = ({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-200 duration-300 outline-none"
|
className="h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-200 duration-300 outline-none"
|
||||||
onClick={() => setDeleteProject(project)}
|
onClick={() => setDeleteProject(project.id)}
|
||||||
>
|
>
|
||||||
<TrashIcon className="h-4 w-4 text-red-500" />
|
<TrashIcon className="h-4 w-4 text-red-500" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -15,7 +15,7 @@ import { Button } from "ui";
|
|||||||
// icons
|
// icons
|
||||||
import { CheckIcon, ChevronDownIcon } from "@heroicons/react/24/outline";
|
import { CheckIcon, ChevronDownIcon } from "@heroicons/react/24/outline";
|
||||||
// types
|
// types
|
||||||
import { IProject, WorkspaceMember } from "types";
|
import { IProject } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
import { WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
||||||
|
|
||||||
@ -27,7 +27,7 @@ type Props = {
|
|||||||
const ControlSettings: React.FC<Props> = ({ control, isSubmitting }) => {
|
const ControlSettings: React.FC<Props> = ({ control, isSubmitting }) => {
|
||||||
const { activeWorkspace } = useUser();
|
const { activeWorkspace } = useUser();
|
||||||
|
|
||||||
const { data: people } = useSWR<WorkspaceMember[]>(
|
const { data: people } = useSWR(
|
||||||
activeWorkspace ? WORKSPACE_MEMBERS : null,
|
activeWorkspace ? WORKSPACE_MEMBERS : null,
|
||||||
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null
|
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null
|
||||||
);
|
);
|
||||||
|
@ -7,7 +7,7 @@ import projectServices from "lib/services/project.service";
|
|||||||
// hooks
|
// hooks
|
||||||
import useUser from "lib/hooks/useUser";
|
import useUser from "lib/hooks/useUser";
|
||||||
// ui
|
// ui
|
||||||
import { Input, Select, TextArea } from "ui";
|
import { Button, Input, Select, TextArea } from "ui";
|
||||||
// types
|
// types
|
||||||
import { IProject } from "types";
|
import { IProject } from "types";
|
||||||
// constants
|
// constants
|
||||||
@ -17,11 +17,12 @@ type Props = {
|
|||||||
register: UseFormRegister<IProject>;
|
register: UseFormRegister<IProject>;
|
||||||
errors: any;
|
errors: any;
|
||||||
setError: UseFormSetError<IProject>;
|
setError: UseFormSetError<IProject>;
|
||||||
|
isSubmitting: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const NETWORK_CHOICES = { "0": "Secret", "2": "Public" };
|
const NETWORK_CHOICES = { "0": "Secret", "2": "Public" };
|
||||||
|
|
||||||
const GeneralSettings: React.FC<Props> = ({ register, errors, setError }) => {
|
const GeneralSettings: React.FC<Props> = ({ register, errors, setError, isSubmitting }) => {
|
||||||
const { activeWorkspace } = useUser();
|
const { activeWorkspace } = useUser();
|
||||||
|
|
||||||
const checkIdentifier = (slug: string, value: string) => {
|
const checkIdentifier = (slug: string, value: string) => {
|
||||||
@ -111,6 +112,11 @@ const GeneralSettings: React.FC<Props> = ({ register, errors, setError }) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button type="submit" disabled={isSubmitting}>
|
||||||
|
{isSubmitting ? "Updating Project..." : "Update Project"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -7,7 +7,7 @@ import { Controller, SubmitHandler, useForm } from "react-hook-form";
|
|||||||
// react-color
|
// react-color
|
||||||
import { TwitterPicker } from "react-color";
|
import { TwitterPicker } from "react-color";
|
||||||
// services
|
// services
|
||||||
import issuesServices from "lib/services/issues.services";
|
import issuesServices from "lib/services/issues.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useUser from "lib/hooks/useUser";
|
import useUser from "lib/hooks/useUser";
|
||||||
// headless ui
|
// headless ui
|
||||||
@ -47,6 +47,7 @@ const LabelsSettings: React.FC = () => {
|
|||||||
setValue,
|
setValue,
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting },
|
||||||
watch,
|
watch,
|
||||||
|
setError,
|
||||||
} = useForm<IIssueLabels>({ defaultValues });
|
} = useForm<IIssueLabels>({ defaultValues });
|
||||||
|
|
||||||
const { data: issueLabels, mutate } = useSWR<IIssueLabels[]>(
|
const { data: issueLabels, mutate } = useSWR<IIssueLabels[]>(
|
||||||
@ -163,23 +164,33 @@ const LabelsSettings: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<div className="w-full flex flex-col justify-center">
|
||||||
type="text"
|
<Input
|
||||||
id="labelName"
|
type="text"
|
||||||
name="name"
|
id="labelName"
|
||||||
register={register}
|
name="name"
|
||||||
placeholder="Lable title"
|
register={register}
|
||||||
/>
|
placeholder="Lable title"
|
||||||
|
validations={{
|
||||||
|
required: "Label title is required",
|
||||||
|
}}
|
||||||
|
error={errors.name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<Button type="button" theme="secondary" onClick={() => setNewLabelForm(false)}>
|
<Button type="button" theme="secondary" onClick={() => setNewLabelForm(false)}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
{isUpdating ? (
|
{isUpdating ? (
|
||||||
<Button type="button" onClick={handleSubmit(handleLabelUpdate)}>
|
<Button
|
||||||
Update
|
type="button"
|
||||||
|
onClick={handleSubmit(handleLabelUpdate)}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{isSubmitting ? "Updating" : "Update"}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button type="button" onClick={handleSubmit(handleNewLabel)}>
|
<Button type="button" onClick={handleSubmit(handleNewLabel)} disabled={isSubmitting}>
|
||||||
Add
|
{isSubmitting ? "Adding" : "Add"}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
@ -193,7 +204,7 @@ const LabelsSettings: React.FC = () => {
|
|||||||
<div className="bg-white p-2 flex items-center justify-between text-gray-900 rounded-md">
|
<div className="bg-white p-2 flex items-center justify-between text-gray-900 rounded-md">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span
|
<span
|
||||||
className={`h-1.5 w-1.5 rounded-full`}
|
className="flex-shrink-0 h-1.5 w-1.5 rounded-full"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: label.colour,
|
backgroundColor: label.colour,
|
||||||
}}
|
}}
|
||||||
|
@ -14,7 +14,7 @@ import { Button, Input, TextArea, Select } from "ui";
|
|||||||
// hooks
|
// hooks
|
||||||
import useToast from "lib/hooks/useToast";
|
import useToast from "lib/hooks/useToast";
|
||||||
// types
|
// types
|
||||||
import { WorkspaceMember } from "types";
|
import { IWorkspaceMemberInvitation } from "types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -30,7 +30,7 @@ const ROLE = {
|
|||||||
20: "Admin",
|
20: "Admin",
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultValues: Partial<WorkspaceMember> = {
|
const defaultValues: Partial<IWorkspaceMemberInvitation> = {
|
||||||
email: "",
|
email: "",
|
||||||
role: 5,
|
role: 5,
|
||||||
message: "",
|
message: "",
|
||||||
@ -57,7 +57,7 @@ const SendWorkspaceInvitationModal: React.FC<Props> = ({
|
|||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting },
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
reset,
|
||||||
} = useForm<WorkspaceMember>({
|
} = useForm<IWorkspaceMemberInvitation>({
|
||||||
defaultValues,
|
defaultValues,
|
||||||
reValidateMode: "onChange",
|
reValidateMode: "onChange",
|
||||||
mode: "all",
|
mode: "all",
|
||||||
|
@ -3,10 +3,10 @@ import Image from "next/image";
|
|||||||
// react
|
// react
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
// types
|
// types
|
||||||
import { IWorkspaceInvitation } from "types";
|
import { IWorkspaceMemberInvitation } from "types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
invitation: IWorkspaceInvitation;
|
invitation: IWorkspaceMemberInvitation;
|
||||||
invitationsRespond: string[];
|
invitationsRespond: string[];
|
||||||
handleInvitation: any;
|
handleInvitation: any;
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useRef, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
// next
|
// next
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
// headless ui
|
// headless ui
|
||||||
@ -11,43 +11,54 @@ import useToast from "lib/hooks/useToast";
|
|||||||
// icons
|
// icons
|
||||||
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
||||||
// ui
|
// ui
|
||||||
import { Button } from "ui";
|
import { Button, Input } from "ui";
|
||||||
// types
|
// types
|
||||||
import type { IWorkspace } from "types";
|
import type { IWorkspace } from "types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
data: IWorkspace | null;
|
||||||
|
onClose: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ConfirmWorkspaceDeletion: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
const ConfirmWorkspaceDeletion: React.FC<Props> = ({ isOpen, data, onClose }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
const { activeWorkspace, mutateWorkspaces } = useUser();
|
const [selectedWorkspace, setSelectedWorkspace] = useState<IWorkspace | null>(null);
|
||||||
|
|
||||||
|
const [confirmProjectName, setConfirmProjectName] = useState("");
|
||||||
|
const [confirmDeleteMyProject, setConfirmDeleteMyProject] = useState(false);
|
||||||
|
|
||||||
|
const canDelete = confirmProjectName === data?.name && confirmDeleteMyProject;
|
||||||
|
|
||||||
|
const { mutateWorkspaces } = useUser();
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const cancelButtonRef = useRef(null);
|
const cancelButtonRef = useRef(null);
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setIsOpen(false);
|
onClose();
|
||||||
setIsDeleteLoading(false);
|
setIsDeleteLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeletion = async () => {
|
const handleDeletion = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
if (!activeWorkspace) return;
|
if (!data || !canDelete) return;
|
||||||
await workspaceService
|
await workspaceService
|
||||||
.deleteWorkspace(activeWorkspace.slug)
|
.deleteWorkspace(data.slug)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
handleClose();
|
handleClose();
|
||||||
mutateWorkspaces((prevData) => {
|
mutateWorkspaces((prevData) => {
|
||||||
return (prevData ?? []).filter(
|
return (prevData ?? []).filter((workspace: IWorkspace) => workspace.slug !== data.slug);
|
||||||
(workspace: IWorkspace) => workspace.slug !== activeWorkspace.slug
|
|
||||||
);
|
|
||||||
}, false);
|
}, false);
|
||||||
|
setToastAlert({
|
||||||
|
type: "success",
|
||||||
|
message: "Workspace deleted successfully",
|
||||||
|
title: "Success",
|
||||||
|
});
|
||||||
router.push("/");
|
router.push("/");
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -56,6 +67,16 @@ const ConfirmWorkspaceDeletion: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) setSelectedWorkspace(data);
|
||||||
|
else {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setSelectedWorkspace(null);
|
||||||
|
clearTimeout(timer);
|
||||||
|
}, 350);
|
||||||
|
}
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition.Root show={isOpen} as={React.Fragment}>
|
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||||
<Dialog
|
<Dialog
|
||||||
@ -103,11 +124,47 @@ const ConfirmWorkspaceDeletion: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
|||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
Are you sure you want to delete workspace - {`"`}
|
Are you sure you want to delete workspace - {`"`}
|
||||||
<span className="italic">{activeWorkspace?.name}</span>
|
<span className="italic">{data?.name}</span>
|
||||||
{`"`} ? All of the data related to the workspace will be permanently
|
{`"`} ? All of the data related to the workspace will be permanently
|
||||||
removed. This action cannot be undone.
|
removed. This action cannot be undone.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mt-3">
|
||||||
|
<p className="text-sm">
|
||||||
|
Enter the workspace name{" "}
|
||||||
|
<span className="font-semibold">{selectedWorkspace?.name}</span> to
|
||||||
|
continue:
|
||||||
|
</p>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Project name"
|
||||||
|
className="mt-2"
|
||||||
|
value={confirmProjectName}
|
||||||
|
onChange={(e) => {
|
||||||
|
setConfirmProjectName(e.target.value);
|
||||||
|
}}
|
||||||
|
name="workspaceName"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3">
|
||||||
|
<p className="text-sm">
|
||||||
|
To confirm, type{" "}
|
||||||
|
<span className="font-semibold">delete my workspace</span> below:
|
||||||
|
</p>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter 'delete my workspace'"
|
||||||
|
className="mt-2"
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e.target.value === "delete my workspace") {
|
||||||
|
setConfirmDeleteMyProject(true);
|
||||||
|
} else {
|
||||||
|
setConfirmDeleteMyProject(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
name="typeDelete"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -116,7 +173,7 @@ const ConfirmWorkspaceDeletion: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={handleDeletion}
|
onClick={handleDeletion}
|
||||||
theme="danger"
|
theme="danger"
|
||||||
disabled={isDeleteLoading}
|
disabled={isDeleteLoading || !canDelete}
|
||||||
className="inline-flex sm:ml-3"
|
className="inline-flex sm:ml-3"
|
||||||
>
|
>
|
||||||
{isDeleteLoading ? "Deleting..." : "Delete"}
|
{isDeleteLoading ? "Deleting..." : "Delete"}
|
@ -15,7 +15,8 @@ export const MAGIC_LINK_SIGNIN = "/api/magic-sign-in/";
|
|||||||
export const USER_ENDPOINT = "/api/users/me/";
|
export const USER_ENDPOINT = "/api/users/me/";
|
||||||
export const CHANGE_PASSWORD = "/api/users/me/change-password/";
|
export const CHANGE_PASSWORD = "/api/users/me/change-password/";
|
||||||
export const USER_ONBOARD_ENDPOINT = "/api/users/me/onboard/";
|
export const USER_ONBOARD_ENDPOINT = "/api/users/me/onboard/";
|
||||||
export const USER_ISSUES_ENDPOINT = "/api/users/me/issues/";
|
export const USER_ISSUES_ENDPOINT = (workspaceSlug: string) =>
|
||||||
|
`/api/workspaces/${workspaceSlug}/my-issues/`;
|
||||||
export const USER_WORKSPACES = "/api/users/me/workspaces";
|
export const USER_WORKSPACES = "/api/users/me/workspaces";
|
||||||
|
|
||||||
// s3 file url
|
// s3 file url
|
||||||
@ -24,7 +25,7 @@ export const S3_URL = `/api/file-assets/`;
|
|||||||
// LIST USER INVITATIONS ---- RESPOND INVITATIONS IN BULK
|
// LIST USER INVITATIONS ---- RESPOND INVITATIONS IN BULK
|
||||||
export const USER_WORKSPACE_INVITATIONS = "/api/users/me/invitations/workspaces/";
|
export const USER_WORKSPACE_INVITATIONS = "/api/users/me/invitations/workspaces/";
|
||||||
export const USER_PROJECT_INVITATIONS = "/api/users/me/invitations/projects/";
|
export const USER_PROJECT_INVITATIONS = "/api/users/me/invitations/projects/";
|
||||||
|
export const LAST_ACTIVE_WORKSPACE_AND_PROJECTS = "/api/users/last-visited-workspace/";
|
||||||
export const USER_WORKSPACE_INVITATION = (invitationId: string) =>
|
export const USER_WORKSPACE_INVITATION = (invitationId: string) =>
|
||||||
`/api/users/me/invitations/${invitationId}/`;
|
`/api/users/me/invitations/${invitationId}/`;
|
||||||
|
|
||||||
@ -33,8 +34,6 @@ export const JOIN_WORKSPACE = (workspaceSlug: string, invitationId: string) =>
|
|||||||
export const JOIN_PROJECT = (workspaceSlug: string) =>
|
export const JOIN_PROJECT = (workspaceSlug: string) =>
|
||||||
`/api/workspaces/${workspaceSlug}/projects/join/`;
|
`/api/workspaces/${workspaceSlug}/projects/join/`;
|
||||||
|
|
||||||
export const USER_ISSUES = "/api/users/me/issues/";
|
|
||||||
|
|
||||||
// workspaces
|
// workspaces
|
||||||
export const WORKSPACES_ENDPOINT = "/api/workspaces/";
|
export const WORKSPACES_ENDPOINT = "/api/workspaces/";
|
||||||
export const WORKSPACE_DETAIL = (workspaceSlug: string) => `/api/workspaces/${workspaceSlug}/`;
|
export const WORKSPACE_DETAIL = (workspaceSlug: string) => `/api/workspaces/${workspaceSlug}/`;
|
||||||
@ -65,6 +64,8 @@ export const PROJECT_MEMBERS = (workspaceSlug: string, projectId: string) =>
|
|||||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/`;
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/`;
|
||||||
export const PROJECT_MEMBER_DETAIL = (workspaceSlug: string, projectId: string, memberId: string) =>
|
export const PROJECT_MEMBER_DETAIL = (workspaceSlug: string, projectId: string, memberId: string) =>
|
||||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/${memberId}/`;
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/${memberId}/`;
|
||||||
|
export const PROJECT_VIEW_ENDPOINT = (workspaceSlug: string, projectId: string) =>
|
||||||
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/project-views/`;
|
||||||
|
|
||||||
export const PROJECT_INVITATIONS = (workspaceSlug: string, projectId: string) =>
|
export const PROJECT_INVITATIONS = (workspaceSlug: string, projectId: string) =>
|
||||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/invitations/`;
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/invitations/`;
|
||||||
|
@ -207,3 +207,12 @@ export const cosineSimilarity = (a: string, b: string) => {
|
|||||||
|
|
||||||
return dotProduct / Math.sqrt(magnitudeA * magnitudeB);
|
return dotProduct / Math.sqrt(magnitudeA * magnitudeB);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createSimilarString = (str: string) => {
|
||||||
|
const shuffled = str
|
||||||
|
.split("")
|
||||||
|
.sort(() => Math.random() - 0.5)
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
return shuffled;
|
||||||
|
};
|
||||||
|
@ -2,12 +2,12 @@ export const CURRENT_USER = "CURRENT_USER";
|
|||||||
export const USER_WORKSPACE_INVITATIONS = "USER_WORKSPACE_INVITATIONS";
|
export const USER_WORKSPACE_INVITATIONS = "USER_WORKSPACE_INVITATIONS";
|
||||||
export const USER_WORKSPACES = "USER_WORKSPACES";
|
export const USER_WORKSPACES = "USER_WORKSPACES";
|
||||||
|
|
||||||
export const WORKSPACE_MEMBERS = "WORKSPACE_MEMBERS";
|
export const WORKSPACE_MEMBERS = (workspaceSlug: string) => `WORKSPACE_MEMBERS_${workspaceSlug}`;
|
||||||
export const WORKSPACE_INVITATIONS = "WORKSPACE_INVITATIONS";
|
export const WORKSPACE_INVITATIONS = "WORKSPACE_INVITATIONS";
|
||||||
export const WORKSPACE_INVITATION = "WORKSPACE_INVITATION";
|
export const WORKSPACE_INVITATION = "WORKSPACE_INVITATION";
|
||||||
|
|
||||||
export const PROJECTS_LIST = (workspaceSlug: string) => `PROJECTS_LIST_${workspaceSlug}`;
|
export const PROJECTS_LIST = (workspaceSlug: string) => `PROJECTS_LIST_${workspaceSlug}`;
|
||||||
export const PROJECT_DETAILS = "PROJECT_DETAILS";
|
export const PROJECT_DETAILS = (projectId: string) => `PROJECT_DETAILS_${projectId}`;
|
||||||
|
|
||||||
export const PROJECT_MEMBERS = (projectId: string) => `PROJECT_MEMBERS_${projectId}`;
|
export const PROJECT_MEMBERS = (projectId: string) => `PROJECT_MEMBERS_${projectId}`;
|
||||||
export const PROJECT_INVITATIONS = "PROJECT_INVITATIONS";
|
export const PROJECT_INVITATIONS = "PROJECT_INVITATIONS";
|
||||||
@ -29,4 +29,4 @@ export const CYCLE_DETAIL = "CYCLE_DETAIL";
|
|||||||
export const STATE_LIST = (projectId: string) => `STATE_LIST_${projectId}`;
|
export const STATE_LIST = (projectId: string) => `STATE_LIST_${projectId}`;
|
||||||
export const STATE_DETAIL = "STATE_DETAIL";
|
export const STATE_DETAIL = "STATE_DETAIL";
|
||||||
|
|
||||||
export const USER_ISSUE = "USER_ISSUE";
|
export const USER_ISSUE = (workspaceSlug: string) => `USER_ISSUE_${workspaceSlug}`;
|
||||||
|
12
apps/app/constants/global.tsx
Normal file
12
apps/app/constants/global.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export const getPriorityIcon = (priority: string) => {
|
||||||
|
switch (priority) {
|
||||||
|
case "urgent":
|
||||||
|
return <span className="material-symbols-rounded">signal_cellular_alt</span>;
|
||||||
|
case "high":
|
||||||
|
return <span className="material-symbols-rounded">signal_cellular_alt_2_bar</span>;
|
||||||
|
case "medium":
|
||||||
|
return <span className="material-symbols-rounded">signal_cellular_alt_1_bar</span>;
|
||||||
|
default:
|
||||||
|
return <span>N/A</span>;
|
||||||
|
}
|
||||||
|
};
|
@ -6,3 +6,5 @@ export const ROLE = {
|
|||||||
15: "Member",
|
15: "Member",
|
||||||
20: "Admin",
|
20: "Admin",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const NETWORK_CHOICES = { "0": "Secret", "2": "Public" };
|
||||||
|
@ -2,3 +2,5 @@ export const TOGGLE_SIDEBAR = "TOGGLE_SIDEBAR";
|
|||||||
export const REHYDRATE_THEME = "REHYDRATE_THEME";
|
export const REHYDRATE_THEME = "REHYDRATE_THEME";
|
||||||
export const SET_ISSUE_VIEW = "SET_ISSUE_VIEW";
|
export const SET_ISSUE_VIEW = "SET_ISSUE_VIEW";
|
||||||
export const SET_GROUP_BY_PROPERTY = "SET_GROUP_BY_PROPERTY";
|
export const SET_GROUP_BY_PROPERTY = "SET_GROUP_BY_PROPERTY";
|
||||||
|
export const SET_ORDER_BY_PROPERTY = "SET_ORDER_BY_PROPERTY";
|
||||||
|
export const SET_FILTER_ISSUES = "SET_FILTER_ISSUES";
|
||||||
|
@ -5,6 +5,8 @@ import {
|
|||||||
REHYDRATE_THEME,
|
REHYDRATE_THEME,
|
||||||
SET_ISSUE_VIEW,
|
SET_ISSUE_VIEW,
|
||||||
SET_GROUP_BY_PROPERTY,
|
SET_GROUP_BY_PROPERTY,
|
||||||
|
SET_ORDER_BY_PROPERTY,
|
||||||
|
SET_FILTER_ISSUES,
|
||||||
} from "constants/theme.context.constants";
|
} from "constants/theme.context.constants";
|
||||||
// components
|
// components
|
||||||
import ToastAlert from "components/toast-alert";
|
import ToastAlert from "components/toast-alert";
|
||||||
@ -12,30 +14,30 @@ import ToastAlert from "components/toast-alert";
|
|||||||
export const themeContext = createContext<ContextType>({} as ContextType);
|
export const themeContext = createContext<ContextType>({} as ContextType);
|
||||||
|
|
||||||
// types
|
// types
|
||||||
import type { IIssue, NestedKeyOf } from "types";
|
import type { IIssue, NestedKeyOf, ProjectViewTheme as Theme } from "types";
|
||||||
|
|
||||||
type Theme = {
|
|
||||||
collapsed: boolean;
|
|
||||||
issueView: "list" | "kanban" | null;
|
|
||||||
groupByProperty: NestedKeyOf<IIssue> | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ReducerActionType = {
|
type ReducerActionType = {
|
||||||
type:
|
type:
|
||||||
| typeof TOGGLE_SIDEBAR
|
| typeof TOGGLE_SIDEBAR
|
||||||
| typeof REHYDRATE_THEME
|
| typeof REHYDRATE_THEME
|
||||||
| typeof SET_ISSUE_VIEW
|
| typeof SET_ISSUE_VIEW
|
||||||
|
| typeof SET_ORDER_BY_PROPERTY
|
||||||
|
| typeof SET_FILTER_ISSUES
|
||||||
| typeof SET_GROUP_BY_PROPERTY;
|
| typeof SET_GROUP_BY_PROPERTY;
|
||||||
payload?: Partial<Theme>;
|
payload?: Partial<Theme>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ContextType = {
|
type ContextType = {
|
||||||
collapsed: boolean;
|
collapsed: boolean;
|
||||||
|
orderBy: NestedKeyOf<IIssue> | null;
|
||||||
issueView: "list" | "kanban" | null;
|
issueView: "list" | "kanban" | null;
|
||||||
groupByProperty: NestedKeyOf<IIssue> | null;
|
groupByProperty: NestedKeyOf<IIssue> | null;
|
||||||
|
filterIssue: "activeIssue" | "backlogIssue" | null;
|
||||||
toggleCollapsed: () => void;
|
toggleCollapsed: () => void;
|
||||||
setIssueView: (display: "list" | "kanban") => void;
|
setIssueView: (display: "list" | "kanban") => void;
|
||||||
setGroupByProperty: (property: NestedKeyOf<IIssue> | null) => void;
|
setGroupByProperty: (property: NestedKeyOf<IIssue> | null) => void;
|
||||||
|
setOrderBy: (property: NestedKeyOf<IIssue> | null) => void;
|
||||||
|
setFilterIssue: (property: "activeIssue" | "backlogIssue" | null) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StateType = Theme;
|
type StateType = Theme;
|
||||||
@ -45,6 +47,8 @@ export const initialState: StateType = {
|
|||||||
collapsed: false,
|
collapsed: false,
|
||||||
issueView: "list",
|
issueView: "list",
|
||||||
groupByProperty: null,
|
groupByProperty: null,
|
||||||
|
orderBy: null,
|
||||||
|
filterIssue: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const reducer: ReducerFunctionType = (state, action) => {
|
export const reducer: ReducerFunctionType = (state, action) => {
|
||||||
@ -87,6 +91,28 @@ export const reducer: ReducerFunctionType = (state, action) => {
|
|||||||
...newState,
|
...newState,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case SET_ORDER_BY_PROPERTY: {
|
||||||
|
const newState = {
|
||||||
|
...state,
|
||||||
|
orderBy: payload?.orderBy || null,
|
||||||
|
};
|
||||||
|
localStorage.setItem("theme", JSON.stringify(newState));
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
...newState,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case SET_FILTER_ISSUES: {
|
||||||
|
const newState = {
|
||||||
|
...state,
|
||||||
|
filterIssue: payload?.filterIssue || null,
|
||||||
|
};
|
||||||
|
localStorage.setItem("theme", JSON.stringify(newState));
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
...newState,
|
||||||
|
};
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
@ -120,6 +146,24 @@ export const ThemeContextProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const setOrderBy = useCallback((property: NestedKeyOf<IIssue> | null) => {
|
||||||
|
dispatch({
|
||||||
|
type: SET_ORDER_BY_PROPERTY,
|
||||||
|
payload: {
|
||||||
|
orderBy: property,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const setFilterIssue = useCallback((property: "activeIssue" | "backlogIssue" | null) => {
|
||||||
|
dispatch({
|
||||||
|
type: SET_FILTER_ISSUES,
|
||||||
|
payload: {
|
||||||
|
filterIssue: property,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: REHYDRATE_THEME,
|
type: REHYDRATE_THEME,
|
||||||
@ -135,6 +179,10 @@ export const ThemeContextProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||||||
setIssueView,
|
setIssueView,
|
||||||
groupByProperty: state.groupByProperty,
|
groupByProperty: state.groupByProperty,
|
||||||
setGroupByProperty,
|
setGroupByProperty,
|
||||||
|
orderBy: state.orderBy,
|
||||||
|
setOrderBy,
|
||||||
|
filterIssue: state.filterIssue,
|
||||||
|
setFilterIssue,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ToastAlert />
|
<ToastAlert />
|
||||||
|
@ -6,9 +6,9 @@ import { useRouter } from "next/router";
|
|||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
// services
|
// services
|
||||||
import userService from "lib/services/user.service";
|
import userService from "lib/services/user.service";
|
||||||
import issuesServices from "lib/services/issues.services";
|
import issuesServices from "lib/services/issues.service";
|
||||||
import stateServices from "lib/services/state.services";
|
import stateServices from "lib/services/state.service";
|
||||||
import sprintsServices from "lib/services/cycles.services";
|
import sprintsServices from "lib/services/cycles.service";
|
||||||
import projectServices from "lib/services/project.service";
|
import projectServices from "lib/services/project.service";
|
||||||
import workspaceService from "lib/services/workspace.service";
|
import workspaceService from "lib/services/workspace.service";
|
||||||
// constants
|
// constants
|
||||||
|
@ -8,11 +8,11 @@ import useUser from "lib/hooks/useUser";
|
|||||||
import Container from "layouts/Container";
|
import Container from "layouts/Container";
|
||||||
import Sidebar from "layouts/Navbar/Sidebar";
|
import Sidebar from "layouts/Navbar/Sidebar";
|
||||||
// components
|
// components
|
||||||
import CreateProjectModal from "components/project/CreateProjectModal";
|
import CreateProjectModal from "components/project/create-project-modal";
|
||||||
// types
|
// types
|
||||||
import type { Props } from "./types";
|
import type { Props } from "./types";
|
||||||
|
|
||||||
const AdminLayout: React.FC<Props> = ({ meta, children }) => {
|
const AppLayout: React.FC<Props> = ({ meta, children, noPadding = false, bg = "primary" }) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -28,10 +28,16 @@ const AdminLayout: React.FC<Props> = ({ meta, children }) => {
|
|||||||
<CreateProjectModal isOpen={isOpen} setIsOpen={setIsOpen} />
|
<CreateProjectModal isOpen={isOpen} setIsOpen={setIsOpen} />
|
||||||
<div className="h-screen w-full flex overflow-x-hidden">
|
<div className="h-screen w-full flex overflow-x-hidden">
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
<main className="h-full w-full min-w-0 p-5 bg-primary overflow-y-auto">{children}</main>
|
<main
|
||||||
|
className={`h-full w-full min-w-0 overflow-y-auto ${noPadding ? "" : "p-5"} ${
|
||||||
|
bg === "primary" ? "bg-primary" : bg === "secondary" ? "bg-secondary" : "bg-primary"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AdminLayout;
|
export default AppLayout;
|
@ -9,8 +9,7 @@ import authenticationService from "lib/services/authentication.service";
|
|||||||
// hooks
|
// hooks
|
||||||
import useUser from "lib/hooks/useUser";
|
import useUser from "lib/hooks/useUser";
|
||||||
import useTheme from "lib/hooks/useTheme";
|
import useTheme from "lib/hooks/useTheme";
|
||||||
// components
|
import useToast from "lib/hooks/useToast";
|
||||||
import CreateProjectModal from "components/project/CreateProjectModal";
|
|
||||||
// headless ui
|
// headless ui
|
||||||
import { Dialog, Disclosure, Menu, Transition } from "@headlessui/react";
|
import { Dialog, Disclosure, Menu, Transition } from "@headlessui/react";
|
||||||
// icons
|
// icons
|
||||||
@ -107,7 +106,6 @@ const userLinks = [
|
|||||||
|
|
||||||
const Sidebar: React.FC = () => {
|
const Sidebar: React.FC = () => {
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||||
const [isCreateProjectModal, setCreateProjectModal] = useState(false);
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@ -119,9 +117,10 @@ const Sidebar: React.FC = () => {
|
|||||||
|
|
||||||
const { collapsed: sidebarCollapse, toggleCollapsed } = useTheme();
|
const { collapsed: sidebarCollapse, toggleCollapsed } = useTheme();
|
||||||
|
|
||||||
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="h-full">
|
<nav className="h-full">
|
||||||
<CreateProjectModal isOpen={isCreateProjectModal} setIsOpen={setCreateProjectModal} />
|
|
||||||
<Transition.Root show={sidebarOpen} as={React.Fragment}>
|
<Transition.Root show={sidebarOpen} as={React.Fragment}>
|
||||||
<Dialog as="div" className="relative z-40 md:hidden" onClose={setSidebarOpen}>
|
<Dialog as="div" className="relative z-40 md:hidden" onClose={setSidebarOpen}>
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
@ -282,9 +281,11 @@ const Sidebar: React.FC = () => {
|
|||||||
last_workspace_id: workspace?.id,
|
last_workspace_id: workspace?.id,
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
router.push("/workspace");
|
const isInProject =
|
||||||
|
router.pathname.includes("/[projectId]/");
|
||||||
|
if (isInProject) router.push("/workspace");
|
||||||
})
|
})
|
||||||
.catch((err) => console.log);
|
.catch((err) => console.error(err));
|
||||||
}}
|
}}
|
||||||
className={`${
|
className={`${
|
||||||
active ? "bg-theme text-white" : "text-gray-900"
|
active ? "bg-theme text-white" : "text-gray-900"
|
||||||
@ -478,7 +479,13 @@ const Sidebar: React.FC = () => {
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
copyTextToClipboard(
|
copyTextToClipboard(
|
||||||
`https://app.plane.so/projects/${project?.id}/issues/`
|
`https://app.plane.so/projects/${project?.id}/issues/`
|
||||||
)
|
).then(() => {
|
||||||
|
setToastAlert({
|
||||||
|
title: "Link Copied",
|
||||||
|
message: "Link copied to clipboard",
|
||||||
|
type: "success",
|
||||||
|
});
|
||||||
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ClipboardDocumentIcon className="h-3 w-3" />
|
<ClipboardDocumentIcon className="h-3 w-3" />
|
||||||
@ -546,7 +553,13 @@ const Sidebar: React.FC = () => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="group flex justify-center items-center gap-2 w-full rounded-md p-2 text-sm bg-theme text-white"
|
className="group flex justify-center items-center gap-2 w-full rounded-md p-2 text-sm bg-theme text-white"
|
||||||
onClick={() => setCreateProjectModal(true)}
|
onClick={() => {
|
||||||
|
const e = new KeyboardEvent("keydown", {
|
||||||
|
ctrlKey: true,
|
||||||
|
key: "p",
|
||||||
|
});
|
||||||
|
document.dispatchEvent(e);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-5 w-5" />
|
<PlusIcon className="h-5 w-5" />
|
||||||
{!sidebarCollapse && "Create Project"}
|
{!sidebarCollapse && "Create Project"}
|
||||||
|
2
apps/app/layouts/types.d.ts
vendored
2
apps/app/layouts/types.d.ts
vendored
@ -8,4 +8,6 @@ export type Meta = {
|
|||||||
export type Props = {
|
export type Props = {
|
||||||
meta?: Meta;
|
meta?: Meta;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
noPadding?: boolean;
|
||||||
|
bg?: "primary" | "secondary";
|
||||||
};
|
};
|
||||||
|
@ -4,7 +4,7 @@ import type { NextPage } from "next";
|
|||||||
// redirect
|
// redirect
|
||||||
import redirect from "lib/redirect";
|
import redirect from "lib/redirect";
|
||||||
|
|
||||||
const withAuth = (WrappedComponent: NextPage) => {
|
const withAuth = (WrappedComponent: NextPage, getBackToSameRoute: boolean = true) => {
|
||||||
const Wrapper: NextPage<any> = (props) => {
|
const Wrapper: NextPage<any> = (props) => {
|
||||||
return <WrappedComponent {...props} />;
|
return <WrappedComponent {...props} />;
|
||||||
};
|
};
|
||||||
@ -17,7 +17,8 @@ const withAuth = (WrappedComponent: NextPage) => {
|
|||||||
const token = cookies?.split("accessToken=")?.[1]?.split(";")?.[0];
|
const token = cookies?.split("accessToken=")?.[1]?.split(";")?.[0];
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
redirect(ctx, "/signin");
|
if (getBackToSameRoute) redirect(ctx, "/signin?next=" + ctx?.asPath);
|
||||||
|
else redirect(ctx, "/signin");
|
||||||
}
|
}
|
||||||
|
|
||||||
const pageProps =
|
const pageProps =
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { useState } from "react";
|
|
||||||
// hooks
|
// hooks
|
||||||
import useTheme from "./useTheme";
|
import useTheme from "./useTheme";
|
||||||
import useUser from "./useUser";
|
import useUser from "./useUser";
|
||||||
@ -7,14 +6,19 @@ import { groupBy, orderArrayBy } from "constants/common";
|
|||||||
// constants
|
// constants
|
||||||
import { PRIORITIES } from "constants/";
|
import { PRIORITIES } from "constants/";
|
||||||
// types
|
// types
|
||||||
import type { IssueResponse, IIssue, NestedKeyOf } from "types";
|
import type { IssueResponse, IIssue } from "types";
|
||||||
|
|
||||||
const useIssuesFilter = (projectIssues?: IssueResponse) => {
|
const useIssuesFilter = (projectIssues?: IssueResponse) => {
|
||||||
const { issueView, setIssueView, groupByProperty, setGroupByProperty } = useTheme();
|
const {
|
||||||
|
issueView,
|
||||||
const [orderBy, setOrderBy] = useState<NestedKeyOf<IIssue> | null>(null);
|
setIssueView,
|
||||||
|
groupByProperty,
|
||||||
const [filterIssue, setFilterIssue] = useState<"activeIssue" | "backlogIssue" | null>(null);
|
setGroupByProperty,
|
||||||
|
orderBy,
|
||||||
|
setOrderBy,
|
||||||
|
filterIssue,
|
||||||
|
setFilterIssue,
|
||||||
|
} = useTheme();
|
||||||
|
|
||||||
const { states } = useUser();
|
const { states } = useUser();
|
||||||
|
|
||||||
@ -52,32 +56,36 @@ const useIssuesFilter = (projectIssues?: IssueResponse) => {
|
|||||||
|
|
||||||
if (filterIssue !== null) {
|
if (filterIssue !== null) {
|
||||||
if (filterIssue === "activeIssue") {
|
if (filterIssue === "activeIssue") {
|
||||||
groupedByIssues = Object.keys(groupedByIssues).reduce((acc, key) => {
|
const filteredStates = states?.filter(
|
||||||
const value = groupedByIssues[key];
|
(state) => state.group === "started" || state.group === "unstarted"
|
||||||
const filteredValue = value.filter(
|
);
|
||||||
(issue) =>
|
groupedByIssues = Object.fromEntries(
|
||||||
issue.state_detail.group === "started" || issue.state_detail.group === "unstarted"
|
filteredStates
|
||||||
);
|
?.sort((a, b) => a.sequence - b.sequence)
|
||||||
if (filteredValue.length > 0) {
|
?.map((state) => [
|
||||||
acc[key] = filteredValue;
|
state.name,
|
||||||
}
|
projectIssues?.results.filter((issue) => issue.state === state.id) ?? [],
|
||||||
return acc;
|
]) ?? []
|
||||||
}, {} as typeof groupedByIssues);
|
);
|
||||||
} else if (filterIssue === "backlogIssue") {
|
} else if (filterIssue === "backlogIssue") {
|
||||||
groupedByIssues = Object.keys(groupedByIssues).reduce((acc, key) => {
|
const filteredStates = states?.filter(
|
||||||
const value = groupedByIssues[key];
|
(state) => state.group === "backlog" || state.group === "cancelled"
|
||||||
const filteredValue = value.filter(
|
);
|
||||||
(issue) =>
|
groupedByIssues = Object.fromEntries(
|
||||||
issue.state_detail.group === "backlog" || issue.state_detail.group === "cancelled"
|
filteredStates
|
||||||
);
|
?.sort((a, b) => a.sequence - b.sequence)
|
||||||
if (filteredValue.length > 0) {
|
?.map((state) => [
|
||||||
acc[key] = filteredValue;
|
state.name,
|
||||||
}
|
projectIssues?.results.filter((issue) => issue.state === state.id) ?? [],
|
||||||
return acc;
|
]) ?? []
|
||||||
}, {} as typeof groupedByIssues);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (groupByProperty === "priority" && orderBy === "priority") {
|
||||||
|
setOrderBy(null);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
groupedByIssues,
|
groupedByIssues,
|
||||||
issueView,
|
issueView,
|
||||||
|
@ -4,7 +4,7 @@ import useSWR from "swr";
|
|||||||
// api routes
|
// api routes
|
||||||
import { ISSUE_PROPERTIES_ENDPOINT } from "constants/api-routes";
|
import { ISSUE_PROPERTIES_ENDPOINT } from "constants/api-routes";
|
||||||
// services
|
// services
|
||||||
import issueServices from "lib/services/issues.services";
|
import issueServices from "lib/services/issues.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useUser from "./useUser";
|
import useUser from "./useUser";
|
||||||
// types
|
// types
|
||||||
|
@ -10,9 +10,12 @@ import {
|
|||||||
PROJECT_MEMBERS,
|
PROJECT_MEMBERS,
|
||||||
PROJECT_MEMBER_DETAIL,
|
PROJECT_MEMBER_DETAIL,
|
||||||
USER_PROJECT_INVITATIONS,
|
USER_PROJECT_INVITATIONS,
|
||||||
|
PROJECT_VIEW_ENDPOINT,
|
||||||
} from "constants/api-routes";
|
} from "constants/api-routes";
|
||||||
// services
|
// services
|
||||||
import APIService from "lib/services/api.service";
|
import APIService from "lib/services/api.service";
|
||||||
|
// types
|
||||||
|
import type { IProject, IProjectMember, IProjectMemberInvitation, ProjectViewTheme } from "types";
|
||||||
|
|
||||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
||||||
|
|
||||||
@ -21,13 +24,13 @@ class ProjectServices extends APIService {
|
|||||||
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
||||||
}
|
}
|
||||||
|
|
||||||
async createProject(workspace_slug: string, data: any): Promise<any> {
|
async createProject(workspacSlug: string, data: Partial<IProject>): Promise<IProject> {
|
||||||
return this.post(PROJECTS_ENDPOINT(workspace_slug), data)
|
return this.post(PROJECTS_ENDPOINT(workspacSlug), data)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
throw error?.response?.data;
|
throw error?.response;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,8 +48,8 @@ class ProjectServices extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjects(workspace_slug: string): Promise<any> {
|
async getProjects(workspacSlug: string): Promise<IProject[]> {
|
||||||
return this.get(PROJECTS_ENDPOINT(workspace_slug))
|
return this.get(PROJECTS_ENDPOINT(workspacSlug))
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
@ -55,8 +58,8 @@ class ProjectServices extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProject(workspace_slug: string, project_id: string): Promise<any> {
|
async getProject(workspacSlug: string, projectId: string): Promise<IProject> {
|
||||||
return this.get(PROJECT_DETAIL(workspace_slug, project_id))
|
return this.get(PROJECT_DETAIL(workspacSlug, projectId))
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
@ -65,8 +68,12 @@ class ProjectServices extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateProject(workspace_slug: string, project_id: string, data: any): Promise<any> {
|
async updateProject(
|
||||||
return this.patch(PROJECT_DETAIL(workspace_slug, project_id), data)
|
workspacSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
data: Partial<IProject>
|
||||||
|
): Promise<IProject> {
|
||||||
|
return this.patch(PROJECT_DETAIL(workspacSlug, projectId), data)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
@ -75,8 +82,8 @@ class ProjectServices extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteProject(workspace_slug: string, project_id: string): Promise<any> {
|
async deleteProject(workspacSlug: string, projectId: string): Promise<any> {
|
||||||
return this.delete(PROJECT_DETAIL(workspace_slug, project_id))
|
return this.delete(PROJECT_DETAIL(workspacSlug, projectId))
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
@ -85,8 +92,8 @@ class ProjectServices extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async inviteProject(workspace_slug: string, project_id: string, data: any): Promise<any> {
|
async inviteProject(workspacSlug: string, projectId: string, data: any): Promise<any> {
|
||||||
return this.post(INVITE_PROJECT(workspace_slug, project_id), data)
|
return this.post(INVITE_PROJECT(workspacSlug, projectId), data)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
@ -95,8 +102,8 @@ class ProjectServices extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async joinProject(workspace_slug: string, data: any): Promise<any> {
|
async joinProject(workspacSlug: string, data: any): Promise<any> {
|
||||||
return this.post(JOIN_PROJECT(workspace_slug), data)
|
return this.post(JOIN_PROJECT(workspacSlug), data)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
@ -115,8 +122,8 @@ class ProjectServices extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async projectMembers(workspace_slug: string, project_id: string): Promise<any> {
|
async projectMembers(workspacSlug: string, projectId: string): Promise<IProjectMember[]> {
|
||||||
return this.get(PROJECT_MEMBERS(workspace_slug, project_id))
|
return this.get(PROJECT_MEMBERS(workspacSlug, projectId))
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
@ -126,12 +133,12 @@ class ProjectServices extends APIService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async updateProjectMember(
|
async updateProjectMember(
|
||||||
workspace_slug: string,
|
workspacSlug: string,
|
||||||
project_id: string,
|
projectId: string,
|
||||||
memberId: string,
|
memberId: string,
|
||||||
data: any
|
data: Partial<IProjectMember>
|
||||||
): Promise<any> {
|
): Promise<IProjectMember> {
|
||||||
return this.put(PROJECT_MEMBER_DETAIL(workspace_slug, project_id, memberId), data)
|
return this.put(PROJECT_MEMBER_DETAIL(workspacSlug, projectId, memberId), data)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
@ -141,11 +148,11 @@ class ProjectServices extends APIService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteProjectMember(
|
async deleteProjectMember(
|
||||||
workspace_slug: string,
|
workspacSlug: string,
|
||||||
project_id: string,
|
projectId: string,
|
||||||
memberId: string
|
memberId: string
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
return this.delete(PROJECT_MEMBER_DETAIL(workspace_slug, project_id, memberId))
|
return this.delete(PROJECT_MEMBER_DETAIL(workspacSlug, projectId, memberId))
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
@ -154,8 +161,11 @@ class ProjectServices extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async projectInvitations(workspace_slug: string, project_id: string): Promise<any> {
|
async projectInvitations(
|
||||||
return this.get(PROJECT_INVITATIONS(workspace_slug, project_id))
|
workspacSlug: string,
|
||||||
|
projectId: string
|
||||||
|
): Promise<IProjectMemberInvitation[]> {
|
||||||
|
return this.get(PROJECT_INVITATIONS(workspacSlug, projectId))
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
@ -165,11 +175,11 @@ class ProjectServices extends APIService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async updateProjectInvitation(
|
async updateProjectInvitation(
|
||||||
workspace_slug: string,
|
workspacSlug: string,
|
||||||
project_id: string,
|
projectId: string,
|
||||||
invitation_id: string
|
invitationId: string
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
return this.put(PROJECT_INVITATION_DETAIL(workspace_slug, project_id, invitation_id))
|
return this.put(PROJECT_INVITATION_DETAIL(workspacSlug, projectId, invitationId))
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
@ -177,12 +187,27 @@ class ProjectServices extends APIService {
|
|||||||
throw error?.response?.data;
|
throw error?.response?.data;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteProjectInvitation(
|
async deleteProjectInvitation(
|
||||||
workspace_slug: string,
|
workspacSlug: string,
|
||||||
project_id: string,
|
projectId: string,
|
||||||
invitation_id: string
|
invitationId: string
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
return this.delete(PROJECT_INVITATION_DETAIL(workspace_slug, project_id, invitation_id))
|
return this.delete(PROJECT_INVITATION_DETAIL(workspacSlug, projectId, invitationId))
|
||||||
|
.then((response) => {
|
||||||
|
return response?.data;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async setProjectView(
|
||||||
|
workspacSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
data: ProjectViewTheme
|
||||||
|
): Promise<any> {
|
||||||
|
await this.patch(PROJECT_VIEW_ENDPOINT(workspacSlug, projectId), data)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
|
@ -16,8 +16,8 @@ class UserService extends APIService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async userIssues(): Promise<any> {
|
async userIssues(workspaceSlug: string): Promise<any> {
|
||||||
return this.get(USER_ISSUES_ENDPOINT)
|
return this.get(USER_ISSUES_ENDPOINT(workspaceSlug))
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
|
@ -11,18 +11,27 @@ import {
|
|||||||
WORKSPACE_INVITATION_DETAIL,
|
WORKSPACE_INVITATION_DETAIL,
|
||||||
USER_WORKSPACE_INVITATION,
|
USER_WORKSPACE_INVITATION,
|
||||||
USER_WORKSPACE_INVITATIONS,
|
USER_WORKSPACE_INVITATIONS,
|
||||||
|
LAST_ACTIVE_WORKSPACE_AND_PROJECTS,
|
||||||
} from "constants/api-routes";
|
} from "constants/api-routes";
|
||||||
// services
|
// services
|
||||||
import APIService from "lib/services/api.service";
|
import APIService from "lib/services/api.service";
|
||||||
|
|
||||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
||||||
|
|
||||||
|
// types
|
||||||
|
import {
|
||||||
|
ILastActiveWorkspaceDetails,
|
||||||
|
IWorkspace,
|
||||||
|
IWorkspaceMember,
|
||||||
|
IWorkspaceMemberInvitation,
|
||||||
|
} from "types";
|
||||||
|
|
||||||
class WorkspaceService extends APIService {
|
class WorkspaceService extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
||||||
}
|
}
|
||||||
|
|
||||||
async userWorkspaces(): Promise<any> {
|
async userWorkspaces(): Promise<IWorkspace[]> {
|
||||||
return this.get(USER_WORKSPACES)
|
return this.get(USER_WORKSPACES)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return response?.data;
|
return response?.data;
|
||||||
@ -32,7 +41,7 @@ class WorkspaceService extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async createWorkspace(data: any): Promise<any> {
|
async createWorkspace(data: Partial<IWorkspace>): Promise<IWorkspace> {
|
||||||
return this.post(WORKSPACES_ENDPOINT, data)
|
return this.post(WORKSPACES_ENDPOINT, data)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return response?.data;
|
return response?.data;
|
||||||
@ -42,17 +51,8 @@ class WorkspaceService extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateWorkspace(workspace_slug: string, data: any): Promise<any> {
|
async updateWorkspace(workspaceSlug: string, data: Partial<IWorkspace>): Promise<IWorkspace> {
|
||||||
return this.patch(WORKSPACE_DETAIL(workspace_slug), data)
|
return this.patch(WORKSPACE_DETAIL(workspaceSlug), data)
|
||||||
.then((response) => {
|
|
||||||
return response?.data;
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
throw error?.response?.data;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
async deleteWorkspace(workspace_slug: string): Promise<any> {
|
|
||||||
return this.delete(WORKSPACE_DETAIL(workspace_slug))
|
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
@ -61,8 +61,8 @@ class WorkspaceService extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async inviteWorkspace(workspace_slug: string, data: any): Promise<any> {
|
async deleteWorkspace(workspaceSlug: string): Promise<any> {
|
||||||
return this.post(INVITE_WORKSPACE(workspace_slug), data)
|
return this.delete(WORKSPACE_DETAIL(workspaceSlug))
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
@ -70,8 +70,19 @@ class WorkspaceService extends APIService {
|
|||||||
throw error?.response?.data;
|
throw error?.response?.data;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async joinWorkspace(workspace_slug: string, InvitationId: string, data: any): Promise<any> {
|
|
||||||
return this.post(JOIN_WORKSPACE(workspace_slug, InvitationId), data, {
|
async inviteWorkspace(workspaceSlug: string, data: any): Promise<any> {
|
||||||
|
return this.post(INVITE_WORKSPACE(workspaceSlug), data)
|
||||||
|
.then((response) => {
|
||||||
|
return response?.data;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async joinWorkspace(workspaceSlug: string, InvitationId: string, data: any): Promise<any> {
|
||||||
|
return this.post(JOIN_WORKSPACE(workspaceSlug, InvitationId), data, {
|
||||||
headers: {},
|
headers: {},
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@ -92,7 +103,17 @@ class WorkspaceService extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async userWorkspaceInvitations(): Promise<any> {
|
async getLastActiveWorkspaceAndProjects(): Promise<ILastActiveWorkspaceDetails> {
|
||||||
|
return this.get(LAST_ACTIVE_WORKSPACE_AND_PROJECTS)
|
||||||
|
.then((response) => {
|
||||||
|
return response?.data;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async userWorkspaceInvitations(): Promise<IWorkspaceMemberInvitation[]> {
|
||||||
return this.get(USER_WORKSPACE_INVITATIONS)
|
return this.get(USER_WORKSPACE_INVITATIONS)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return response?.data;
|
return response?.data;
|
||||||
@ -102,8 +123,8 @@ class WorkspaceService extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async workspaceMembers(workspace_slug: string): Promise<any> {
|
async workspaceMembers(workspaceSlug: string): Promise<IWorkspaceMember[]> {
|
||||||
return this.get(WORKSPACE_MEMBERS(workspace_slug))
|
return this.get(WORKSPACE_MEMBERS(workspaceSlug))
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
@ -112,8 +133,12 @@ class WorkspaceService extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateWorkspaceMember(workspace_slug: string, memberId: string, data: any): Promise<any> {
|
async updateWorkspaceMember(
|
||||||
return this.put(WORKSPACE_MEMBER_DETAIL(workspace_slug, memberId), data)
|
workspaceSlug: string,
|
||||||
|
memberId: string,
|
||||||
|
data: Partial<IWorkspaceMember>
|
||||||
|
): Promise<IWorkspaceMember> {
|
||||||
|
return this.put(WORKSPACE_MEMBER_DETAIL(workspaceSlug, memberId), data)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
@ -122,8 +147,8 @@ class WorkspaceService extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteWorkspaceMember(workspace_slug: string, memberId: string): Promise<any> {
|
async deleteWorkspaceMember(workspaceSlug: string, memberId: string): Promise<any> {
|
||||||
return this.delete(WORKSPACE_MEMBER_DETAIL(workspace_slug, memberId))
|
return this.delete(WORKSPACE_MEMBER_DETAIL(workspaceSlug, memberId))
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
@ -132,8 +157,8 @@ class WorkspaceService extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async workspaceInvitations(workspace_slug: string): Promise<any> {
|
async workspaceInvitations(workspaceSlug: string): Promise<IWorkspaceMemberInvitation[]> {
|
||||||
return this.get(WORKSPACE_INVITATIONS(workspace_slug))
|
return this.get(WORKSPACE_INVITATIONS(workspaceSlug))
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
@ -142,8 +167,8 @@ class WorkspaceService extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getWorkspaceInvitation(invitation_id: string): Promise<any> {
|
async getWorkspaceInvitation(invitationId: string): Promise<IWorkspaceMemberInvitation> {
|
||||||
return this.get(USER_WORKSPACE_INVITATION(invitation_id), { headers: {} })
|
return this.get(USER_WORKSPACE_INVITATION(invitationId), { headers: {} })
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
@ -152,8 +177,11 @@ class WorkspaceService extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateWorkspaceInvitation(workspace_slug: string, invitation_id: string): Promise<any> {
|
async updateWorkspaceInvitation(
|
||||||
return this.put(WORKSPACE_INVITATION_DETAIL(workspace_slug, invitation_id))
|
workspaceSlug: string,
|
||||||
|
invitationId: string
|
||||||
|
): Promise<IWorkspaceMemberInvitation> {
|
||||||
|
return this.put(WORKSPACE_INVITATION_DETAIL(workspaceSlug, invitationId))
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
@ -161,8 +189,9 @@ class WorkspaceService extends APIService {
|
|||||||
throw error?.response?.data;
|
throw error?.response?.data;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async deleteWorkspaceInvitations(workspace_slug: string, invitation_id: string): Promise<any> {
|
|
||||||
return this.delete(WORKSPACE_INVITATION_DETAIL(workspace_slug, invitation_id))
|
async deleteWorkspaceInvitations(workspaceSlug: string, invitationId: string): Promise<any> {
|
||||||
|
return this.delete(WORKSPACE_INVITATION_DETAIL(workspaceSlug, invitationId))
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
|
@ -8,6 +8,8 @@ import { useForm } from "react-hook-form";
|
|||||||
import workspaceService from "lib/services/workspace.service";
|
import workspaceService from "lib/services/workspace.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useUser from "lib/hooks/useUser";
|
import useUser from "lib/hooks/useUser";
|
||||||
|
// hoc
|
||||||
|
import withAuth from "lib/hoc/withAuthWrapper";
|
||||||
// layouts
|
// layouts
|
||||||
import DefaultLayout from "layouts/DefaultLayout";
|
import DefaultLayout from "layouts/DefaultLayout";
|
||||||
// ui
|
// ui
|
||||||
@ -144,4 +146,4 @@ const CreateWorkspace: NextPage = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CreateWorkspace;
|
export default withAuth(CreateWorkspace);
|
||||||
|
@ -11,6 +11,8 @@ import userService from "lib/services/user.service";
|
|||||||
import useUser from "lib/hooks/useUser";
|
import useUser from "lib/hooks/useUser";
|
||||||
// constants
|
// constants
|
||||||
import { USER_WORKSPACE_INVITATIONS } from "constants/api-routes";
|
import { USER_WORKSPACE_INVITATIONS } from "constants/api-routes";
|
||||||
|
// hoc
|
||||||
|
import withAuth from "lib/hoc/withAuthWrapper";
|
||||||
// layouts
|
// layouts
|
||||||
import DefaultLayout from "layouts/DefaultLayout";
|
import DefaultLayout from "layouts/DefaultLayout";
|
||||||
// components
|
// components
|
||||||
@ -20,7 +22,7 @@ import { Button, Spinner, EmptySpace, EmptySpaceItem } from "ui";
|
|||||||
// icons
|
// icons
|
||||||
import { CubeIcon, PlusIcon } from "@heroicons/react/24/outline";
|
import { CubeIcon, PlusIcon } from "@heroicons/react/24/outline";
|
||||||
// types
|
// types
|
||||||
import type { IWorkspaceInvitation } from "types";
|
import type { IWorkspaceMemberInvitation } from "types";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
const OnBoard: NextPage = () => {
|
const OnBoard: NextPage = () => {
|
||||||
@ -30,13 +32,12 @@ const OnBoard: NextPage = () => {
|
|||||||
|
|
||||||
const [invitationsRespond, setInvitationsRespond] = useState<string[]>([]);
|
const [invitationsRespond, setInvitationsRespond] = useState<string[]>([]);
|
||||||
|
|
||||||
const { data: invitations, mutate } = useSWR<IWorkspaceInvitation[]>(
|
const { data: invitations, mutate } = useSWR(USER_WORKSPACE_INVITATIONS, () =>
|
||||||
USER_WORKSPACE_INVITATIONS,
|
workspaceService.userWorkspaceInvitations()
|
||||||
() => workspaceService.userWorkspaceInvitations()
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleInvitation = (
|
const handleInvitation = (
|
||||||
workspace_invitation: IWorkspaceInvitation,
|
workspace_invitation: IWorkspaceMemberInvitation,
|
||||||
action: "accepted" | "withdraw"
|
action: "accepted" | "withdraw"
|
||||||
) => {
|
) => {
|
||||||
if (action === "accepted") {
|
if (action === "accepted") {
|
||||||
@ -204,4 +205,4 @@ const OnBoard: NextPage = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default OnBoard;
|
export default withAuth(OnBoard);
|
||||||
|
@ -5,7 +5,7 @@ import type { NextPage } from "next";
|
|||||||
// swr
|
// swr
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
// layouts
|
// layouts
|
||||||
import AdminLayout from "layouts/AdminLayout";
|
import AppLayout from "layouts/AppLayout";
|
||||||
// hooks
|
// hooks
|
||||||
import useUser from "lib/hooks/useUser";
|
import useUser from "lib/hooks/useUser";
|
||||||
// ui
|
// ui
|
||||||
@ -18,7 +18,9 @@ import { USER_ISSUE } from "constants/fetch-keys";
|
|||||||
import { classNames } from "constants/common";
|
import { classNames } from "constants/common";
|
||||||
// services
|
// services
|
||||||
import userService from "lib/services/user.service";
|
import userService from "lib/services/user.service";
|
||||||
import issuesServices from "lib/services/issues.services";
|
import issuesServices from "lib/services/issues.service";
|
||||||
|
// hoc
|
||||||
|
import withAuth from "lib/hoc/withAuthWrapper";
|
||||||
// components
|
// components
|
||||||
import ChangeStateDropdown from "components/project/issues/my-issues/ChangeStateDropdown";
|
import ChangeStateDropdown from "components/project/issues/my-issues/ChangeStateDropdown";
|
||||||
// icons
|
// icons
|
||||||
@ -31,11 +33,11 @@ import { Menu, Transition } from "@headlessui/react";
|
|||||||
const MyIssues: NextPage = () => {
|
const MyIssues: NextPage = () => {
|
||||||
const [selectedWorkspace, setSelectedWorkspace] = useState<string | null>(null);
|
const [selectedWorkspace, setSelectedWorkspace] = useState<string | null>(null);
|
||||||
|
|
||||||
const { user, workspaces } = useUser();
|
const { user, workspaces, activeWorkspace } = useUser();
|
||||||
|
|
||||||
const { data: myIssues, mutate: mutateMyIssues } = useSWR<IIssue[]>(
|
const { data: myIssues, mutate: mutateMyIssues } = useSWR<IIssue[]>(
|
||||||
user ? USER_ISSUE : null,
|
user && activeWorkspace ? USER_ISSUE(activeWorkspace.slug) : null,
|
||||||
user ? () => userService.userIssues() : null
|
user && activeWorkspace ? () => userService.userIssues(activeWorkspace.slug) : null
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateMyIssues = (
|
const updateMyIssues = (
|
||||||
@ -74,7 +76,7 @@ const MyIssues: NextPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminLayout>
|
<AppLayout>
|
||||||
<div className="w-full h-full flex flex-col space-y-5">
|
<div className="w-full h-full flex flex-col space-y-5">
|
||||||
{myIssues ? (
|
{myIssues ? (
|
||||||
<>
|
<>
|
||||||
@ -274,8 +276,8 @@ const MyIssues: NextPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</AdminLayout>
|
</AppLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default MyIssues;
|
export default withAuth(MyIssues);
|
||||||
|
@ -1,22 +1,30 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
// next
|
// next
|
||||||
|
import Link from "next/link";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import type { NextPage } from "next";
|
import type { NextPage } from "next";
|
||||||
|
// swr
|
||||||
|
import useSWR from "swr";
|
||||||
// react hook form
|
// react hook form
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
// react dropzone
|
// react dropzone
|
||||||
import Dropzone, { useDropzone } from "react-dropzone";
|
import Dropzone from "react-dropzone";
|
||||||
// hooks
|
// hooks
|
||||||
import useUser from "lib/hooks/useUser";
|
import useUser from "lib/hooks/useUser";
|
||||||
|
import useToast from "lib/hooks/useToast";
|
||||||
|
// hoc
|
||||||
|
import withAuth from "lib/hoc/withAuthWrapper";
|
||||||
// layouts
|
// layouts
|
||||||
import AdminLayout from "layouts/AdminLayout";
|
import AppLayout from "layouts/AppLayout";
|
||||||
|
// constants
|
||||||
|
import { USER_ISSUE, USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys";
|
||||||
// services
|
// services
|
||||||
import userService from "lib/services/user.service";
|
import userService from "lib/services/user.service";
|
||||||
import fileServices from "lib/services/file.services";
|
import fileServices from "lib/services/file.service";
|
||||||
|
import workspaceService from "lib/services/workspace.service";
|
||||||
// ui
|
// ui
|
||||||
import { BreadcrumbItem, Breadcrumbs, Button, Input, Spinner } from "ui";
|
import { BreadcrumbItem, Breadcrumbs, Button, Input, Spinner } from "ui";
|
||||||
// types
|
// icons
|
||||||
import type { IIssue, IUser, IWorkspaceInvitation } from "types";
|
|
||||||
import {
|
import {
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
ClipboardDocumentListIcon,
|
ClipboardDocumentListIcon,
|
||||||
@ -26,11 +34,8 @@ import {
|
|||||||
UserPlusIcon,
|
UserPlusIcon,
|
||||||
XMarkIcon,
|
XMarkIcon,
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
import useSWR from "swr";
|
// types
|
||||||
import { USER_ISSUE, USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys";
|
import type { IIssue, IUser } from "types";
|
||||||
import useToast from "lib/hooks/useToast";
|
|
||||||
import Link from "next/link";
|
|
||||||
import workspaceService from "lib/services/workspace.service";
|
|
||||||
|
|
||||||
const defaultValues: Partial<IUser> = {
|
const defaultValues: Partial<IUser> = {
|
||||||
avatar: "",
|
avatar: "",
|
||||||
@ -44,13 +49,19 @@ const Profile: NextPage = () => {
|
|||||||
const [isImageUploading, setIsImageUploading] = useState(false);
|
const [isImageUploading, setIsImageUploading] = useState(false);
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
|
||||||
const { user: myProfile, mutateUser, projects } = useUser();
|
const { user: myProfile, mutateUser, projects, activeWorkspace } = useUser();
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const onSubmit = (formData: IUser) => {
|
const onSubmit = (formData: IUser) => {
|
||||||
|
const payload: Partial<IUser> = {
|
||||||
|
id: formData.id,
|
||||||
|
first_name: formData.first_name,
|
||||||
|
last_name: formData.last_name,
|
||||||
|
avatar: formData.avatar,
|
||||||
|
};
|
||||||
userService
|
userService
|
||||||
.updateUser(formData)
|
.updateUser(payload)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
mutateUser(response, false);
|
mutateUser(response, false);
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
@ -75,11 +86,11 @@ const Profile: NextPage = () => {
|
|||||||
} = useForm<IUser>({ defaultValues });
|
} = useForm<IUser>({ defaultValues });
|
||||||
|
|
||||||
const { data: myIssues } = useSWR<IIssue[]>(
|
const { data: myIssues } = useSWR<IIssue[]>(
|
||||||
myProfile ? USER_ISSUE : null,
|
myProfile && activeWorkspace ? USER_ISSUE(activeWorkspace.slug) : null,
|
||||||
myProfile ? () => userService.userIssues() : null
|
myProfile && activeWorkspace ? () => userService.userIssues(activeWorkspace.slug) : null
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: invitations } = useSWR<IWorkspaceInvitation[]>(USER_WORKSPACE_INVITATIONS, () =>
|
const { data: invitations } = useSWR(USER_WORKSPACE_INVITATIONS, () =>
|
||||||
workspaceService.userWorkspaceInvitations()
|
workspaceService.userWorkspaceInvitations()
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -92,7 +103,7 @@ const Profile: NextPage = () => {
|
|||||||
icon: RectangleStackIcon,
|
icon: RectangleStackIcon,
|
||||||
title: "My Issues",
|
title: "My Issues",
|
||||||
number: myIssues?.length ?? 0,
|
number: myIssues?.length ?? 0,
|
||||||
description: "View the list of issues assigned to you across the workspace.",
|
description: "View the list of issues assigned to you for this workspace.",
|
||||||
href: "/me/my-issues",
|
href: "/me/my-issues",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -112,7 +123,7 @@ const Profile: NextPage = () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminLayout
|
<AppLayout
|
||||||
meta={{
|
meta={{
|
||||||
title: "Plane - My Profile",
|
title: "Plane - My Profile",
|
||||||
}}
|
}}
|
||||||
@ -125,7 +136,8 @@ const Profile: NextPage = () => {
|
|||||||
<>
|
<>
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
<section className="relative p-5 rounded-xl flex gap-10 bg-secondary">
|
<section className="relative p-5 rounded-xl flex gap-10 bg-secondary">
|
||||||
<div
|
<button
|
||||||
|
type="button"
|
||||||
className="absolute top-4 right-4 bg-indigo-100 hover:bg-theme hover:text-white rounded p-1 cursor-pointer duration-300"
|
className="absolute top-4 right-4 bg-indigo-100 hover:bg-theme hover:text-white rounded p-1 cursor-pointer duration-300"
|
||||||
onClick={() => setIsEditing((prevData) => !prevData)}
|
onClick={() => setIsEditing((prevData) => !prevData)}
|
||||||
>
|
>
|
||||||
@ -134,7 +146,7 @@ const Profile: NextPage = () => {
|
|||||||
) : (
|
) : (
|
||||||
<PencilIcon className="h-4 w-4" />
|
<PencilIcon className="h-4 w-4" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</button>
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<Dropzone
|
<Dropzone
|
||||||
multiple={false}
|
multiple={false}
|
||||||
@ -241,21 +253,7 @@ const Profile: NextPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-sm text-gray-500">Email ID</h4>
|
<h4 className="text-sm text-gray-500">Email ID</h4>
|
||||||
{isEditing ? (
|
<h2>{myProfile.email}</h2>
|
||||||
<Input
|
|
||||||
id="email"
|
|
||||||
type="email"
|
|
||||||
register={register}
|
|
||||||
error={errors.email}
|
|
||||||
name="email"
|
|
||||||
validations={{
|
|
||||||
required: "Email is required",
|
|
||||||
}}
|
|
||||||
placeholder="Enter email"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<h2>{myProfile.email}</h2>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{isEditing && (
|
{isEditing && (
|
||||||
@ -303,8 +301,8 @@ const Profile: NextPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</AdminLayout>
|
</AppLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Profile;
|
export default withAuth(Profile);
|
||||||
|
@ -10,7 +10,7 @@ import useUser from "lib/hooks/useUser";
|
|||||||
// hoc
|
// hoc
|
||||||
import withAuthWrapper from "lib/hoc/withAuthWrapper";
|
import withAuthWrapper from "lib/hoc/withAuthWrapper";
|
||||||
// layouts
|
// layouts
|
||||||
import AdminLayout from "layouts/AdminLayout";
|
import AppLayout from "layouts/AppLayout";
|
||||||
// ui
|
// ui
|
||||||
import { Button } from "ui";
|
import { Button } from "ui";
|
||||||
// swr
|
// swr
|
||||||
@ -52,7 +52,7 @@ const MyWorkspacesInvites: NextPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminLayout
|
<AppLayout
|
||||||
meta={{
|
meta={{
|
||||||
title: "Plane - My Workspace Invites",
|
title: "Plane - My Workspace Invites",
|
||||||
}}
|
}}
|
||||||
@ -147,7 +147,7 @@ const MyWorkspacesInvites: NextPage = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AdminLayout>
|
</AppLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -5,14 +5,16 @@ import type { NextPage } from "next";
|
|||||||
// swr
|
// swr
|
||||||
import useSWR, { mutate } from "swr";
|
import useSWR, { mutate } from "swr";
|
||||||
// services
|
// services
|
||||||
import issuesServices from "lib/services/issues.services";
|
import issuesServices from "lib/services/issues.service";
|
||||||
import sprintService from "lib/services/cycles.services";
|
import sprintService from "lib/services/cycles.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useUser from "lib/hooks/useUser";
|
import useUser from "lib/hooks/useUser";
|
||||||
// fetching keys
|
// fetching keys
|
||||||
import { CYCLE_ISSUES, CYCLE_LIST } from "constants/fetch-keys";
|
import { CYCLE_ISSUES, CYCLE_LIST } from "constants/fetch-keys";
|
||||||
|
// hoc
|
||||||
|
import withAuth from "lib/hoc/withAuthWrapper";
|
||||||
// layouts
|
// layouts
|
||||||
import AdminLayout from "layouts/AdminLayout";
|
import AppLayout from "layouts/AppLayout";
|
||||||
// components
|
// components
|
||||||
import CycleView from "components/project/cycles/CycleView";
|
import CycleView from "components/project/cycles/CycleView";
|
||||||
import ConfirmIssueDeletion from "components/project/issues/ConfirmIssueDeletion";
|
import ConfirmIssueDeletion from "components/project/issues/ConfirmIssueDeletion";
|
||||||
@ -165,7 +167,7 @@ const ProjectSprints: NextPage = () => {
|
|||||||
}, [selectedIssues]);
|
}, [selectedIssues]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminLayout
|
<AppLayout
|
||||||
meta={{
|
meta={{
|
||||||
title: "Plane - Cycles",
|
title: "Plane - Cycles",
|
||||||
}}
|
}}
|
||||||
@ -254,8 +256,8 @@ const ProjectSprints: NextPage = () => {
|
|||||||
<Spinner />
|
<Spinner />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</AdminLayout>
|
</AppLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ProjectSprints;
|
export default withAuth(ProjectSprints);
|
||||||
|
@ -11,8 +11,7 @@ import { Controller, useForm } from "react-hook-form";
|
|||||||
// headless ui
|
// headless ui
|
||||||
import { Disclosure, Menu, Tab, Transition } from "@headlessui/react";
|
import { Disclosure, Menu, Tab, Transition } from "@headlessui/react";
|
||||||
// services
|
// services
|
||||||
import issuesServices from "lib/services/issues.services";
|
import issuesServices from "lib/services/issues.service";
|
||||||
import stateServices from "lib/services/state.services";
|
|
||||||
// fetch keys
|
// fetch keys
|
||||||
import {
|
import {
|
||||||
PROJECT_ISSUES_ACTIVITY,
|
PROJECT_ISSUES_ACTIVITY,
|
||||||
@ -22,8 +21,10 @@ import {
|
|||||||
} from "constants/fetch-keys";
|
} from "constants/fetch-keys";
|
||||||
// hooks
|
// hooks
|
||||||
import useUser from "lib/hooks/useUser";
|
import useUser from "lib/hooks/useUser";
|
||||||
|
// hoc
|
||||||
|
import withAuth from "lib/hoc/withAuthWrapper";
|
||||||
// layouts
|
// layouts
|
||||||
import AdminLayout from "layouts/AdminLayout";
|
import AppLayout from "layouts/AppLayout";
|
||||||
// components
|
// components
|
||||||
import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal";
|
import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal";
|
||||||
import IssueCommentSection from "components/project/issues/issue-detail/comment/IssueCommentSection";
|
import IssueCommentSection from "components/project/issues/issue-detail/comment/IssueCommentSection";
|
||||||
@ -38,7 +39,7 @@ import { Spinner, TextArea } from "ui";
|
|||||||
import HeaderButton from "ui/HeaderButton";
|
import HeaderButton from "ui/HeaderButton";
|
||||||
import { BreadcrumbItem, Breadcrumbs } from "ui/Breadcrumbs";
|
import { BreadcrumbItem, Breadcrumbs } from "ui/Breadcrumbs";
|
||||||
// types
|
// types
|
||||||
import { IIssue, IIssueComment, IssueResponse, IState } from "types";
|
import { IIssue, IIssueComment, IssueResponse } from "types";
|
||||||
// icons
|
// icons
|
||||||
import {
|
import {
|
||||||
ChevronLeftIcon,
|
ChevronLeftIcon,
|
||||||
@ -208,7 +209,7 @@ const IssueDetail: NextPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminLayout>
|
<AppLayout noPadding={true} bg="secondary">
|
||||||
<CreateUpdateIssuesModal
|
<CreateUpdateIssuesModal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
setIsOpen={setIsOpen}
|
setIsOpen={setIsOpen}
|
||||||
@ -222,46 +223,23 @@ const IssueDetail: NextPage = () => {
|
|||||||
setIsOpen={setIsAddAsSubIssueOpen}
|
setIsOpen={setIsAddAsSubIssueOpen}
|
||||||
parent={issueDetail}
|
parent={issueDetail}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex items-center justify-between w-full mb-5">
|
|
||||||
<Breadcrumbs>
|
|
||||||
<BreadcrumbItem
|
|
||||||
title={`${activeProject?.name ?? "Project"} Issues`}
|
|
||||||
link={`/projects/${activeProject?.id}/issues`}
|
|
||||||
/>
|
|
||||||
<BreadcrumbItem
|
|
||||||
title={`Issue ${activeProject?.identifier ?? "Project"}-${
|
|
||||||
issueDetail?.sequence_id ?? "..."
|
|
||||||
} Details`}
|
|
||||||
/>
|
|
||||||
</Breadcrumbs>
|
|
||||||
<div className="flex items-center gap-x-3">
|
|
||||||
<HeaderButton
|
|
||||||
Icon={ChevronLeftIcon}
|
|
||||||
label="Previous"
|
|
||||||
className={`${!prevIssue ? "cursor-not-allowed opacity-70" : ""}`}
|
|
||||||
onClick={() => {
|
|
||||||
if (!prevIssue) return;
|
|
||||||
router.push(`/projects/${prevIssue.project}/issues/${prevIssue.id}`);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<HeaderButton
|
|
||||||
Icon={ChevronRightIcon}
|
|
||||||
disabled={!nextIssue}
|
|
||||||
label="Next"
|
|
||||||
className={`${!nextIssue ? "cursor-not-allowed opacity-70" : ""}`}
|
|
||||||
onClick={() => {
|
|
||||||
if (!nextIssue) return;
|
|
||||||
router.push(`/projects/${nextIssue.project}/issues/${nextIssue?.id}`);
|
|
||||||
}}
|
|
||||||
position="reverse"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{issueDetail && activeProject ? (
|
{issueDetail && activeProject ? (
|
||||||
<div className="grid grid-cols-4 gap-5">
|
<div className="flex gap-5">
|
||||||
<div className="col-span-3 space-y-5">
|
<div className="basis-3/4 space-y-5 p-5">
|
||||||
<div className="bg-secondary rounded-lg p-4">
|
<div className="mb-5">
|
||||||
|
<Breadcrumbs>
|
||||||
|
<BreadcrumbItem
|
||||||
|
title={`${activeProject?.name ?? "Project"} Issues`}
|
||||||
|
link={`/projects/${activeProject?.id}/issues`}
|
||||||
|
/>
|
||||||
|
<BreadcrumbItem
|
||||||
|
title={`Issue ${activeProject?.identifier ?? "Project"}-${
|
||||||
|
issueDetail?.sequence_id ?? "..."
|
||||||
|
} Details`}
|
||||||
|
/>
|
||||||
|
</Breadcrumbs>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-lg">
|
||||||
{issueDetail.parent !== null && issueDetail.parent !== "" ? (
|
{issueDetail.parent !== null && issueDetail.parent !== "" ? (
|
||||||
<div className="bg-gray-100 flex items-center gap-2 p-2 text-xs rounded mb-5 w-min whitespace-nowrap">
|
<div className="bg-gray-100 flex items-center gap-2 p-2 text-xs rounded mb-5 w-min whitespace-nowrap">
|
||||||
<Link href={`/projects/${activeProject.id}/issues/${issueDetail.parent}`}>
|
<Link href={`/projects/${activeProject.id}/issues/${issueDetail.parent}`}>
|
||||||
@ -273,7 +251,8 @@ const IssueDetail: NextPage = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span className="flex-shrink-0 text-gray-600">
|
<span className="flex-shrink-0 text-gray-600">
|
||||||
{activeProject.identifier}-{issueDetail.sequence_id}
|
{activeProject.identifier}-
|
||||||
|
{issues?.results.find((i) => i.id === issueDetail.parent)?.sequence_id}
|
||||||
</span>
|
</span>
|
||||||
<span className="font-medium truncate">
|
<span className="font-medium truncate">
|
||||||
{issues?.results
|
{issues?.results
|
||||||
@ -365,7 +344,7 @@ const IssueDetail: NextPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
{subIssues && subIssues.length > 0 ? (
|
{subIssues && subIssues.length > 0 ? (
|
||||||
<Disclosure>
|
<Disclosure defaultOpen={true}>
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
@ -437,7 +416,7 @@ const IssueDetail: NextPage = () => {
|
|||||||
{subIssues.map((subIssue) => (
|
{subIssues.map((subIssue) => (
|
||||||
<div
|
<div
|
||||||
key={subIssue.id}
|
key={subIssue.id}
|
||||||
className="flex justify-between items-center gap-2 p-2 hover:bg-gray-100"
|
className="group flex justify-between items-center gap-2 rounded p-2 hover:bg-gray-100"
|
||||||
>
|
>
|
||||||
<Link href={`/projects/${activeProject.id}/issues/${subIssue.id}`}>
|
<Link href={`/projects/${activeProject.id}/issues/${subIssue.id}`}>
|
||||||
<a className="flex items-center gap-2 rounded text-xs">
|
<a className="flex items-center gap-2 rounded text-xs">
|
||||||
@ -455,9 +434,9 @@ const IssueDetail: NextPage = () => {
|
|||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
<div>
|
<div className="opacity-0 group-hover:opacity-100">
|
||||||
<Menu as="div" className="relative inline-block">
|
<Menu as="div" className="relative inline-block">
|
||||||
<Menu.Button className="grid relative place-items-center focus:outline-none">
|
<Menu.Button className="grid relative place-items-center hover:bg-gray-200 p-1 focus:outline-none">
|
||||||
<EllipsisHorizontalIcon className="h-4 w-4" />
|
<EllipsisHorizontalIcon className="h-4 w-4" />
|
||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
|
|
||||||
@ -553,7 +532,7 @@ const IssueDetail: NextPage = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-secondary rounded-lg p-4 space-y-5">
|
<div className="bg-secondary rounded-lg space-y-5">
|
||||||
<Tab.Group>
|
<Tab.Group>
|
||||||
<Tab.List className="flex gap-x-3">
|
<Tab.List className="flex gap-x-3">
|
||||||
{["Comments", "Activity"].map((item) => (
|
{["Comments", "Activity"].map((item) => (
|
||||||
@ -589,11 +568,34 @@ const IssueDetail: NextPage = () => {
|
|||||||
</Tab.Group>
|
</Tab.Group>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="sticky top-0 h-min bg-secondary p-4 rounded-lg">
|
<div className="basis-1/4 rounded-lg space-y-5 p-5 border-l">
|
||||||
|
<div className="flex justify-end items-center gap-x-3 mb-5">
|
||||||
|
<HeaderButton
|
||||||
|
Icon={ChevronLeftIcon}
|
||||||
|
label="Previous"
|
||||||
|
className={`${!prevIssue ? "cursor-not-allowed opacity-70" : ""}`}
|
||||||
|
onClick={() => {
|
||||||
|
if (!prevIssue) return;
|
||||||
|
router.push(`/projects/${prevIssue.project}/issues/${prevIssue.id}`);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<HeaderButton
|
||||||
|
Icon={ChevronRightIcon}
|
||||||
|
disabled={!nextIssue}
|
||||||
|
label="Next"
|
||||||
|
className={`${!nextIssue ? "cursor-not-allowed opacity-70" : ""}`}
|
||||||
|
onClick={() => {
|
||||||
|
if (!nextIssue) return;
|
||||||
|
router.push(`/projects/${nextIssue.project}/issues/${nextIssue?.id}`);
|
||||||
|
}}
|
||||||
|
position="reverse"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<IssueDetailSidebar
|
<IssueDetailSidebar
|
||||||
control={control}
|
control={control}
|
||||||
issueDetail={issueDetail}
|
issueDetail={issueDetail}
|
||||||
submitChanges={submitChanges}
|
submitChanges={submitChanges}
|
||||||
|
watch={watch}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -602,8 +604,8 @@ const IssueDetail: NextPage = () => {
|
|||||||
<Spinner />
|
<Spinner />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</AdminLayout>
|
</AppLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default IssueDetail;
|
export default withAuth(IssueDetail);
|
||||||
|
@ -18,7 +18,7 @@ import projectService from "lib/services/project.service";
|
|||||||
// commons
|
// commons
|
||||||
import { classNames, replaceUnderscoreIfSnakeCase } from "constants/common";
|
import { classNames, replaceUnderscoreIfSnakeCase } from "constants/common";
|
||||||
// layouts
|
// layouts
|
||||||
import AdminLayout from "layouts/AdminLayout";
|
import AppLayout from "layouts/AppLayout";
|
||||||
// hooks
|
// hooks
|
||||||
import useIssuesFilter from "lib/hooks/useIssuesFilter";
|
import useIssuesFilter from "lib/hooks/useIssuesFilter";
|
||||||
// components
|
// components
|
||||||
@ -34,7 +34,7 @@ import HeaderButton from "ui/HeaderButton";
|
|||||||
import { ChevronDownIcon, ListBulletIcon, RectangleStackIcon } from "@heroicons/react/24/outline";
|
import { ChevronDownIcon, ListBulletIcon, RectangleStackIcon } from "@heroicons/react/24/outline";
|
||||||
import { PlusIcon, Squares2X2Icon } from "@heroicons/react/20/solid";
|
import { PlusIcon, Squares2X2Icon } from "@heroicons/react/20/solid";
|
||||||
// types
|
// types
|
||||||
import type { IIssue, Properties, NestedKeyOf, ProjectMember } from "types";
|
import type { IIssue, Properties, NestedKeyOf } from "types";
|
||||||
|
|
||||||
const groupByOptions: Array<{ name: string; key: NestedKeyOf<IIssue> | null }> = [
|
const groupByOptions: Array<{ name: string; key: NestedKeyOf<IIssue> | null }> = [
|
||||||
{ name: "State", key: "state_detail.name" },
|
{ name: "State", key: "state_detail.name" },
|
||||||
@ -44,8 +44,8 @@ const groupByOptions: Array<{ name: string; key: NestedKeyOf<IIssue> | null }> =
|
|||||||
];
|
];
|
||||||
|
|
||||||
const orderByOptions: Array<{ name: string; key: NestedKeyOf<IIssue> }> = [
|
const orderByOptions: Array<{ name: string; key: NestedKeyOf<IIssue> }> = [
|
||||||
{ name: "Created", key: "created_at" },
|
{ name: "Last created", key: "created_at" },
|
||||||
{ name: "Update", key: "updated_at" },
|
{ name: "Last updated", key: "updated_at" },
|
||||||
{ name: "Priority", key: "priority" },
|
{ name: "Priority", key: "priority" },
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -86,11 +86,19 @@ const ProjectIssues: NextPage = () => {
|
|||||||
projectId as string
|
projectId as string
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: members } = useSWR<ProjectMember[]>(
|
const { data: members } = useSWR(
|
||||||
activeWorkspace && activeProject ? PROJECT_MEMBERS : null,
|
activeWorkspace && activeProject
|
||||||
|
? PROJECT_MEMBERS(activeWorkspace.slug, activeProject.id)
|
||||||
|
: null,
|
||||||
activeWorkspace && activeProject
|
activeWorkspace && activeProject
|
||||||
? () => projectService.projectMembers(activeWorkspace.slug, activeProject.id)
|
? () => projectService.projectMembers(activeWorkspace.slug, activeProject.id)
|
||||||
: null
|
: null,
|
||||||
|
{
|
||||||
|
onErrorRetry(err, _, __, revalidate, revalidateOpts) {
|
||||||
|
if (err?.status === 403) return;
|
||||||
|
setTimeout(() => revalidate(revalidateOpts), 5000);
|
||||||
|
},
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -115,7 +123,7 @@ const ProjectIssues: NextPage = () => {
|
|||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminLayout>
|
<AppLayout>
|
||||||
<CreateUpdateIssuesModal
|
<CreateUpdateIssuesModal
|
||||||
isOpen={isOpen && selectedIssue?.actionType !== "delete"}
|
isOpen={isOpen && selectedIssue?.actionType !== "delete"}
|
||||||
setIsOpen={setIsOpen}
|
setIsOpen={setIsOpen}
|
||||||
@ -173,7 +181,7 @@ const ProjectIssues: NextPage = () => {
|
|||||||
<Popover.Button
|
<Popover.Button
|
||||||
className={classNames(
|
className={classNames(
|
||||||
open ? "text-gray-900" : "text-gray-500",
|
open ? "text-gray-900" : "text-gray-500",
|
||||||
"group inline-flex items-center rounded-md bg-transparent text-base font-medium hover:text-gray-900 focus:outline-none border border-gray-300 px-3 py-1"
|
"group inline-flex items-center rounded-md bg-transparent text-xs font-medium hover:text-gray-900 focus:outline-none border border-gray-300 px-2 py-2"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span>View</span>
|
<span>View</span>
|
||||||
@ -195,85 +203,83 @@ const ProjectIssues: NextPage = () => {
|
|||||||
leaveFrom="opacity-100 translate-y-0"
|
leaveFrom="opacity-100 translate-y-0"
|
||||||
leaveTo="opacity-0 translate-y-1"
|
leaveTo="opacity-0 translate-y-1"
|
||||||
>
|
>
|
||||||
<Popover.Panel className="absolute mr-5 right-1/2 z-10 mt-3 w-screen max-w-xs translate-x-1/2 transform px-2 sm:px-0 bg-gray-0 backdrop-filter backdrop-blur-xl bg-opacity-100 rounded-lg shadow-lg overflow-hidden">
|
<Popover.Panel className="absolute mr-5 right-1/2 z-10 mt-1 w-screen max-w-xs translate-x-1/2 transform p-4 bg-white rounded-lg shadow-lg overflow-hidden">
|
||||||
<div className="overflow-hidden py-8 px-4">
|
<div className="relative flex flex-col gap-1 gap-y-4">
|
||||||
<div className="relative flex flex-col gap-1 gap-y-4">
|
<div className="flex justify-between">
|
||||||
<div className="flex justify-between">
|
<h4 className="text-base text-gray-600">Group by</h4>
|
||||||
<h4 className="text-base text-gray-600">Group by</h4>
|
<CustomMenu
|
||||||
<CustomMenu
|
label={
|
||||||
label={
|
groupByOptions.find((option) => option.key === groupByProperty)
|
||||||
groupByOptions.find((option) => option.key === groupByProperty)
|
?.name ?? "Select"
|
||||||
?.name ?? "Select"
|
}
|
||||||
}
|
>
|
||||||
>
|
{groupByOptions.map((option) => (
|
||||||
{groupByOptions.map((option) => (
|
<CustomMenu.MenuItem
|
||||||
|
key={option.key}
|
||||||
|
onClick={() => setGroupByProperty(option.key)}
|
||||||
|
>
|
||||||
|
{option.name}
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
))}
|
||||||
|
</CustomMenu>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<h4 className="text-base text-gray-600">Order by</h4>
|
||||||
|
<CustomMenu
|
||||||
|
label={
|
||||||
|
orderByOptions.find((option) => option.key === orderBy)?.name ??
|
||||||
|
"Select"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{orderByOptions.map((option) =>
|
||||||
|
groupByProperty === "priority" &&
|
||||||
|
option.key === "priority" ? null : (
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
key={option.key}
|
key={option.key}
|
||||||
onClick={() => setGroupByProperty(option.key)}
|
onClick={() => setOrderBy(option.key)}
|
||||||
>
|
>
|
||||||
{option.name}
|
{option.name}
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
))}
|
)
|
||||||
</CustomMenu>
|
)}
|
||||||
</div>
|
</CustomMenu>
|
||||||
<div className="flex justify-between">
|
</div>
|
||||||
<h4 className="text-base text-gray-600">Order by</h4>
|
<div className="flex justify-between">
|
||||||
<CustomMenu
|
<h4 className="text-base text-gray-600">Issue type</h4>
|
||||||
label={
|
<CustomMenu
|
||||||
orderByOptions.find((option) => option.key === orderBy)?.name ??
|
label={
|
||||||
"Select"
|
filterIssueOptions.find((option) => option.key === filterIssue)
|
||||||
}
|
?.name ?? "Select"
|
||||||
>
|
}
|
||||||
{orderByOptions.map((option) =>
|
>
|
||||||
groupByProperty === "priority" &&
|
{filterIssueOptions.map((option) => (
|
||||||
option.key === "priority" ? null : (
|
<CustomMenu.MenuItem
|
||||||
<CustomMenu.MenuItem
|
key={option.key}
|
||||||
key={option.key}
|
onClick={() => setFilterIssue(option.key)}
|
||||||
onClick={() => setOrderBy(option.key)}
|
>
|
||||||
>
|
{option.name}
|
||||||
{option.name}
|
</CustomMenu.MenuItem>
|
||||||
</CustomMenu.MenuItem>
|
))}
|
||||||
)
|
</CustomMenu>
|
||||||
)}
|
</div>
|
||||||
</CustomMenu>
|
<div className="border-b-2"></div>
|
||||||
</div>
|
<div className="relative flex flex-col gap-1">
|
||||||
<div className="flex justify-between">
|
<h4 className="text-base text-gray-600">Properties</h4>
|
||||||
<h4 className="text-base text-gray-600">Issue type</h4>
|
<div>
|
||||||
<CustomMenu
|
{Object.keys(properties).map((key) => (
|
||||||
label={
|
<button
|
||||||
filterIssueOptions.find((option) => option.key === filterIssue)
|
key={key}
|
||||||
?.name ?? "Select"
|
type="button"
|
||||||
}
|
className={`px-2 py-1 inline capitalize rounded border border-indigo-600 text-sm m-1 ${
|
||||||
>
|
properties[key as keyof Properties]
|
||||||
{filterIssueOptions.map((option) => (
|
? "border-indigo-600 bg-indigo-600 text-white"
|
||||||
<CustomMenu.MenuItem
|
: ""
|
||||||
key={option.key}
|
}`}
|
||||||
onClick={() => setFilterIssue(option.key)}
|
onClick={() => setProperties(key as keyof Properties)}
|
||||||
>
|
>
|
||||||
{option.name}
|
{replaceUnderscoreIfSnakeCase(key)}
|
||||||
</CustomMenu.MenuItem>
|
</button>
|
||||||
))}
|
))}
|
||||||
</CustomMenu>
|
|
||||||
</div>
|
|
||||||
<div className="border-b-2"></div>
|
|
||||||
<div className="relative flex flex-col gap-1">
|
|
||||||
<h4 className="text-base text-gray-600">Properties</h4>
|
|
||||||
<div>
|
|
||||||
{Object.keys(properties).map((key) => (
|
|
||||||
<button
|
|
||||||
key={key}
|
|
||||||
type="button"
|
|
||||||
className={`px-2 py-1 inline capitalize rounded border border-indigo-600 text-sm m-1 ${
|
|
||||||
properties[key as keyof Properties]
|
|
||||||
? "border-indigo-600 bg-indigo-600 text-white"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
onClick={() => setProperties(key as keyof Properties)}
|
|
||||||
>
|
|
||||||
{replaceUnderscoreIfSnakeCase(key)}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -336,7 +342,7 @@ const ProjectIssues: NextPage = () => {
|
|||||||
</EmptySpace>
|
</EmptySpace>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</AdminLayout>
|
</AppLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -14,8 +14,10 @@ import useUser from "lib/hooks/useUser";
|
|||||||
import useToast from "lib/hooks/useToast";
|
import useToast from "lib/hooks/useToast";
|
||||||
// fetching keys
|
// fetching keys
|
||||||
import { PROJECT_MEMBERS, PROJECT_INVITATIONS } from "constants/fetch-keys";
|
import { PROJECT_MEMBERS, PROJECT_INVITATIONS } from "constants/fetch-keys";
|
||||||
|
// hoc
|
||||||
|
import withAuth from "lib/hoc/withAuthWrapper";
|
||||||
// layouts
|
// layouts
|
||||||
import AdminLayout from "layouts/AdminLayout";
|
import AppLayout from "layouts/AppLayout";
|
||||||
// components
|
// components
|
||||||
import SendProjectInvitationModal from "components/project/SendProjectInvitationModal";
|
import SendProjectInvitationModal from "components/project/SendProjectInvitationModal";
|
||||||
import ConfirmProjectMemberRemove from "components/project/ConfirmProjectMemberRemove";
|
import ConfirmProjectMemberRemove from "components/project/ConfirmProjectMemberRemove";
|
||||||
@ -53,7 +55,13 @@ const ProjectMembers: NextPage = () => {
|
|||||||
activeWorkspace && projectId ? PROJECT_MEMBERS(projectId as string) : null,
|
activeWorkspace && projectId ? PROJECT_MEMBERS(projectId as string) : null,
|
||||||
activeWorkspace && projectId
|
activeWorkspace && projectId
|
||||||
? () => projectService.projectMembers(activeWorkspace.slug, projectId as any)
|
? () => projectService.projectMembers(activeWorkspace.slug, projectId as any)
|
||||||
: null
|
: null,
|
||||||
|
{
|
||||||
|
onErrorRetry(err, _, __, revalidate, revalidateOpts) {
|
||||||
|
if (err?.status === 403) return;
|
||||||
|
setTimeout(() => revalidate(revalidateOpts), 5000);
|
||||||
|
},
|
||||||
|
}
|
||||||
);
|
);
|
||||||
const { data: projectInvitations, mutate: mutateInvitations } = useSWR(
|
const { data: projectInvitations, mutate: mutateInvitations } = useSWR(
|
||||||
activeWorkspace && projectId ? PROJECT_INVITATIONS : null,
|
activeWorkspace && projectId ? PROJECT_INVITATIONS : null,
|
||||||
@ -84,7 +92,7 @@ const ProjectMembers: NextPage = () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminLayout>
|
<AppLayout>
|
||||||
<ConfirmProjectMemberRemove
|
<ConfirmProjectMemberRemove
|
||||||
isOpen={Boolean(selectedRemoveMember) || Boolean(selectedInviteRemoveMember)}
|
isOpen={Boolean(selectedRemoveMember) || Boolean(selectedInviteRemoveMember)}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
@ -103,8 +111,7 @@ const ProjectMembers: NextPage = () => {
|
|||||||
selectedRemoveMember
|
selectedRemoveMember
|
||||||
);
|
);
|
||||||
mutateMembers(
|
mutateMembers(
|
||||||
(prevData: any[]) =>
|
(prevData) => prevData?.filter((item: any) => item.id !== selectedRemoveMember),
|
||||||
prevData?.filter((item: any) => item.id !== selectedRemoveMember),
|
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -115,8 +122,7 @@ const ProjectMembers: NextPage = () => {
|
|||||||
selectedInviteRemoveMember
|
selectedInviteRemoveMember
|
||||||
);
|
);
|
||||||
mutateInvitations(
|
mutateInvitations(
|
||||||
(prevData: any[]) =>
|
(prevData) => prevData?.filter((item: any) => item.id !== selectedInviteRemoveMember),
|
||||||
prevData?.filter((item: any) => item.id !== selectedInviteRemoveMember),
|
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -235,7 +241,7 @@ const ProjectMembers: NextPage = () => {
|
|||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500 sm:pl-6">
|
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500 sm:pl-6">
|
||||||
{member.status ? (
|
{member.member ? (
|
||||||
<span className="p-0.5 px-2 text-sm bg-green-700 text-white rounded-full">
|
<span className="p-0.5 px-2 text-sm bg-green-700 text-white rounded-full">
|
||||||
Active
|
Active
|
||||||
</span>
|
</span>
|
||||||
@ -261,7 +267,7 @@ const ProjectMembers: NextPage = () => {
|
|||||||
className="w-full text-left py-2 pl-2"
|
className="w-full text-left py-2 pl-2"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!member.status) {
|
if (!member.member) {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
message: "You can't edit a pending invitation.",
|
message: "You can't edit a pending invitation.",
|
||||||
@ -282,7 +288,7 @@ const ProjectMembers: NextPage = () => {
|
|||||||
className="w-full text-left py-2 pl-2"
|
className="w-full text-left py-2 pl-2"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (member.status) {
|
if (member.member) {
|
||||||
setSelectedRemoveMember(member.id);
|
setSelectedRemoveMember(member.id);
|
||||||
} else {
|
} else {
|
||||||
setSelectedInviteRemoveMember(member.id);
|
setSelectedInviteRemoveMember(member.id);
|
||||||
@ -303,8 +309,8 @@ const ProjectMembers: NextPage = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</AdminLayout>
|
</AppLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ProjectMembers;
|
export default withAuth(ProjectMembers);
|
||||||
|
@ -1,52 +1,60 @@
|
|||||||
import React, { useEffect, useCallback, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
// next
|
// next
|
||||||
import type { NextPage } from "next";
|
import type { NextPage } from "next";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
// swr
|
// swr
|
||||||
import { mutate } from "swr";
|
import useSWR, { mutate } from "swr";
|
||||||
// swr
|
|
||||||
import useSWR from "swr";
|
|
||||||
// react hook form
|
// react hook form
|
||||||
import { useForm, Controller } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
// headless ui
|
// headless ui
|
||||||
import { Listbox, Tab, Transition } from "@headlessui/react";
|
import { Tab } from "@headlessui/react";
|
||||||
|
// hoc
|
||||||
|
import withAuth from "lib/hoc/withAuthWrapper";
|
||||||
// layouts
|
// layouts
|
||||||
import AdminLayout from "layouts/AdminLayout";
|
import AppLayout from "layouts/AppLayout";
|
||||||
// service
|
// service
|
||||||
import projectServices from "lib/services/project.service";
|
import projectServices from "lib/services/project.service";
|
||||||
import workspaceService from "lib/services/workspace.service";
|
|
||||||
// hooks
|
// hooks
|
||||||
import useUser from "lib/hooks/useUser";
|
import useUser from "lib/hooks/useUser";
|
||||||
import useToast from "lib/hooks/useToast";
|
import useToast from "lib/hooks/useToast";
|
||||||
// fetch keys
|
// fetch keys
|
||||||
import { PROJECT_DETAILS, PROJECTS_LIST, WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
import { PROJECT_DETAILS, PROJECTS_LIST } from "constants/fetch-keys";
|
||||||
// commons
|
|
||||||
import { addSpaceIfCamelCase, debounce } from "constants/common";
|
|
||||||
// components
|
|
||||||
import CreateUpdateStateModal from "components/project/issues/BoardView/state/CreateUpdateStateModal";
|
|
||||||
// ui
|
// ui
|
||||||
import { Spinner, Button, Input, TextArea, Select } from "ui";
|
import { Spinner } from "ui";
|
||||||
import { Breadcrumbs, BreadcrumbItem } from "ui/Breadcrumbs";
|
import { Breadcrumbs, BreadcrumbItem } from "ui/Breadcrumbs";
|
||||||
// icons
|
|
||||||
import {
|
|
||||||
ChevronDownIcon,
|
|
||||||
CheckIcon,
|
|
||||||
PlusIcon,
|
|
||||||
PencilSquareIcon,
|
|
||||||
RectangleGroupIcon,
|
|
||||||
PencilIcon,
|
|
||||||
} from "@heroicons/react/24/outline";
|
|
||||||
// types
|
// types
|
||||||
import type { IProject, IWorkspace, WorkspaceMember } from "types";
|
import type { IProject, IWorkspace } from "types";
|
||||||
|
|
||||||
const defaultValues: Partial<IProject> = {
|
const defaultValues: Partial<IProject> = {
|
||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
|
identifier: "",
|
||||||
|
network: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const NETWORK_CHOICES = { "0": "Secret", "2": "Public" };
|
|
||||||
|
|
||||||
const ProjectSettings: NextPage = () => {
|
const ProjectSettings: NextPage = () => {
|
||||||
|
// FIXME: instead of using dynamic import inside component use it outside
|
||||||
|
const GeneralSettings = dynamic(() => import("components/project/settings/GeneralSettings"), {
|
||||||
|
loading: () => <p>Loading...</p>,
|
||||||
|
ssr: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const ControlSettings = dynamic(() => import("components/project/settings/ControlSettings"), {
|
||||||
|
loading: () => <p>Loading...</p>,
|
||||||
|
ssr: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const StatesSettings = dynamic(() => import("components/project/settings/StatesSettings"), {
|
||||||
|
loading: () => <p>Loading...</p>,
|
||||||
|
ssr: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const LabelsSettings = dynamic(() => import("components/project/settings/LabelsSettings"), {
|
||||||
|
loading: () => <p>Loading...</p>,
|
||||||
|
ssr: false,
|
||||||
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@ -58,30 +66,21 @@ const ProjectSettings: NextPage = () => {
|
|||||||
defaultValues,
|
defaultValues,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [isCreateStateModalOpen, setIsCreateStateModalOpen] = useState(false);
|
|
||||||
const [selectedState, setSelectedState] = useState<string | undefined>();
|
|
||||||
const [newGroupForm, setNewGroupForm] = useState(false);
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { projectId } = router.query;
|
const { projectId } = router.query;
|
||||||
|
|
||||||
const { activeWorkspace, activeProject, states } = useUser();
|
const { activeWorkspace, activeProject } = useUser();
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const { data: projectDetails } = useSWR<IProject>(
|
const { data: projectDetails } = useSWR<IProject>(
|
||||||
activeWorkspace && projectId ? PROJECT_DETAILS : null,
|
activeWorkspace && projectId ? PROJECT_DETAILS(projectId as string) : null,
|
||||||
activeWorkspace
|
activeWorkspace
|
||||||
? () => projectServices.getProject(activeWorkspace.slug, projectId as string)
|
? () => projectServices.getProject(activeWorkspace.slug, projectId as string)
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: people } = useSWR<WorkspaceMember[]>(
|
|
||||||
activeWorkspace ? WORKSPACE_MEMBERS : null,
|
|
||||||
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
projectDetails &&
|
projectDetails &&
|
||||||
reset({
|
reset({
|
||||||
@ -93,7 +92,7 @@ const ProjectSettings: NextPage = () => {
|
|||||||
}, [projectDetails, reset]);
|
}, [projectDetails, reset]);
|
||||||
|
|
||||||
const onSubmit = async (formData: IProject) => {
|
const onSubmit = async (formData: IProject) => {
|
||||||
if (!activeWorkspace) return;
|
if (!activeWorkspace || !projectId) return;
|
||||||
const payload: Partial<IProject> = {
|
const payload: Partial<IProject> = {
|
||||||
name: formData.name,
|
name: formData.name,
|
||||||
network: formData.network,
|
network: formData.network,
|
||||||
@ -105,7 +104,11 @@ const ProjectSettings: NextPage = () => {
|
|||||||
await projectServices
|
await projectServices
|
||||||
.updateProject(activeWorkspace.slug, projectId as string, payload)
|
.updateProject(activeWorkspace.slug, projectId as string, payload)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
mutate<IProject>(PROJECT_DETAILS, (prevData) => ({ ...prevData, ...res }), false);
|
mutate<IProject>(
|
||||||
|
PROJECT_DETAILS(projectId as string),
|
||||||
|
(prevData) => ({ ...prevData, ...res }),
|
||||||
|
false
|
||||||
|
);
|
||||||
mutate<IProject[]>(
|
mutate<IProject[]>(
|
||||||
PROJECTS_LIST(activeWorkspace.slug),
|
PROJECTS_LIST(activeWorkspace.slug),
|
||||||
(prevData) => {
|
(prevData) => {
|
||||||
@ -130,28 +133,9 @@ const ProjectSettings: NextPage = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkIdentifier = (slug: string, value: string) => {
|
|
||||||
projectServices.checkProjectIdentifierAvailability(slug, value).then((response) => {
|
|
||||||
console.log(response);
|
|
||||||
if (response.exists) setError("identifier", { message: "Identifier already exists" });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
const checkIdentifierAvailability = useCallback(debounce(checkIdentifier, 1500), []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminLayout>
|
<AppLayout>
|
||||||
<div className="space-y-5 mb-5">
|
<div className="space-y-5 mb-5">
|
||||||
<CreateUpdateStateModal
|
|
||||||
isOpen={isCreateStateModalOpen || Boolean(selectedState)}
|
|
||||||
handleClose={() => {
|
|
||||||
setSelectedState(undefined);
|
|
||||||
setIsCreateStateModalOpen(false);
|
|
||||||
}}
|
|
||||||
projectId={projectId as string}
|
|
||||||
data={selectedState ? states?.find((state) => state.id === selectedState) : undefined}
|
|
||||||
/>
|
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
<BreadcrumbItem title="Projects" link="/projects" />
|
<BreadcrumbItem title="Projects" link="/projects" />
|
||||||
<BreadcrumbItem title={`${activeProject?.name ?? "Project"} Settings`} />
|
<BreadcrumbItem title={`${activeProject?.name ?? "Project"} Settings`} />
|
||||||
@ -159,386 +143,51 @@ const ProjectSettings: NextPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
{projectDetails ? (
|
{projectDetails ? (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="mt-3">
|
<Tab.Group>
|
||||||
<Tab.Group>
|
<Tab.List className="flex items-center gap-x-4 gap-y-2 flex-wrap mb-8">
|
||||||
<Tab.List className="flex items-center gap-x-4 gap-y-2 flex-wrap">
|
{["General", "Control", "States", "Labels"].map((tab, index) => (
|
||||||
{["General", "Control", "States", "Labels"].map((tab, index) => (
|
<Tab
|
||||||
<Tab
|
key={index}
|
||||||
key={index}
|
className={({ selected }) =>
|
||||||
className={({ selected }) =>
|
`px-6 py-2 border-2 border-theme hover:bg-theme hover:text-white text-xs font-medium rounded-md outline-none duration-300 ${
|
||||||
`px-6 py-2 border-2 border-theme hover:bg-theme hover:text-white text-xs font-medium rounded-md duration-300 ${
|
selected ? "bg-theme text-white" : ""
|
||||||
selected ? "bg-theme text-white" : ""
|
}`
|
||||||
}`
|
}
|
||||||
}
|
>
|
||||||
>
|
{tab}
|
||||||
{tab}
|
</Tab>
|
||||||
</Tab>
|
))}
|
||||||
))}
|
</Tab.List>
|
||||||
</Tab.List>
|
<Tab.Panels>
|
||||||
<Tab.Panels className="mt-8">
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<Tab.Panel>
|
<Tab.Panel>
|
||||||
<section className="space-y-5">
|
<GeneralSettings
|
||||||
<div>
|
register={register}
|
||||||
<h3 className="text-lg font-medium leading-6 text-gray-900">General</h3>
|
errors={errors}
|
||||||
<p className="mt-1 text-sm text-gray-500">
|
setError={setError}
|
||||||
This information will be displayed to every member of the project.
|
isSubmitting={isSubmitting}
|
||||||
</p>
|
/>
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-4 gap-3">
|
|
||||||
<div className="col-span-2">
|
|
||||||
<Input
|
|
||||||
id="name"
|
|
||||||
name="name"
|
|
||||||
error={errors.name}
|
|
||||||
register={register}
|
|
||||||
placeholder="Project Name"
|
|
||||||
label="Name"
|
|
||||||
validations={{
|
|
||||||
required: "Name is required",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Select
|
|
||||||
name="network"
|
|
||||||
id="network"
|
|
||||||
options={Object.keys(NETWORK_CHOICES).map((key) => ({
|
|
||||||
value: key,
|
|
||||||
label: NETWORK_CHOICES[key as keyof typeof NETWORK_CHOICES],
|
|
||||||
}))}
|
|
||||||
label="Network"
|
|
||||||
register={register}
|
|
||||||
validations={{
|
|
||||||
required: "Network is required",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Input
|
|
||||||
id="identifier"
|
|
||||||
name="identifier"
|
|
||||||
error={errors.identifier}
|
|
||||||
register={register}
|
|
||||||
placeholder="Enter identifier"
|
|
||||||
label="Identifier"
|
|
||||||
onChange={(e: any) => {
|
|
||||||
if (!activeWorkspace || !e.target.value) return;
|
|
||||||
checkIdentifierAvailability(activeWorkspace.slug, e.target.value);
|
|
||||||
}}
|
|
||||||
validations={{
|
|
||||||
required: "Identifier is required",
|
|
||||||
minLength: {
|
|
||||||
value: 1,
|
|
||||||
message: "Identifier must at least be of 1 character",
|
|
||||||
},
|
|
||||||
maxLength: {
|
|
||||||
value: 9,
|
|
||||||
message: "Identifier must at most be of 9 characters",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<TextArea
|
|
||||||
id="description"
|
|
||||||
name="description"
|
|
||||||
error={errors.description}
|
|
||||||
register={register}
|
|
||||||
label="Description"
|
|
||||||
placeholder="Enter project description"
|
|
||||||
validations={{
|
|
||||||
required: "Description is required",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-end">
|
|
||||||
<Button type="submit" disabled={isSubmitting}>
|
|
||||||
{isSubmitting ? "Updating Project..." : "Update Project"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
<Tab.Panel>
|
<Tab.Panel>
|
||||||
<section className="space-y-5">
|
<ControlSettings control={control} isSubmitting={isSubmitting} />
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-medium leading-6 text-gray-900">Control</h3>
|
|
||||||
<p className="mt-1 text-sm text-gray-500">Set the control for the project.</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between gap-3">
|
|
||||||
<div className="w-full md:w-1/2">
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="project_lead"
|
|
||||||
render={({ field: { onChange, value } }) => (
|
|
||||||
<Listbox value={value} onChange={onChange}>
|
|
||||||
{({ open }) => (
|
|
||||||
<>
|
|
||||||
<Listbox.Label>
|
|
||||||
<div className="text-gray-500 mb-2">Project Lead</div>
|
|
||||||
</Listbox.Label>
|
|
||||||
<div className="relative">
|
|
||||||
<Listbox.Button className="bg-white relative w-full border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
|
||||||
<span className="block truncate">
|
|
||||||
{people?.find((person) => person.member.id === value)
|
|
||||||
?.member.first_name ?? "Select Lead"}
|
|
||||||
</span>
|
|
||||||
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
|
||||||
<ChevronDownIcon
|
|
||||||
className="h-5 w-5 text-gray-400"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</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 w-full bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
|
|
||||||
{people?.map((person) => (
|
|
||||||
<Listbox.Option
|
|
||||||
key={person.id}
|
|
||||||
className={({ active }) =>
|
|
||||||
`${
|
|
||||||
active ? "text-white bg-theme" : "text-gray-900"
|
|
||||||
} cursor-default select-none relative py-2 pl-3 pr-9`
|
|
||||||
}
|
|
||||||
value={person.member.id}
|
|
||||||
>
|
|
||||||
{({ selected, active }) => (
|
|
||||||
<>
|
|
||||||
<span
|
|
||||||
className={`${
|
|
||||||
selected ? "font-semibold" : "font-normal"
|
|
||||||
} block truncate`}
|
|
||||||
>
|
|
||||||
{person.member.first_name}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{selected ? (
|
|
||||||
<span
|
|
||||||
className={`absolute inset-y-0 right-0 flex items-center pr-4 ${
|
|
||||||
active ? "text-white" : "text-indigo-600"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<CheckIcon
|
|
||||||
className="h-5 w-5"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Listbox.Option>
|
|
||||||
))}
|
|
||||||
</Listbox.Options>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Listbox>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="w-full md:w-1/2">
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="default_assignee"
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<Listbox value={value} onChange={onChange}>
|
|
||||||
{({ open }) => (
|
|
||||||
<>
|
|
||||||
<Listbox.Label>
|
|
||||||
<div className="text-gray-500 mb-2">Default Assignee</div>
|
|
||||||
</Listbox.Label>
|
|
||||||
<div className="relative">
|
|
||||||
<Listbox.Button className="bg-white relative w-full border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
|
||||||
<span className="block truncate">
|
|
||||||
{people?.find((p) => p.member.id === value)?.member
|
|
||||||
.first_name ?? "Select Default Assignee"}
|
|
||||||
</span>
|
|
||||||
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
|
||||||
<ChevronDownIcon
|
|
||||||
className="h-5 w-5 text-gray-400"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</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 w-full bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
|
|
||||||
{people?.map((person) => (
|
|
||||||
<Listbox.Option
|
|
||||||
key={person.id}
|
|
||||||
className={({ active }) =>
|
|
||||||
`${
|
|
||||||
active ? "text-white bg-theme" : "text-gray-900"
|
|
||||||
} cursor-default select-none relative py-2 pl-3 pr-9`
|
|
||||||
}
|
|
||||||
value={person.member.id}
|
|
||||||
>
|
|
||||||
{({ selected, active }) => (
|
|
||||||
<>
|
|
||||||
<span
|
|
||||||
className={`${
|
|
||||||
selected ? "font-semibold" : "font-normal"
|
|
||||||
} block truncate`}
|
|
||||||
>
|
|
||||||
{person.member.first_name}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{selected ? (
|
|
||||||
<span
|
|
||||||
className={`absolute inset-y-0 right-0 flex items-center pr-4 ${
|
|
||||||
active ? "text-white" : "text-indigo-600"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<CheckIcon
|
|
||||||
className="h-5 w-5"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Listbox.Option>
|
|
||||||
))}
|
|
||||||
</Listbox.Options>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Listbox>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-end">
|
|
||||||
<Button type="submit" disabled={isSubmitting}>
|
|
||||||
{isSubmitting ? "Updating Project..." : "Update Project"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
<Tab.Panel>
|
</form>
|
||||||
<section className="space-y-5">
|
<Tab.Panel>
|
||||||
<div>
|
<StatesSettings projectId={projectId} />
|
||||||
<h3 className="text-lg font-medium leading-6 text-gray-900">State</h3>
|
</Tab.Panel>
|
||||||
<p className="mt-1 text-sm text-gray-500">
|
<Tab.Panel>
|
||||||
Manage the state of this project.
|
<LabelsSettings />
|
||||||
</p>
|
</Tab.Panel>
|
||||||
</div>
|
</Tab.Panels>
|
||||||
<div className="flex justify-between gap-3">
|
</Tab.Group>
|
||||||
<div className="w-full space-y-5">
|
|
||||||
{states?.map((state) => (
|
|
||||||
<div
|
|
||||||
key={state.id}
|
|
||||||
className="bg-white px-4 py-2 rounded flex justify-between items-center"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-x-2">
|
|
||||||
<div
|
|
||||||
className="w-3 h-3 rounded-full"
|
|
||||||
style={{
|
|
||||||
backgroundColor: state.color,
|
|
||||||
}}
|
|
||||||
></div>
|
|
||||||
<h4>{addSpaceIfCamelCase(state.name)}</h4>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<button type="button" onClick={() => setSelectedState(state.id)}>
|
|
||||||
<PencilSquareIcon className="h-5 w-5 text-gray-400" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
className="flex items-center gap-x-1"
|
|
||||||
onClick={() => setIsCreateStateModalOpen(true)}
|
|
||||||
>
|
|
||||||
<PlusIcon className="h-4 w-4" />
|
|
||||||
<span>Add State</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</Tab.Panel>
|
|
||||||
<Tab.Panel>
|
|
||||||
<section className="space-y-5">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-medium leading-6 text-gray-900">Labels</h3>
|
|
||||||
<p className="mt-1 text-sm text-gray-500">
|
|
||||||
Manage the labels of this project.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
className="flex items-center gap-x-1"
|
|
||||||
onClick={() => setNewGroupForm(true)}
|
|
||||||
>
|
|
||||||
<PlusIcon className="h-4 w-4" />
|
|
||||||
New group
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-5">
|
|
||||||
<div
|
|
||||||
className={`bg-white px-4 py-2 flex items-center gap-2 ${
|
|
||||||
newGroupForm ? "" : "hidden"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Input type="text" name="groupName" />
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
theme="secondary"
|
|
||||||
onClick={() => setNewGroupForm(false)}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button type="button">Save</Button>
|
|
||||||
</div>
|
|
||||||
{["", ""].map((group, index) => (
|
|
||||||
<div key={index} className="bg-white p-4 text-gray-900 rounded-md">
|
|
||||||
<h3 className="font-medium leading-5 flex items-center gap-2">
|
|
||||||
<RectangleGroupIcon className="h-5 w-5" />
|
|
||||||
This is the label group title
|
|
||||||
</h3>
|
|
||||||
<div className="pl-5 mt-4">
|
|
||||||
<div className="group text-sm flex justify-between items-center p-2 hover:bg-gray-100 rounded">
|
|
||||||
<h5 className="flex items-center gap-2">
|
|
||||||
<div className="w-2 h-2 bg-red-600 rounded-full"></div>
|
|
||||||
This is the label title
|
|
||||||
</h5>
|
|
||||||
<div className="hidden group-hover:block">
|
|
||||||
<PencilIcon className="h-3 w-3" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</Tab.Panel>
|
|
||||||
</Tab.Panels>
|
|
||||||
</Tab.Group>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="h-full w-full flex justify-center items-center">
|
<div className="h-full w-full flex justify-center items-center">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</AdminLayout>
|
</AppLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ProjectSettings;
|
export default withAuth(ProjectSettings);
|
||||||
|
@ -1,28 +1,32 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useState } from "react";
|
||||||
// next
|
// next
|
||||||
import type { NextPage } from "next";
|
import type { NextPage } from "next";
|
||||||
// hooks
|
// hooks
|
||||||
import useUser from "lib/hooks/useUser";
|
import useUser from "lib/hooks/useUser";
|
||||||
|
// hoc
|
||||||
|
import withAuth from "lib/hoc/withAuthWrapper";
|
||||||
// layouts
|
// layouts
|
||||||
import AdminLayout from "layouts/AdminLayout";
|
import AppLayout from "layouts/AppLayout";
|
||||||
// components
|
// components
|
||||||
import CreateProjectModal from "components/project/CreateProjectModal";
|
import ProjectMemberInvitations from "components/project/memberInvitations";
|
||||||
import ConfirmProjectDeletion from "components/project/ConfirmProjectDeletion";
|
import ConfirmProjectDeletion from "components/project/confirm-project-deletion";
|
||||||
// ui
|
// ui
|
||||||
import { Button, Spinner } from "ui";
|
import {
|
||||||
// types
|
Button,
|
||||||
import { IProject } from "types";
|
Spinner,
|
||||||
|
HeaderButton,
|
||||||
|
Breadcrumbs,
|
||||||
|
BreadcrumbItem,
|
||||||
|
EmptySpace,
|
||||||
|
EmptySpaceItem,
|
||||||
|
} from "ui";
|
||||||
// services
|
// services
|
||||||
import projectService from "lib/services/project.service";
|
import projectService from "lib/services/project.service";
|
||||||
import ProjectMemberInvitations from "components/project/memberInvitations";
|
// icons
|
||||||
import { ClipboardDocumentListIcon, PlusIcon } from "@heroicons/react/24/outline";
|
import { ClipboardDocumentListIcon, PlusIcon } from "@heroicons/react/24/outline";
|
||||||
import { BreadcrumbItem, Breadcrumbs } from "ui/Breadcrumbs";
|
|
||||||
import { EmptySpace, EmptySpaceItem } from "ui/EmptySpace";
|
|
||||||
import HeaderButton from "ui/HeaderButton";
|
|
||||||
|
|
||||||
const Projects: NextPage = () => {
|
const Projects: NextPage = () => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [deleteProject, setDeleteProject] = useState<string | null>(null);
|
||||||
const [deleteProject, setDeleteProject] = useState<IProject | undefined>();
|
|
||||||
const [invitationsRespond, setInvitationsRespond] = useState<string[]>([]);
|
const [invitationsRespond, setInvitationsRespond] = useState<string[]>([]);
|
||||||
|
|
||||||
const { projects, activeWorkspace, mutateProjects } = useUser();
|
const { projects, activeWorkspace, mutateProjects } = useUser();
|
||||||
@ -52,21 +56,12 @@ const Projects: NextPage = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isOpen) return;
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
setDeleteProject(undefined);
|
|
||||||
clearTimeout(timer);
|
|
||||||
}, 300);
|
|
||||||
}, [isOpen]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminLayout>
|
<AppLayout>
|
||||||
<CreateProjectModal isOpen={isOpen && !deleteProject} setIsOpen={setIsOpen} />
|
|
||||||
<ConfirmProjectDeletion
|
<ConfirmProjectDeletion
|
||||||
isOpen={isOpen && !!deleteProject}
|
isOpen={!!deleteProject}
|
||||||
setIsOpen={setIsOpen}
|
onClose={() => setDeleteProject(null)}
|
||||||
data={deleteProject}
|
data={projects?.find((item) => item.id === deleteProject) ?? null}
|
||||||
/>
|
/>
|
||||||
{projects ? (
|
{projects ? (
|
||||||
<>
|
<>
|
||||||
@ -87,7 +82,10 @@ const Projects: NextPage = () => {
|
|||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
Icon={PlusIcon}
|
Icon={PlusIcon}
|
||||||
action={() => setIsOpen(true)}
|
action={() => {
|
||||||
|
const e = new KeyboardEvent("keydown", { key: "p", ctrlKey: true });
|
||||||
|
document.dispatchEvent(e);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</EmptySpace>
|
</EmptySpace>
|
||||||
</div>
|
</div>
|
||||||
@ -98,7 +96,14 @@ const Projects: NextPage = () => {
|
|||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
<div className="flex items-center justify-between cursor-pointer w-full">
|
<div className="flex items-center justify-between cursor-pointer w-full">
|
||||||
<h2 className="text-2xl font-medium">Projects</h2>
|
<h2 className="text-2xl font-medium">Projects</h2>
|
||||||
<HeaderButton Icon={PlusIcon} label="Add Project" onClick={() => setIsOpen(true)} />
|
<HeaderButton
|
||||||
|
Icon={PlusIcon}
|
||||||
|
label="Add Project"
|
||||||
|
onClick={() => {
|
||||||
|
const e = new KeyboardEvent("keydown", { key: "p", ctrlKey: true });
|
||||||
|
document.dispatchEvent(e);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{projects.map((item) => (
|
{projects.map((item) => (
|
||||||
@ -125,8 +130,8 @@ const Projects: NextPage = () => {
|
|||||||
<Spinner />
|
<Spinner />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</AdminLayout>
|
</AppLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Projects;
|
export default withAuth(Projects);
|
||||||
|
@ -43,8 +43,14 @@ const SignIn: NextPage = () => {
|
|||||||
async (res: any) => {
|
async (res: any) => {
|
||||||
await mutateUser();
|
await mutateUser();
|
||||||
await mutateWorkspaces();
|
await mutateWorkspaces();
|
||||||
if (res.user.is_onboarded) router.push("/");
|
const nextLocation = router.asPath.split("?next=")[1];
|
||||||
else router.push("/invitations");
|
|
||||||
|
if (nextLocation) {
|
||||||
|
router.push(nextLocation as string);
|
||||||
|
} else {
|
||||||
|
if (res.user.is_onboarded) router.push("/");
|
||||||
|
else router.push("/invitations");
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[mutateUser, mutateWorkspaces, router]
|
[mutateUser, mutateWorkspaces, router]
|
||||||
);
|
);
|
||||||
|
@ -35,12 +35,12 @@ const WorkspaceInvitation: NextPage = () => {
|
|||||||
|
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
|
|
||||||
const { data: invitationDetail, error } = useSWR(
|
const { data: invitationDetail, error } = useSWR(invitationId && WORKSPACE_INVITATION, () =>
|
||||||
invitationId && WORKSPACE_INVITATION,
|
invitationId ? workspaceService.getWorkspaceInvitation(invitationId as string) : null
|
||||||
() => invitationId && workspaceService.getWorkspaceInvitation(invitationId as string)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleAccept = () => {
|
const handleAccept = () => {
|
||||||
|
if (!invitationDetail) return;
|
||||||
workspaceService
|
workspaceService
|
||||||
.joinWorkspace(invitationDetail.workspace.slug, invitationDetail.id, {
|
.joinWorkspace(invitationDetail.workspace.slug, invitationDetail.id, {
|
||||||
accepted: true,
|
accepted: true,
|
||||||
|
@ -4,7 +4,7 @@ import Link from "next/link";
|
|||||||
// react
|
// react
|
||||||
import React from "react";
|
import React from "react";
|
||||||
// layouts
|
// layouts
|
||||||
import AdminLayout from "layouts/AdminLayout";
|
import AppLayout from "layouts/AppLayout";
|
||||||
// swr
|
// swr
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
// hooks
|
// hooks
|
||||||
@ -26,8 +26,8 @@ const Workspace: NextPage = () => {
|
|||||||
const { user, activeWorkspace, projects } = useUser();
|
const { user, activeWorkspace, projects } = useUser();
|
||||||
|
|
||||||
const { data: myIssues } = useSWR<IIssue[]>(
|
const { data: myIssues } = useSWR<IIssue[]>(
|
||||||
user ? USER_ISSUE : null,
|
user && activeWorkspace ? USER_ISSUE(activeWorkspace.slug) : null,
|
||||||
user ? () => userService.userIssues() : null
|
user && activeWorkspace ? () => userService.userIssues(activeWorkspace.slug) : null
|
||||||
);
|
);
|
||||||
|
|
||||||
const cards = [
|
const cards = [
|
||||||
@ -46,7 +46,7 @@ const Workspace: NextPage = () => {
|
|||||||
const hours = new Date().getHours();
|
const hours = new Date().getHours();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminLayout>
|
<AppLayout>
|
||||||
<div className="h-full w-full space-y-5">
|
<div className="h-full w-full space-y-5">
|
||||||
{user ? (
|
{user ? (
|
||||||
<div className="font-medium text-2xl">
|
<div className="font-medium text-2xl">
|
||||||
@ -167,7 +167,7 @@ const Workspace: NextPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AdminLayout>
|
</AppLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ import { WORKSPACE_INVITATIONS, WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
|||||||
// hoc
|
// hoc
|
||||||
import withAuthWrapper from "lib/hoc/withAuthWrapper";
|
import withAuthWrapper from "lib/hoc/withAuthWrapper";
|
||||||
// layouts
|
// layouts
|
||||||
import AdminLayout from "layouts/AdminLayout";
|
import AppLayout from "layouts/AppLayout";
|
||||||
// components
|
// components
|
||||||
import SendWorkspaceInvitationModal from "components/workspace/SendWorkspaceInvitationModal";
|
import SendWorkspaceInvitationModal from "components/workspace/SendWorkspaceInvitationModal";
|
||||||
import ConfirmWorkspaceMemberRemove from "components/workspace/ConfirmWorkspaceMemberRemove";
|
import ConfirmWorkspaceMemberRemove from "components/workspace/ConfirmWorkspaceMemberRemove";
|
||||||
@ -41,7 +41,7 @@ const WorkspaceInvite: NextPage = () => {
|
|||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const { data: workspaceMembers, mutate: mutateMembers } = useSWR<any[]>(
|
const { data: workspaceMembers, mutate: mutateMembers } = useSWR<any[]>(
|
||||||
activeWorkspace ? WORKSPACE_MEMBERS : null,
|
activeWorkspace ? WORKSPACE_MEMBERS(activeWorkspace.slug) : null,
|
||||||
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null
|
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null
|
||||||
);
|
);
|
||||||
const { data: workspaceInvitations, mutate: mutateInvitations } = useSWR<any[]>(
|
const { data: workspaceInvitations, mutate: mutateInvitations } = useSWR<any[]>(
|
||||||
@ -71,7 +71,7 @@ const WorkspaceInvite: NextPage = () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminLayout
|
<AppLayout
|
||||||
meta={{
|
meta={{
|
||||||
title: "Plane - Workspace Invite",
|
title: "Plane - Workspace Invite",
|
||||||
}}
|
}}
|
||||||
@ -229,7 +229,7 @@ const WorkspaceInvite: NextPage = () => {
|
|||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500 sm:pl-6">
|
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500 sm:pl-6">
|
||||||
{member.status ? (
|
{member.member ? (
|
||||||
<span className="p-0.5 px-2 text-sm bg-green-700 text-white rounded-full">
|
<span className="p-0.5 px-2 text-sm bg-green-700 text-white rounded-full">
|
||||||
Active
|
Active
|
||||||
</span>
|
</span>
|
||||||
@ -277,7 +277,7 @@ const WorkspaceInvite: NextPage = () => {
|
|||||||
className="w-full text-left py-2 pl-2"
|
className="w-full text-left py-2 pl-2"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (member.status) {
|
if (member.member) {
|
||||||
setSelectedRemoveMember(member.id);
|
setSelectedRemoveMember(member.id);
|
||||||
} else {
|
} else {
|
||||||
setSelectedInviteRemoveMember(member.id);
|
setSelectedInviteRemoveMember(member.id);
|
||||||
@ -299,8 +299,8 @@ const WorkspaceInvite: NextPage = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</AdminLayout>
|
</AppLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withAuthWrapper(WorkspaceInvite);
|
export default withAuthWrapper(WorkspaceInvite);
|
||||||
|
@ -3,25 +3,28 @@ import React, { useEffect, useState } from "react";
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
// react hook form
|
// react hook form
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
// headless ui
|
||||||
|
import { Tab } from "@headlessui/react";
|
||||||
// react dropzone
|
// react dropzone
|
||||||
import Dropzone from "react-dropzone";
|
import Dropzone from "react-dropzone";
|
||||||
// services
|
// services
|
||||||
import workspaceService from "lib/services/workspace.service";
|
import workspaceService from "lib/services/workspace.service";
|
||||||
import fileServices from "lib/services/file.services";
|
import fileServices from "lib/services/file.service";
|
||||||
|
// hoc
|
||||||
|
import withAuth from "lib/hoc/withAuthWrapper";
|
||||||
// layouts
|
// layouts
|
||||||
import AdminLayout from "layouts/AdminLayout";
|
import AppLayout from "layouts/AppLayout";
|
||||||
|
|
||||||
// hooks
|
// hooks
|
||||||
import useUser from "lib/hooks/useUser";
|
import useUser from "lib/hooks/useUser";
|
||||||
import useToast from "lib/hooks/useToast";
|
import useToast from "lib/hooks/useToast";
|
||||||
// components
|
// components
|
||||||
import ConfirmWorkspaceDeletion from "components/workspace/ConfirmWorkspaceDeletion";
|
import ConfirmWorkspaceDeletion from "components/workspace/confirm-workspace-deletion";
|
||||||
// ui
|
// ui
|
||||||
import { Spinner, Button, Input, Select } from "ui";
|
import { Spinner, Button, Input, Select } from "ui";
|
||||||
import { BreadcrumbItem, Breadcrumbs } from "ui/Breadcrumbs";
|
import { BreadcrumbItem, Breadcrumbs } from "ui/Breadcrumbs";
|
||||||
// types
|
// types
|
||||||
import type { IWorkspace } from "types";
|
import type { IWorkspace } from "types";
|
||||||
import { Tab } from "@headlessui/react";
|
|
||||||
|
|
||||||
const defaultValues: Partial<IWorkspace> = {
|
const defaultValues: Partial<IWorkspace> = {
|
||||||
name: "",
|
name: "",
|
||||||
@ -83,12 +86,18 @@ const WorkspaceSettings = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminLayout
|
<AppLayout
|
||||||
meta={{
|
meta={{
|
||||||
title: "Plane - Workspace Settings",
|
title: "Plane - Workspace Settings",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ConfirmWorkspaceDeletion isOpen={isOpen} setIsOpen={setIsOpen} />
|
<ConfirmWorkspaceDeletion
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
data={activeWorkspace ?? null}
|
||||||
|
/>
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
<BreadcrumbItem title={`${activeWorkspace?.name ?? "Workspace"} Settings`} />
|
<BreadcrumbItem title={`${activeWorkspace?.name ?? "Workspace"} Settings`} />
|
||||||
@ -228,8 +237,8 @@ const WorkspaceSettings = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</AdminLayout>
|
</AppLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default WorkspaceSettings;
|
export default withAuth(WorkspaceSettings);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600;700;800&display=swap");
|
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600;700;800&display=swap");
|
||||||
|
@import url("https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@48,400,0,0");
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
16
apps/app/types/invitation.d.ts
vendored
16
apps/app/types/invitation.d.ts
vendored
@ -1,16 +0,0 @@
|
|||||||
import { IWorkspace } from "./";
|
|
||||||
|
|
||||||
export interface IWorkspaceInvitation {
|
|
||||||
id: string;
|
|
||||||
workspace: IWorkspace;
|
|
||||||
created_at: Date;
|
|
||||||
updated_at: Date;
|
|
||||||
email: string;
|
|
||||||
accepted: boolean;
|
|
||||||
token: string;
|
|
||||||
message: string;
|
|
||||||
responded_at: Date;
|
|
||||||
role: number;
|
|
||||||
created_by: null;
|
|
||||||
updated_by: null;
|
|
||||||
}
|
|
2
apps/app/types/issues.d.ts
vendored
2
apps/app/types/issues.d.ts
vendored
@ -27,7 +27,7 @@ export interface IIssue {
|
|||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
priority: string | null;
|
priority: string | null;
|
||||||
start_date: null;
|
start_date: string | null;
|
||||||
target_date: string | null;
|
target_date: string | null;
|
||||||
sequence_id: number;
|
sequence_id: number;
|
||||||
attachments: any[];
|
attachments: any[];
|
||||||
|
43
apps/app/types/projects.d.ts
vendored
43
apps/app/types/projects.d.ts
vendored
@ -1,4 +1,4 @@
|
|||||||
import type { IWorkspace } from "./";
|
import type { IUserLite, IWorkspace } from "./";
|
||||||
|
|
||||||
export interface IProject {
|
export interface IProject {
|
||||||
id: string;
|
id: string;
|
||||||
@ -15,3 +15,44 @@ export interface IProject {
|
|||||||
created_by: string;
|
created_by: string;
|
||||||
updated_by: string;
|
updated_by: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ProjectViewTheme = {
|
||||||
|
collapsed: boolean;
|
||||||
|
issueView: "list" | "kanban" | null;
|
||||||
|
groupByProperty: NestedKeyOf<IIssue> | null;
|
||||||
|
filterIssue: "activeIssue" | "backlogIssue" | null;
|
||||||
|
orderBy: NestedKeyOf<IIssue> | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface IProjectMember {
|
||||||
|
member: IUserLite;
|
||||||
|
project: IProject;
|
||||||
|
workspace: IWorkspace;
|
||||||
|
comment: string;
|
||||||
|
role: 5 | 10 | 15 | 20;
|
||||||
|
view_props: ProjectViewTheme;
|
||||||
|
|
||||||
|
created_at: Date;
|
||||||
|
updated_at: Date;
|
||||||
|
created_by: string;
|
||||||
|
updated_by: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IProjectMemberInvitation {
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
project: IProject;
|
||||||
|
workspace: IWorkspace;
|
||||||
|
|
||||||
|
email: string;
|
||||||
|
accepted: boolean;
|
||||||
|
token: string;
|
||||||
|
message: string;
|
||||||
|
responded_at: Date;
|
||||||
|
role: 5 | 10 | 15 | 20;
|
||||||
|
|
||||||
|
created_at: Date;
|
||||||
|
updated_at: Date;
|
||||||
|
created_by: string;
|
||||||
|
updated_by: string;
|
||||||
|
}
|
||||||
|
8
apps/app/types/users.d.ts
vendored
8
apps/app/types/users.d.ts
vendored
@ -16,3 +16,11 @@ export interface IUser {
|
|||||||
token: string;
|
token: string;
|
||||||
[...rest: string]: any;
|
[...rest: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IUserLite {
|
||||||
|
readonly id: string;
|
||||||
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
|
email: string;
|
||||||
|
avatar: string;
|
||||||
|
}
|
||||||
|
31
apps/app/types/workspace.d.ts
vendored
31
apps/app/types/workspace.d.ts
vendored
@ -1,4 +1,4 @@
|
|||||||
import type { IUser } from "./";
|
import type { IUser, IUserLite } from "./";
|
||||||
|
|
||||||
export interface IWorkspace {
|
export interface IWorkspace {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
@ -13,26 +13,31 @@ export interface IWorkspace {
|
|||||||
company_size: number;
|
company_size: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkspaceMember {
|
export interface IWorkspaceMemberInvitation {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
accepted: boolean;
|
||||||
|
token: string;
|
||||||
message: string;
|
message: string;
|
||||||
|
responded_at: Date;
|
||||||
role: 5 | 10 | 15 | 20;
|
role: 5 | 10 | 15 | 20;
|
||||||
member: IUser;
|
workspace: IWorkspace;
|
||||||
workspace: IWorkspace | string;
|
}
|
||||||
|
|
||||||
|
export interface IWorkspaceMember {
|
||||||
|
readonly id: string;
|
||||||
|
user: IUserLite;
|
||||||
|
workspace: IWorkspace;
|
||||||
|
member: IUserLite;
|
||||||
|
role: 5 | 10 | 15 | 20;
|
||||||
|
company_role: string | null;
|
||||||
created_at: Date;
|
created_at: Date;
|
||||||
updated_at: Date;
|
updated_at: Date;
|
||||||
created_by: string;
|
created_by: string;
|
||||||
updated_by: string;
|
updated_by: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProjectMember {
|
export interface ILastActiveWorkspaceDetails {
|
||||||
readonly id: string;
|
workspace_details: IWorkspace;
|
||||||
readonly project: string;
|
project_details: IProject[];
|
||||||
email: string;
|
|
||||||
message: string;
|
|
||||||
role: 5 | 10 | 15 | 20;
|
|
||||||
member: any;
|
|
||||||
member_id: string;
|
|
||||||
user_id: string;
|
|
||||||
}
|
}
|
||||||
|
@ -9,12 +9,7 @@ type EmptySpaceProps = {
|
|||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
children: any;
|
children: any;
|
||||||
Icon?: (
|
Icon?: (props: any) => JSX.Element;
|
||||||
props: React.SVGProps<SVGSVGElement> & {
|
|
||||||
title?: string | undefined;
|
|
||||||
titleId?: string | undefined;
|
|
||||||
}
|
|
||||||
) => JSX.Element;
|
|
||||||
link?: { text: string; href: string };
|
link?: { text: string; href: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user