Merge pull request #30 from dakshesh14/main

fix: mutation error for project detail, feat: protecting all routes wrapper
This commit is contained in:
Vamsi Kurama 2022-12-12 19:03:13 +05:30 committed by GitHub
commit ba869c37db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
59 changed files with 556 additions and 302 deletions

4
apps/app/.env.example Normal file
View 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

View File

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

View File

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

View File

@ -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,7 +54,7 @@ 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(activeWorkspace.slug) : null, activeWorkspace ? WORKSPACE_MEMBERS(activeWorkspace.slug) : null,
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null, activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null,
{ {

View File

@ -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="projectName"
/>
</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"}

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback } from "react"; import React, { useState, useEffect } from "react";
// swr // swr
import useSWR, { mutate } from "swr"; import useSWR, { mutate } from "swr";
// react hook form // react hook form
@ -8,6 +8,10 @@ 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"; 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, WORKSPACE_MEMBERS } from "constants/fetch-keys"; import { PROJECTS_LIST, WORKSPACE_MEMBERS } from "constants/fetch-keys";
// hooks // hooks
@ -15,21 +19,19 @@ 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, WorkspaceMember } from "types"; import { IProject } from "types";
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
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<{ const IsGuestCondition: React.FC<{
@ -60,11 +62,16 @@ const CreateProjectModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
const { activeWorkspace, user } = useUser(); const { activeWorkspace, user } = useUser();
const { data: workspaceMembers } = useSWR<WorkspaceMember[]>( const { data: workspaceMembers } = useSWR(
activeWorkspace ? WORKSPACE_MEMBERS(activeWorkspace.slug) : null, activeWorkspace ? WORKSPACE_MEMBERS(activeWorkspace.slug) : null,
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null,
{
shouldRetryOnError: false,
}
); );
const [recommendedIdentifier, setRecommendedIdentifier] = useState<string[]>([]);
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
const [isChangeIdentifierRequired, setIsChangeIdentifierRequired] = useState(true); const [isChangeIdentifierRequired, setIsChangeIdentifierRequired] = useState(true);
@ -75,12 +82,11 @@ const CreateProjectModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
handleSubmit, handleSubmit,
reset, reset,
setError, setError,
clearErrors,
watch, watch,
setValue, setValue,
} = useForm<IProject>({ } = useForm<IProject>({
defaultValues, defaultValues,
reValidateMode: "onChange",
mode: "all",
}); });
const onSubmit = async (formData: IProject) => { const onSubmit = async (formData: IProject) => {
@ -111,6 +117,7 @@ const CreateProjectModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
handleClose(); handleClose();
return; 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, {
@ -123,22 +130,6 @@ 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) => {
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), []);
useEffect(() => {
if (projectName && isChangeIdentifierRequired) {
setValue("identifier", projectName.replace(/ /g, "-").toUpperCase().substring(0, 3));
}
}, [projectName, projectIdentifier, setValue, isChangeIdentifierRequired]);
if (workspaceMembers) { if (workspaceMembers) {
const isMember = workspaceMembers.find((member) => member.member.id === user?.id); const isMember = workspaceMembers.find((member) => member.member.id === user?.id);
const isGuest = workspaceMembers.find( const isGuest = workspaceMembers.find(
@ -148,6 +139,30 @@ const CreateProjectModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
if ((!isMember || isGuest) && isOpen) return <IsGuestCondition setIsOpen={setIsOpen} />; if ((!isMember || isGuest) && isOpen) return <IsGuestCondition setIsOpen={setIsOpen} />;
} }
useEffect(() => {
if (projectName && isChangeIdentifierRequired) {
setValue("identifier", projectName.replace(/ /g, "").toUpperCase().substring(0, 3));
}
}, [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}>
@ -234,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: {
@ -251,6 +262,27 @@ const CreateProjectModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
}, },
}} }}
/> />
{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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,14 +10,14 @@ 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, 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 { addSpaceIfCamelCase, classNames, renderShortNumericDateFormat } from "constants/common"; import { addSpaceIfCamelCase, classNames, renderShortNumericDateFormat } from "constants/common";
@ -60,7 +60,7 @@ 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
); );

View File

@ -6,8 +6,8 @@ import { Listbox, Transition } from "@headlessui/react";
// react hook form // react hook form
import { useForm, Controller, UseFormWatch } 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";
@ -40,14 +40,7 @@ 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 { import type { IIssue, IIssueLabels, IssueResponse, IState, NestedKeyOf } from "types";
IIssue,
IIssueLabels,
IssueResponse,
IState,
NestedKeyOf,
WorkspaceMember,
} from "types";
import { TwitterPicker } from "react-color"; import { TwitterPicker } from "react-color";
import IssuesListModal from "components/project/issues/IssuesListModal"; import IssuesListModal from "components/project/issues/IssuesListModal";
@ -84,7 +77,7 @@ const IssueDetailSidebar: React.FC<Props> = ({
: null : null
); );
const { data: people } = useSWR<WorkspaceMember[]>( const { data: people } = useSWR(
activeWorkspace ? WORKSPACE_MEMBERS(activeWorkspace.slug) : null, activeWorkspace ? WORKSPACE_MEMBERS(activeWorkspace.slug) : null,
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null
); );

View File

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

View File

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

View File

@ -28,7 +28,7 @@ type Props = {
slug: string; slug: string;
invitationsRespond: string[]; invitationsRespond: string[];
handleInvitation: (project_invitation: any, action: "accepted" | "withdraw") => void; handleInvitation: (project_invitation: any, action: "accepted" | "withdraw") => void;
setDeleteProject: React.Dispatch<React.SetStateAction<IProject | undefined>>; setDeleteProject: (id: string | null) => void;
}; };
const ProjectMemberInvitations: React.FC<Props> = ({ const ProjectMemberInvitations: React.FC<Props> = ({
@ -100,7 +100,7 @@ const ProjectMemberInvitations: React.FC<Props> = ({
<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>

View File

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

View File

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

View File

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

View File

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

View File

@ -65,6 +65,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/`;

View File

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

View File

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

View File

@ -6,3 +6,5 @@ export const ROLE = {
15: "Member", 15: "Member",
20: "Admin", 20: "Admin",
}; };
export const NETWORK_CHOICES = { "0": "Secret", "2": "Public" };

View File

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

View File

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

View File

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

View File

@ -8,7 +8,7 @@ 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";

View File

@ -10,8 +10,6 @@ import authenticationService from "lib/services/authentication.service";
import useUser from "lib/hooks/useUser"; import useUser from "lib/hooks/useUser";
import useTheme from "lib/hooks/useTheme"; import useTheme from "lib/hooks/useTheme";
import useToast from "lib/hooks/useToast"; import useToast from "lib/hooks/useToast";
// components
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
@ -108,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();
@ -124,7 +121,6 @@ const Sidebar: React.FC = () => {
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
@ -557,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"}

View File

@ -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,29 +56,29 @@ 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); );
} }
} }

View File

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

View File

@ -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,8 +24,8 @@ 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;
}) })
@ -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;
}) })

View File

@ -17,12 +17,15 @@ 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 { 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 +35,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 +45,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 +55,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 +64,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 +97,7 @@ class WorkspaceService extends APIService {
}); });
} }
async userWorkspaceInvitations(): Promise<any> { 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 +107,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 +117,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 +131,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 +141,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 +151,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 +161,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 +173,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;
}) })

View File

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

View File

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

View File

@ -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
@ -278,4 +280,4 @@ const MyIssues: NextPage = () => {
); );
}; };
export default MyIssues; export default withAuth(MyIssues);

View File

@ -8,15 +8,17 @@ import { useForm } from "react-hook-form";
import Dropzone, { useDropzone } from "react-dropzone"; import Dropzone, { useDropzone } from "react-dropzone";
// hooks // hooks
import useUser from "lib/hooks/useUser"; import useUser from "lib/hooks/useUser";
// hoc
import withAuth from "lib/hoc/withAuthWrapper";
// layouts // layouts
import AppLayout from "layouts/AppLayout"; import AppLayout from "layouts/AppLayout";
// 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";
// ui // ui
import { BreadcrumbItem, Breadcrumbs, Button, Input, Spinner } from "ui"; import { BreadcrumbItem, Breadcrumbs, Button, Input, Spinner } from "ui";
// types // types
import type { IIssue, IUser, IWorkspaceInvitation } from "types"; import type { IIssue, IUser, IWorkspaceMemberInvitation } from "types";
import { import {
ChevronRightIcon, ChevronRightIcon,
ClipboardDocumentListIcon, ClipboardDocumentListIcon,
@ -79,8 +81,9 @@ const Profile: NextPage = () => {
myProfile ? () => userService.userIssues() : null myProfile ? () => userService.userIssues() : null
); );
const { data: invitations } = useSWR<IWorkspaceInvitation[]>(USER_WORKSPACE_INVITATIONS, () => const { data: invitations } = useSWR<IWorkspaceMemberInvitation[]>(
workspaceService.userWorkspaceInvitations() USER_WORKSPACE_INVITATIONS,
() => workspaceService.userWorkspaceInvitations()
); );
useEffect(() => { useEffect(() => {
@ -307,4 +310,4 @@ const Profile: NextPage = () => {
); );
}; };
export default Profile; export default withAuth(Profile);

View File

@ -5,12 +5,14 @@ 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 AppLayout from "layouts/AppLayout"; import AppLayout from "layouts/AppLayout";
// components // components
@ -258,4 +260,4 @@ const ProjectSprints: NextPage = () => {
); );
}; };
export default ProjectSprints; export default withAuth(ProjectSprints);

View File

@ -11,7 +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";
// fetch keys // fetch keys
import { import {
PROJECT_ISSUES_ACTIVITY, PROJECT_ISSUES_ACTIVITY,
@ -21,6 +21,8 @@ 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 AppLayout from "layouts/AppLayout"; import AppLayout from "layouts/AppLayout";
// components // components
@ -606,4 +608,4 @@ const IssueDetail: NextPage = () => {
); );
}; };
export default IssueDetail; export default withAuth(IssueDetail);

View File

@ -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" },
@ -86,7 +86,7 @@ const ProjectIssues: NextPage = () => {
projectId as string projectId as string
); );
const { data: members } = useSWR<ProjectMember[]>( const { data: members } = useSWR(
activeWorkspace && activeProject activeWorkspace && activeProject
? PROJECT_MEMBERS(activeWorkspace.slug, activeProject.id) ? PROJECT_MEMBERS(activeWorkspace.slug, activeProject.id)
: null, : null,

View File

@ -14,6 +14,8 @@ 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 AppLayout from "layouts/AppLayout"; import AppLayout from "layouts/AppLayout";
// components // components
@ -109,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
); );
} }
@ -121,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
); );
} }
@ -313,4 +313,4 @@ const ProjectMembers: NextPage = () => {
); );
}; };
export default ProjectMembers; export default withAuth(ProjectMembers);

View File

@ -9,6 +9,8 @@ import useSWR, { mutate } from "swr";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
// headless ui // headless ui
import { Tab } from "@headlessui/react"; import { Tab } from "@headlessui/react";
// hoc
import withAuth from "lib/hoc/withAuthWrapper";
// layouts // layouts
import AppLayout from "layouts/AppLayout"; import AppLayout from "layouts/AppLayout";
// service // service
@ -27,9 +29,12 @@ import type { IProject, IWorkspace } from "types";
const defaultValues: Partial<IProject> = { const defaultValues: Partial<IProject> = {
name: "", name: "",
description: "", description: "",
identifier: "",
network: 0,
}; };
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"), { const GeneralSettings = dynamic(() => import("components/project/settings/GeneralSettings"), {
loading: () => <p>Loading...</p>, loading: () => <p>Loading...</p>,
ssr: false, ssr: false,
@ -70,7 +75,7 @@ const ProjectSettings: NextPage = () => {
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
@ -87,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,
@ -99,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) => {
@ -181,4 +190,4 @@ const ProjectSettings: NextPage = () => {
); );
}; };
export default ProjectSettings; export default withAuth(ProjectSettings);

View File

@ -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 AppLayout from "layouts/AppLayout"; 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 (
<AppLayout> <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) => (
@ -129,4 +134,4 @@ const Projects: NextPage = () => {
); );
}; };
export default Projects; export default withAuth(Projects);

View File

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

View File

@ -7,7 +7,9 @@ import { useForm } from "react-hook-form";
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 AppLayout from "layouts/AppLayout"; import AppLayout from "layouts/AppLayout";
@ -232,4 +234,4 @@ const WorkspaceSettings = () => {
); );
}; };
export default WorkspaceSettings; export default withAuth(WorkspaceSettings);

View File

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

View File

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

View File

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

View File

@ -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,26 @@ 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 {
readonly id: string;
readonly project: string;
email: string;
message: string;
role: 5 | 10 | 15 | 20;
member: any;
member_id: string;
user_id: string;
}