diff --git a/apps/app/.env.example b/apps/app/.env.example new file mode 100644 index 000000000..aff5c6c31 --- /dev/null +++ b/apps/app/.env.example @@ -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 \ No newline at end of file diff --git a/apps/app/components/command-palette/addAsSubIssue.tsx b/apps/app/components/command-palette/addAsSubIssue.tsx index e8c585e45..aadc04574 100644 --- a/apps/app/components/command-palette/addAsSubIssue.tsx +++ b/apps/app/components/command-palette/addAsSubIssue.tsx @@ -7,7 +7,7 @@ import { useForm } from "react-hook-form"; // headless ui import { Combobox, Dialog, Transition } from "@headlessui/react"; // services -import issuesServices from "lib/services/issues.services"; +import issuesServices from "lib/services/issues.service"; // hooks import useUser from "lib/hooks/useUser"; // icons diff --git a/apps/app/components/command-palette/index.tsx b/apps/app/components/command-palette/index.tsx index 63490c97d..32f71a3db 100644 --- a/apps/app/components/command-palette/index.tsx +++ b/apps/app/components/command-palette/index.tsx @@ -8,7 +8,7 @@ import { SubmitHandler, useForm } from "react-hook-form"; // headless ui import { Combobox, Dialog, Transition } from "@headlessui/react"; // services -import issuesServices from "lib/services/issues.services"; +import issuesServices from "lib/services/issues.service"; // hooks import useUser from "lib/hooks/useUser"; import useTheme from "lib/hooks/useTheme"; @@ -22,7 +22,7 @@ import { } from "@heroicons/react/24/outline"; // components 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 CreateUpdateCycleModal from "components/project/cycles/CreateUpdateCyclesModal"; // ui diff --git a/apps/app/components/project/SendProjectInvitationModal.tsx b/apps/app/components/project/SendProjectInvitationModal.tsx index 0c0485803..6227c569f 100644 --- a/apps/app/components/project/SendProjectInvitationModal.tsx +++ b/apps/app/components/project/SendProjectInvitationModal.tsx @@ -20,7 +20,7 @@ import { Button, Select, TextArea } from "ui"; import { ChevronDownIcon, CheckIcon } from "@heroicons/react/20/solid"; // types -import { ProjectMember, WorkspaceMember } from "types"; +import { IProjectMemberInvitation } from "types"; type Props = { isOpen: boolean; @@ -28,6 +28,11 @@ type Props = { members: any[]; }; +type ProjectMember = IProjectMemberInvitation & { + member_id: string; + user_id: string; +}; + const defaultValues: Partial = { email: "", message: "", @@ -49,7 +54,7 @@ const SendProjectInvitationModal: React.FC = ({ isOpen, setIsOpen, member const { setToastAlert } = useToast(); - const { data: people } = useSWR( + const { data: people } = useSWR( activeWorkspace ? WORKSPACE_MEMBERS(activeWorkspace.slug) : null, activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null, { diff --git a/apps/app/components/project/ConfirmProjectDeletion.tsx b/apps/app/components/project/confirm-project-deletion.tsx similarity index 63% rename from apps/app/components/project/ConfirmProjectDeletion.tsx rename to apps/app/components/project/confirm-project-deletion.tsx index 0b0dde39e..46391bb5c 100644 --- a/apps/app/components/project/ConfirmProjectDeletion.tsx +++ b/apps/app/components/project/confirm-project-deletion.tsx @@ -9,19 +9,26 @@ import useToast from "lib/hooks/useToast"; // icons import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; // ui -import { Button } from "ui"; +import { Button, Input } from "ui"; // types import type { IProject } from "types"; type Props = { isOpen: boolean; - setIsOpen: React.Dispatch>; - data?: IProject; + onClose: () => void; + data: IProject | null; }; -const ConfirmProjectDeletion: React.FC = ({ isOpen, setIsOpen, data }) => { +const ConfirmProjectDeletion: React.FC = ({ isOpen, data, onClose }) => { const [isDeleteLoading, setIsDeleteLoading] = useState(false); + const [selectedProject, setSelectedProject] = useState(null); + + const [confirmProjectName, setConfirmProjectName] = useState(""); + const [confirmDeleteMyProject, setConfirmDeleteMyProject] = useState(false); + + const canDelete = confirmProjectName === data?.name && confirmDeleteMyProject; + const { activeWorkspace, mutateProjects } = useUser(); const { setToastAlert } = useToast(); @@ -29,13 +36,18 @@ const ConfirmProjectDeletion: React.FC = ({ isOpen, setIsOpen, data }) => const cancelButtonRef = useRef(null); const handleClose = () => { - setIsOpen(false); setIsDeleteLoading(false); + const timer = setTimeout(() => { + setConfirmProjectName(""); + setConfirmDeleteMyProject(false); + clearTimeout(timer); + }, 350); + onClose(); }; const handleDeletion = async () => { setIsDeleteLoading(true); - if (!data || !activeWorkspace) return; + if (!data || !activeWorkspace || !canDelete) return; await projectService .deleteProject(activeWorkspace.slug, data.id) .then(() => { @@ -54,8 +66,14 @@ const ConfirmProjectDeletion: React.FC = ({ isOpen, setIsOpen, data }) => }; useEffect(() => { - data && setIsOpen(true); - }, [data, setIsOpen]); + if (data) setSelectedProject(data); + else { + const timer = setTimeout(() => { + setSelectedProject(null); + clearTimeout(timer); + }, 300); + } + }, [data]); return ( @@ -104,11 +122,48 @@ const ConfirmProjectDeletion: React.FC = ({ isOpen, setIsOpen, data }) =>

Are you sure you want to delete project - {`"`} - {data?.name} + {selectedProject?.name} {`"`} ? All of the data related to the project will be permanently removed. This action cannot be undone.

+
+
+

+ Enter the project name{" "} + {selectedProject?.name} to + continue: +

+ { + setConfirmProjectName(e.target.value); + }} + name="projectName" + /> +
+
+

+ To confirm, type delete my project{" "} + below: +

+ { + if (e.target.value === "delete my project") { + setConfirmDeleteMyProject(true); + } else { + setConfirmDeleteMyProject(false); + } + }} + name="projectName" + /> +
@@ -117,7 +172,7 @@ const ConfirmProjectDeletion: React.FC = ({ isOpen, setIsOpen, data }) => type="button" onClick={handleDeletion} theme="danger" - disabled={isDeleteLoading} + disabled={isDeleteLoading || !canDelete} className="inline-flex sm:ml-3" > {isDeleteLoading ? "Deleting..." : "Delete"} diff --git a/apps/app/components/project/CreateProjectModal.tsx b/apps/app/components/project/create-project-modal.tsx similarity index 78% rename from apps/app/components/project/CreateProjectModal.tsx rename to apps/app/components/project/create-project-modal.tsx index c93f9123b..f6ad48a78 100644 --- a/apps/app/components/project/CreateProjectModal.tsx +++ b/apps/app/components/project/create-project-modal.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from "react"; +import React, { useState, useEffect } from "react"; // swr import useSWR, { mutate } from "swr"; // react hook form @@ -8,6 +8,10 @@ import { Dialog, Transition } from "@headlessui/react"; // services 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 import { PROJECTS_LIST, WORKSPACE_MEMBERS } from "constants/fetch-keys"; // hooks @@ -15,21 +19,19 @@ import useUser from "lib/hooks/useUser"; import useToast from "lib/hooks/useToast"; // ui import { Button, Input, TextArea, Select } from "ui"; -// common -import { debounce } from "constants/common"; // types -import { IProject, WorkspaceMember } from "types"; +import { IProject } from "types"; type Props = { isOpen: boolean; setIsOpen: React.Dispatch>; }; -const NETWORK_CHOICES = { "0": "Secret", "2": "Public" }; - const defaultValues: Partial = { name: "", + identifier: "", description: "", + network: 0, }; const IsGuestCondition: React.FC<{ @@ -60,11 +62,16 @@ const CreateProjectModal: React.FC = ({ isOpen, setIsOpen }) => { const { activeWorkspace, user } = useUser(); - const { data: workspaceMembers } = useSWR( + const { data: workspaceMembers } = useSWR( activeWorkspace ? WORKSPACE_MEMBERS(activeWorkspace.slug) : null, - activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null + activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null, + { + shouldRetryOnError: false, + } ); + const [recommendedIdentifier, setRecommendedIdentifier] = useState([]); + const { setToastAlert } = useToast(); const [isChangeIdentifierRequired, setIsChangeIdentifierRequired] = useState(true); @@ -75,12 +82,11 @@ const CreateProjectModal: React.FC = ({ isOpen, setIsOpen }) => { handleSubmit, reset, setError, + clearErrors, watch, setValue, } = useForm({ defaultValues, - reValidateMode: "onChange", - mode: "all", }); const onSubmit = async (formData: IProject) => { @@ -111,6 +117,7 @@ const CreateProjectModal: React.FC = ({ isOpen, setIsOpen }) => { handleClose(); return; } + err = err.data; Object.keys(err).map((key) => { const errorMessages = err[key]; setError(key as keyof IProject, { @@ -123,22 +130,6 @@ const CreateProjectModal: React.FC = ({ isOpen, setIsOpen }) => { const projectName = watch("name") ?? ""; 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) { const isMember = workspaceMembers.find((member) => member.member.id === user?.id); const isGuest = workspaceMembers.find( @@ -148,6 +139,30 @@ const CreateProjectModal: React.FC = ({ isOpen, setIsOpen }) => { if ((!isMember || isGuest) && isOpen) return ; } + 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 ( @@ -234,11 +249,7 @@ const CreateProjectModal: React.FC = ({ isOpen, setIsOpen }) => { placeholder="Enter Project Identifier" error={errors.identifier} register={register} - onChange={(e: any) => { - setIsChangeIdentifierRequired(false); - if (!activeWorkspace || !e.target.value) return; - checkIdentifierAvailability(activeWorkspace.slug, e.target.value); - }} + onChange={() => setIsChangeIdentifierRequired(false)} validations={{ required: "Identifier is required", minLength: { @@ -251,6 +262,27 @@ const CreateProjectModal: React.FC = ({ isOpen, setIsOpen }) => { }, }} /> + {errors.identifier && ( +
+

Ops! Identifier is already taken. Try one of the following:

+
+ {recommendedIdentifier.map((identifier) => ( + + ))} +
+
+ )} diff --git a/apps/app/components/project/cycles/ConfirmCycleDeletion.tsx b/apps/app/components/project/cycles/ConfirmCycleDeletion.tsx index fb5cf565b..b3571831a 100644 --- a/apps/app/components/project/cycles/ConfirmCycleDeletion.tsx +++ b/apps/app/components/project/cycles/ConfirmCycleDeletion.tsx @@ -4,7 +4,7 @@ import { mutate } from "swr"; // headless ui import { Dialog, Transition } from "@headlessui/react"; // services -import cycleService from "lib/services/cycles.services"; +import cycleService from "lib/services/cycles.service"; // fetch api import { CYCLE_LIST } from "constants/fetch-keys"; // hooks diff --git a/apps/app/components/project/cycles/CreateUpdateCyclesModal.tsx b/apps/app/components/project/cycles/CreateUpdateCyclesModal.tsx index 0fd25f481..3f3127082 100644 --- a/apps/app/components/project/cycles/CreateUpdateCyclesModal.tsx +++ b/apps/app/components/project/cycles/CreateUpdateCyclesModal.tsx @@ -6,7 +6,7 @@ import { useForm } from "react-hook-form"; // headless import { Dialog, Transition } from "@headlessui/react"; // services -import cycleService from "lib/services/cycles.services"; +import cycleService from "lib/services/cycles.service"; // fetch keys import { CYCLE_LIST } from "constants/fetch-keys"; // hooks diff --git a/apps/app/components/project/cycles/CycleIssuesListModal.tsx b/apps/app/components/project/cycles/CycleIssuesListModal.tsx index 5f29a5bfd..d25d069df 100644 --- a/apps/app/components/project/cycles/CycleIssuesListModal.tsx +++ b/apps/app/components/project/cycles/CycleIssuesListModal.tsx @@ -7,7 +7,7 @@ import { Combobox, Dialog, Transition } from "@headlessui/react"; // ui import { Button } from "ui"; // services -import issuesServices from "lib/services/issues.services"; +import issuesServices from "lib/services/issues.service"; // hooks import useUser from "lib/hooks/useUser"; import useToast from "lib/hooks/useToast"; diff --git a/apps/app/components/project/cycles/CycleView.tsx b/apps/app/components/project/cycles/CycleView.tsx index f46079165..73a0d0af4 100644 --- a/apps/app/components/project/cycles/CycleView.tsx +++ b/apps/app/components/project/cycles/CycleView.tsx @@ -7,7 +7,7 @@ import useSWR, { mutate } from "swr"; // headless ui import { Disclosure, Transition, Menu } from "@headlessui/react"; // services -import cycleServices from "lib/services/cycles.services"; +import cycleServices from "lib/services/cycles.service"; // hooks import useUser from "lib/hooks/useUser"; // components @@ -22,7 +22,7 @@ import type { CycleViewProps as Props, CycleIssueResponse, IssueResponse } from import { CYCLE_ISSUES } from "constants/fetch-keys"; // constants 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 { Draggable } from "react-beautiful-dnd"; diff --git a/apps/app/components/project/issues/BoardView/index.tsx b/apps/app/components/project/issues/BoardView/index.tsx index fe69a091e..4881f22ed 100644 --- a/apps/app/components/project/issues/BoardView/index.tsx +++ b/apps/app/components/project/issues/BoardView/index.tsx @@ -7,8 +7,8 @@ import useSWR from "swr"; import type { DropResult } from "react-beautiful-dnd"; import { DragDropContext } from "react-beautiful-dnd"; // services -import stateServices from "lib/services/state.services"; -import issuesServices from "lib/services/issues.services"; +import stateServices from "lib/services/state.service"; +import issuesServices from "lib/services/issues.service"; // hooks import useUser from "lib/hooks/useUser"; // fetching keys @@ -20,7 +20,7 @@ import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssue // ui import { Spinner } from "ui"; // types -import type { IState, IIssue, Properties, NestedKeyOf, ProjectMember } from "types"; +import type { IState, IIssue, Properties, NestedKeyOf, IProjectMember } from "types"; import ConfirmIssueDeletion from "../ConfirmIssueDeletion"; import { TrashIcon } from "@heroicons/react/24/outline"; @@ -30,7 +30,7 @@ type Props = { groupedByIssues: { [key: string]: IIssue[]; }; - members: ProjectMember[] | undefined; + members: IProjectMember[] | undefined; }; const BoardView: React.FC = ({ properties, selectedGroup, groupedByIssues, members }) => { diff --git a/apps/app/components/project/issues/BoardView/state/ConfirmStateDeletion.tsx b/apps/app/components/project/issues/BoardView/state/ConfirmStateDeletion.tsx index b8754437a..448c50acc 100644 --- a/apps/app/components/project/issues/BoardView/state/ConfirmStateDeletion.tsx +++ b/apps/app/components/project/issues/BoardView/state/ConfirmStateDeletion.tsx @@ -4,7 +4,7 @@ import { mutate } from "swr"; // headless ui import { Dialog, Transition } from "@headlessui/react"; // services -import stateServices from "lib/services/state.services"; +import stateServices from "lib/services/state.service"; // fetch api import { STATE_LIST } from "constants/fetch-keys"; // hooks @@ -43,7 +43,7 @@ const ConfirmStateDeletion: React.FC = ({ isOpen, setIsOpen, data }) => { mutate( STATE_LIST(data.project), (prevData) => prevData?.filter((state) => state.id !== data?.id), - false, + false ); handleClose(); }) @@ -98,18 +98,15 @@ const ConfirmStateDeletion: React.FC = ({ isOpen, setIsOpen, data }) => { />
- + Delete State

Are you sure you want to delete state - {`"`} {data?.name} - {`"`} ? All of the data related to the state will be - permanently removed. This action cannot be undone. + {`"`} ? All of the data related to the state will be permanently removed. + This action cannot be undone.

diff --git a/apps/app/components/project/issues/BoardView/state/CreateUpdateStateModal.tsx b/apps/app/components/project/issues/BoardView/state/CreateUpdateStateModal.tsx index 9f3b3951b..3ba149820 100644 --- a/apps/app/components/project/issues/BoardView/state/CreateUpdateStateModal.tsx +++ b/apps/app/components/project/issues/BoardView/state/CreateUpdateStateModal.tsx @@ -8,7 +8,7 @@ import { TwitterPicker } from "react-color"; // headless import { Dialog, Popover, Transition } from "@headlessui/react"; // services -import stateService from "lib/services/state.services"; +import stateService from "lib/services/state.service"; // fetch keys import { STATE_LIST } from "constants/fetch-keys"; // hooks diff --git a/apps/app/components/project/issues/ConfirmIssueDeletion.tsx b/apps/app/components/project/issues/ConfirmIssueDeletion.tsx index 3b161ae6c..f1476c682 100644 --- a/apps/app/components/project/issues/ConfirmIssueDeletion.tsx +++ b/apps/app/components/project/issues/ConfirmIssueDeletion.tsx @@ -6,7 +6,7 @@ import { Dialog, Transition } from "@headlessui/react"; // fetching keys import { PROJECT_ISSUES_LIST } from "constants/fetch-keys"; // services -import issueServices from "lib/services/issues.services"; +import issueServices from "lib/services/issues.service"; // hooks import useUser from "lib/hooks/useUser"; import useToast from "lib/hooks/useToast"; diff --git a/apps/app/components/project/issues/CreateUpdateIssueModal/SelectAssignee.tsx b/apps/app/components/project/issues/CreateUpdateIssueModal/SelectAssignee.tsx index 376ab65e6..dc5d2b741 100644 --- a/apps/app/components/project/issues/CreateUpdateIssueModal/SelectAssignee.tsx +++ b/apps/app/components/project/issues/CreateUpdateIssueModal/SelectAssignee.tsx @@ -11,7 +11,7 @@ import useUser from "lib/hooks/useUser"; import { PROJECT_MEMBERS } from "constants/fetch-keys"; // types 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 { SearchListbox } from "ui"; @@ -23,7 +23,7 @@ type Props = { const SelectAssignee: React.FC = ({ control }) => { const { activeWorkspace, activeProject } = useUser(); - const { data: people } = useSWR( + const { data: people } = useSWR( activeWorkspace && activeProject ? PROJECT_MEMBERS(activeProject.id) : null, activeWorkspace && activeProject ? () => projectServices.projectMembers(activeWorkspace.slug, activeProject.id) diff --git a/apps/app/components/project/issues/CreateUpdateIssueModal/SelectLabels.tsx b/apps/app/components/project/issues/CreateUpdateIssueModal/SelectLabels.tsx index a7ef75203..6fad13a44 100644 --- a/apps/app/components/project/issues/CreateUpdateIssueModal/SelectLabels.tsx +++ b/apps/app/components/project/issues/CreateUpdateIssueModal/SelectLabels.tsx @@ -6,7 +6,7 @@ import { useForm, Controller } from "react-hook-form"; // headless ui import { Listbox, Transition } from "@headlessui/react"; // services -import issuesServices from "lib/services/issues.services"; +import issuesServices from "lib/services/issues.service"; // hooks import useUser from "lib/hooks/useUser"; // fetching keys diff --git a/apps/app/components/project/issues/CreateUpdateIssueModal/index.tsx b/apps/app/components/project/issues/CreateUpdateIssueModal/index.tsx index b7559d988..14032c6c6 100644 --- a/apps/app/components/project/issues/CreateUpdateIssueModal/index.tsx +++ b/apps/app/components/project/issues/CreateUpdateIssueModal/index.tsx @@ -16,7 +16,7 @@ import { // headless import { Dialog, Menu, Transition } from "@headlessui/react"; // services -import issuesServices from "lib/services/issues.services"; +import issuesServices from "lib/services/issues.service"; // hooks import useUser from "lib/hooks/useUser"; import useToast from "lib/hooks/useToast"; diff --git a/apps/app/components/project/issues/ListView/index.tsx b/apps/app/components/project/issues/ListView/index.tsx index c4a7e19f1..78d5ab68b 100644 --- a/apps/app/components/project/issues/ListView/index.tsx +++ b/apps/app/components/project/issues/ListView/index.tsx @@ -10,14 +10,14 @@ import { Listbox, Transition } from "@headlessui/react"; // icons import { PencilIcon, TrashIcon } from "@heroicons/react/24/outline"; // types -import { IIssue, IssueResponse, NestedKeyOf, Properties, WorkspaceMember } from "types"; +import { IIssue, IssueResponse, NestedKeyOf, Properties } from "types"; // hooks import useUser from "lib/hooks/useUser"; // fetch keys import { PRIORITIES } from "constants/"; import { PROJECT_ISSUES_LIST, WORKSPACE_MEMBERS } from "constants/fetch-keys"; // services -import issuesServices from "lib/services/issues.services"; +import issuesServices from "lib/services/issues.service"; import workspaceService from "lib/services/workspace.service"; // constants import { addSpaceIfCamelCase, classNames, renderShortNumericDateFormat } from "constants/common"; @@ -60,7 +60,7 @@ const ListView: React.FC = ({ }); }; - const { data: people } = useSWR( + const { data: people } = useSWR( activeWorkspace ? WORKSPACE_MEMBERS : null, activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null ); diff --git a/apps/app/components/project/issues/issue-detail/IssueDetailSidebar.tsx b/apps/app/components/project/issues/issue-detail/IssueDetailSidebar.tsx index 12f0773f2..71c84a745 100644 --- a/apps/app/components/project/issues/issue-detail/IssueDetailSidebar.tsx +++ b/apps/app/components/project/issues/issue-detail/IssueDetailSidebar.tsx @@ -6,8 +6,8 @@ import { Listbox, Transition } from "@headlessui/react"; // react hook form import { useForm, Controller, UseFormWatch } from "react-hook-form"; // services -import stateServices from "lib/services/state.services"; -import issuesServices from "lib/services/issues.services"; +import stateServices from "lib/services/state.service"; +import issuesServices from "lib/services/issues.service"; import workspaceService from "lib/services/workspace.service"; // hooks import useUser from "lib/hooks/useUser"; @@ -40,14 +40,7 @@ import { } from "@heroicons/react/24/outline"; // types import type { Control } from "react-hook-form"; -import type { - IIssue, - IIssueLabels, - IssueResponse, - IState, - NestedKeyOf, - WorkspaceMember, -} from "types"; +import type { IIssue, IIssueLabels, IssueResponse, IState, NestedKeyOf } from "types"; import { TwitterPicker } from "react-color"; import IssuesListModal from "components/project/issues/IssuesListModal"; @@ -84,7 +77,7 @@ const IssueDetailSidebar: React.FC = ({ : null ); - const { data: people } = useSWR( + const { data: people } = useSWR( activeWorkspace ? WORKSPACE_MEMBERS(activeWorkspace.slug) : null, activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null ); diff --git a/apps/app/components/project/issues/issue-detail/comment/IssueCommentSection.tsx b/apps/app/components/project/issues/issue-detail/comment/IssueCommentSection.tsx index 364381ce1..fe07421f1 100644 --- a/apps/app/components/project/issues/issue-detail/comment/IssueCommentSection.tsx +++ b/apps/app/components/project/issues/issue-detail/comment/IssueCommentSection.tsx @@ -4,7 +4,7 @@ import { mutate } from "swr"; // react hook form import { useForm } from "react-hook-form"; // services -import issuesServices from "lib/services/issues.services"; +import issuesServices from "lib/services/issues.service"; // fetch keys import { PROJECT_ISSUES_COMMENTS } from "constants/fetch-keys"; // components diff --git a/apps/app/components/project/issues/my-issues/ChangeStateDropdown.tsx b/apps/app/components/project/issues/my-issues/ChangeStateDropdown.tsx index 1b84fd937..9e90c0e8d 100644 --- a/apps/app/components/project/issues/my-issues/ChangeStateDropdown.tsx +++ b/apps/app/components/project/issues/my-issues/ChangeStateDropdown.tsx @@ -8,7 +8,7 @@ import useUser from "lib/hooks/useUser"; import { addSpaceIfCamelCase, classNames } from "constants/common"; import { STATE_LIST } from "constants/fetch-keys"; // services -import stateServices from "lib/services/state.services"; +import stateServices from "lib/services/state.service"; // ui import { Listbox, Transition } from "@headlessui/react"; // types diff --git a/apps/app/components/project/memberInvitations.tsx b/apps/app/components/project/memberInvitations.tsx index 46804695f..4ecf8e1b3 100644 --- a/apps/app/components/project/memberInvitations.tsx +++ b/apps/app/components/project/memberInvitations.tsx @@ -28,7 +28,7 @@ type Props = { slug: string; invitationsRespond: string[]; handleInvitation: (project_invitation: any, action: "accepted" | "withdraw") => void; - setDeleteProject: React.Dispatch>; + setDeleteProject: (id: string | null) => void; }; const ProjectMemberInvitations: React.FC = ({ @@ -100,7 +100,7 @@ const ProjectMemberInvitations: React.FC = ({ diff --git a/apps/app/components/project/settings/ControlSettings.tsx b/apps/app/components/project/settings/ControlSettings.tsx index c60ec54f0..5ee4f58ec 100644 --- a/apps/app/components/project/settings/ControlSettings.tsx +++ b/apps/app/components/project/settings/ControlSettings.tsx @@ -15,7 +15,7 @@ import { Button } from "ui"; // icons import { CheckIcon, ChevronDownIcon } from "@heroicons/react/24/outline"; // types -import { IProject, WorkspaceMember } from "types"; +import { IProject } from "types"; // fetch-keys import { WORKSPACE_MEMBERS } from "constants/fetch-keys"; @@ -27,7 +27,7 @@ type Props = { const ControlSettings: React.FC = ({ control, isSubmitting }) => { const { activeWorkspace } = useUser(); - const { data: people } = useSWR( + const { data: people } = useSWR( activeWorkspace ? WORKSPACE_MEMBERS : null, activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null ); diff --git a/apps/app/components/project/settings/LabelsSettings.tsx b/apps/app/components/project/settings/LabelsSettings.tsx index 139c005b6..f67ed1d8b 100644 --- a/apps/app/components/project/settings/LabelsSettings.tsx +++ b/apps/app/components/project/settings/LabelsSettings.tsx @@ -7,7 +7,7 @@ import { Controller, SubmitHandler, useForm } from "react-hook-form"; // react-color import { TwitterPicker } from "react-color"; // services -import issuesServices from "lib/services/issues.services"; +import issuesServices from "lib/services/issues.service"; // hooks import useUser from "lib/hooks/useUser"; // headless ui diff --git a/apps/app/components/workspace/SendWorkspaceInvitationModal.tsx b/apps/app/components/workspace/SendWorkspaceInvitationModal.tsx index 8b9e5d99f..5e40c3bce 100644 --- a/apps/app/components/workspace/SendWorkspaceInvitationModal.tsx +++ b/apps/app/components/workspace/SendWorkspaceInvitationModal.tsx @@ -14,7 +14,7 @@ import { Button, Input, TextArea, Select } from "ui"; // hooks import useToast from "lib/hooks/useToast"; // types -import { WorkspaceMember } from "types"; +import { IWorkspaceMemberInvitation } from "types"; type Props = { isOpen: boolean; @@ -30,7 +30,7 @@ const ROLE = { 20: "Admin", }; -const defaultValues: Partial = { +const defaultValues: Partial = { email: "", role: 5, message: "", @@ -57,7 +57,7 @@ const SendWorkspaceInvitationModal: React.FC = ({ formState: { errors, isSubmitting }, handleSubmit, reset, - } = useForm({ + } = useForm({ defaultValues, reValidateMode: "onChange", mode: "all", diff --git a/apps/app/components/workspace/SingleInvitation.tsx b/apps/app/components/workspace/SingleInvitation.tsx index 27614e984..da3483c8a 100644 --- a/apps/app/components/workspace/SingleInvitation.tsx +++ b/apps/app/components/workspace/SingleInvitation.tsx @@ -3,10 +3,10 @@ import Image from "next/image"; // react import { useState } from "react"; // types -import { IWorkspaceInvitation } from "types"; +import { IWorkspaceMemberInvitation } from "types"; type Props = { - invitation: IWorkspaceInvitation; + invitation: IWorkspaceMemberInvitation; invitationsRespond: string[]; handleInvitation: any; }; diff --git a/apps/app/constants/api-routes.ts b/apps/app/constants/api-routes.ts index cafd0fb05..fdf3d0bc7 100644 --- a/apps/app/constants/api-routes.ts +++ b/apps/app/constants/api-routes.ts @@ -65,6 +65,8 @@ export const PROJECT_MEMBERS = (workspaceSlug: string, projectId: string) => `/api/workspaces/${workspaceSlug}/projects/${projectId}/members/`; export const PROJECT_MEMBER_DETAIL = (workspaceSlug: string, projectId: string, memberId: string) => `/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) => `/api/workspaces/${workspaceSlug}/projects/${projectId}/invitations/`; diff --git a/apps/app/constants/common.ts b/apps/app/constants/common.ts index 1937d65e3..dbc65ebcd 100644 --- a/apps/app/constants/common.ts +++ b/apps/app/constants/common.ts @@ -207,3 +207,12 @@ export const cosineSimilarity = (a: string, b: string) => { return dotProduct / Math.sqrt(magnitudeA * magnitudeB); }; + +export const createSimilarString = (str: string) => { + const shuffled = str + .split("") + .sort(() => Math.random() - 0.5) + .join(""); + + return shuffled; +}; diff --git a/apps/app/constants/fetch-keys.ts b/apps/app/constants/fetch-keys.ts index b8dfa5618..9e3611024 100644 --- a/apps/app/constants/fetch-keys.ts +++ b/apps/app/constants/fetch-keys.ts @@ -7,7 +7,7 @@ export const WORKSPACE_INVITATIONS = "WORKSPACE_INVITATIONS"; export const WORKSPACE_INVITATION = "WORKSPACE_INVITATION"; 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_INVITATIONS = "PROJECT_INVITATIONS"; diff --git a/apps/app/constants/index.ts b/apps/app/constants/index.ts index 9e319009d..603652e9f 100644 --- a/apps/app/constants/index.ts +++ b/apps/app/constants/index.ts @@ -6,3 +6,5 @@ export const ROLE = { 15: "Member", 20: "Admin", }; + +export const NETWORK_CHOICES = { "0": "Secret", "2": "Public" }; diff --git a/apps/app/constants/theme.context.constants.ts b/apps/app/constants/theme.context.constants.ts index d40e8138e..f170511ed 100644 --- a/apps/app/constants/theme.context.constants.ts +++ b/apps/app/constants/theme.context.constants.ts @@ -2,3 +2,5 @@ export const TOGGLE_SIDEBAR = "TOGGLE_SIDEBAR"; export const REHYDRATE_THEME = "REHYDRATE_THEME"; export const SET_ISSUE_VIEW = "SET_ISSUE_VIEW"; 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"; diff --git a/apps/app/contexts/theme.context.tsx b/apps/app/contexts/theme.context.tsx index 6df316ac7..bece44540 100644 --- a/apps/app/contexts/theme.context.tsx +++ b/apps/app/contexts/theme.context.tsx @@ -5,6 +5,8 @@ import { REHYDRATE_THEME, SET_ISSUE_VIEW, SET_GROUP_BY_PROPERTY, + SET_ORDER_BY_PROPERTY, + SET_FILTER_ISSUES, } from "constants/theme.context.constants"; // components import ToastAlert from "components/toast-alert"; @@ -12,30 +14,30 @@ import ToastAlert from "components/toast-alert"; export const themeContext = createContext({} as ContextType); // types -import type { IIssue, NestedKeyOf } from "types"; - -type Theme = { - collapsed: boolean; - issueView: "list" | "kanban" | null; - groupByProperty: NestedKeyOf | null; -}; +import type { IIssue, NestedKeyOf, ProjectViewTheme as Theme } from "types"; type ReducerActionType = { type: | typeof TOGGLE_SIDEBAR | typeof REHYDRATE_THEME | typeof SET_ISSUE_VIEW + | typeof SET_ORDER_BY_PROPERTY + | typeof SET_FILTER_ISSUES | typeof SET_GROUP_BY_PROPERTY; payload?: Partial; }; type ContextType = { collapsed: boolean; + orderBy: NestedKeyOf | null; issueView: "list" | "kanban" | null; groupByProperty: NestedKeyOf | null; + filterIssue: "activeIssue" | "backlogIssue" | null; toggleCollapsed: () => void; setIssueView: (display: "list" | "kanban") => void; setGroupByProperty: (property: NestedKeyOf | null) => void; + setOrderBy: (property: NestedKeyOf | null) => void; + setFilterIssue: (property: "activeIssue" | "backlogIssue" | null) => void; }; type StateType = Theme; @@ -45,6 +47,8 @@ export const initialState: StateType = { collapsed: false, issueView: "list", groupByProperty: null, + orderBy: null, + filterIssue: null, }; export const reducer: ReducerFunctionType = (state, action) => { @@ -87,6 +91,28 @@ export const reducer: ReducerFunctionType = (state, action) => { ...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: { return state; } @@ -120,6 +146,24 @@ export const ThemeContextProvider: React.FC<{ children: React.ReactNode }> = ({ }); }, []); + const setOrderBy = useCallback((property: NestedKeyOf | 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(() => { dispatch({ type: REHYDRATE_THEME, @@ -135,6 +179,10 @@ export const ThemeContextProvider: React.FC<{ children: React.ReactNode }> = ({ setIssueView, groupByProperty: state.groupByProperty, setGroupByProperty, + orderBy: state.orderBy, + setOrderBy, + filterIssue: state.filterIssue, + setFilterIssue, }} > diff --git a/apps/app/contexts/user.context.tsx b/apps/app/contexts/user.context.tsx index 6b52b0939..71f5a7ec8 100644 --- a/apps/app/contexts/user.context.tsx +++ b/apps/app/contexts/user.context.tsx @@ -6,9 +6,9 @@ import { useRouter } from "next/router"; import useSWR from "swr"; // services import userService from "lib/services/user.service"; -import issuesServices from "lib/services/issues.services"; -import stateServices from "lib/services/state.services"; -import sprintsServices from "lib/services/cycles.services"; +import issuesServices from "lib/services/issues.service"; +import stateServices from "lib/services/state.service"; +import sprintsServices from "lib/services/cycles.service"; import projectServices from "lib/services/project.service"; import workspaceService from "lib/services/workspace.service"; // constants diff --git a/apps/app/layouts/AppLayout.tsx b/apps/app/layouts/AppLayout.tsx index c8bd3daa5..dbdddf0a7 100644 --- a/apps/app/layouts/AppLayout.tsx +++ b/apps/app/layouts/AppLayout.tsx @@ -8,7 +8,7 @@ import useUser from "lib/hooks/useUser"; import Container from "layouts/Container"; import Sidebar from "layouts/Navbar/Sidebar"; // components -import CreateProjectModal from "components/project/CreateProjectModal"; +import CreateProjectModal from "components/project/create-project-modal"; // types import type { Props } from "./types"; diff --git a/apps/app/layouts/Navbar/Sidebar.tsx b/apps/app/layouts/Navbar/Sidebar.tsx index 70a510ec4..9f61f9ac8 100644 --- a/apps/app/layouts/Navbar/Sidebar.tsx +++ b/apps/app/layouts/Navbar/Sidebar.tsx @@ -10,8 +10,6 @@ import authenticationService from "lib/services/authentication.service"; import useUser from "lib/hooks/useUser"; import useTheme from "lib/hooks/useTheme"; import useToast from "lib/hooks/useToast"; -// components -import CreateProjectModal from "components/project/CreateProjectModal"; // headless ui import { Dialog, Disclosure, Menu, Transition } from "@headlessui/react"; // icons @@ -108,7 +106,6 @@ const userLinks = [ const Sidebar: React.FC = () => { const [sidebarOpen, setSidebarOpen] = useState(false); - const [isCreateProjectModal, setCreateProjectModal] = useState(false); const router = useRouter(); @@ -124,7 +121,6 @@ const Sidebar: React.FC = () => { return (