diff --git a/apps/app/components/project/CreateProjectModal.tsx b/apps/app/components/project/CreateProjectModal.tsx index 6402829dc..fe4047acb 100644 --- a/apps/app/components/project/CreateProjectModal.tsx +++ b/apps/app/components/project/CreateProjectModal.tsx @@ -8,6 +8,8 @@ import { Dialog, Transition } from "@headlessui/react"; // services import projectServices from "lib/services/project.service"; import workspaceService from "lib/services/workspace.service"; +// constants +import { NETWORK_CHOICES } from "constants/"; // fetch keys import { PROJECTS_LIST, WORKSPACE_MEMBERS } from "constants/fetch-keys"; // hooks @@ -15,8 +17,6 @@ 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"; @@ -25,11 +25,11 @@ type Props = { setIsOpen: React.Dispatch>; }; -const NETWORK_CHOICES = { "0": "Secret", "2": "Public" }; - const defaultValues: Partial = { name: "", + identifier: "", description: "", + network: 0, }; const IsGuestCondition: React.FC<{ @@ -62,7 +62,10 @@ const CreateProjectModal: React.FC = ({ isOpen, setIsOpen }) => { 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 { setToastAlert } = useToast(); @@ -79,8 +82,6 @@ const CreateProjectModal: React.FC = ({ isOpen, setIsOpen }) => { setValue, } = useForm({ defaultValues, - reValidateMode: "onChange", - mode: "all", }); const onSubmit = async (formData: IProject) => { @@ -111,6 +112,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,16 +125,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)); @@ -234,11 +226,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: { 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/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/lib/hooks/useIssuesFilter.tsx b/apps/app/lib/hooks/useIssuesFilter.tsx index 3ebf1a2c7..c598691fe 100644 --- a/apps/app/lib/hooks/useIssuesFilter.tsx +++ b/apps/app/lib/hooks/useIssuesFilter.tsx @@ -1,4 +1,3 @@ -import { useState } from "react"; // hooks import useTheme from "./useTheme"; import useUser from "./useUser"; @@ -7,14 +6,19 @@ import { groupBy, orderArrayBy } from "constants/common"; // constants import { PRIORITIES } from "constants/"; // types -import type { IssueResponse, IIssue, NestedKeyOf } from "types"; +import type { IssueResponse, IIssue } from "types"; const useIssuesFilter = (projectIssues?: IssueResponse) => { - const { issueView, setIssueView, groupByProperty, setGroupByProperty } = useTheme(); - - const [orderBy, setOrderBy] = useState | null>(null); - - const [filterIssue, setFilterIssue] = useState<"activeIssue" | "backlogIssue" | null>(null); + const { + issueView, + setIssueView, + groupByProperty, + setGroupByProperty, + orderBy, + setOrderBy, + filterIssue, + setFilterIssue, + } = useTheme(); const { states } = useUser(); @@ -52,29 +56,29 @@ const useIssuesFilter = (projectIssues?: IssueResponse) => { if (filterIssue !== null) { if (filterIssue === "activeIssue") { - groupedByIssues = Object.keys(groupedByIssues).reduce((acc, key) => { - const value = groupedByIssues[key]; - const filteredValue = value.filter( - (issue) => - issue.state_detail.group === "started" || issue.state_detail.group === "unstarted" - ); - if (filteredValue.length > 0) { - acc[key] = filteredValue; - } - return acc; - }, {} as typeof groupedByIssues); + const filteredStates = states?.filter( + (state) => state.group === "started" || state.group === "unstarted" + ); + groupedByIssues = Object.fromEntries( + filteredStates + ?.sort((a, b) => a.sequence - b.sequence) + ?.map((state) => [ + state.name, + projectIssues?.results.filter((issue) => issue.state === state.id) ?? [], + ]) ?? [] + ); } else if (filterIssue === "backlogIssue") { - groupedByIssues = Object.keys(groupedByIssues).reduce((acc, key) => { - const value = groupedByIssues[key]; - const filteredValue = value.filter( - (issue) => - issue.state_detail.group === "backlog" || issue.state_detail.group === "cancelled" - ); - if (filteredValue.length > 0) { - acc[key] = filteredValue; - } - return acc; - }, {} as typeof groupedByIssues); + const filteredStates = states?.filter( + (state) => state.group === "backlog" || state.group === "cancelled" + ); + groupedByIssues = Object.fromEntries( + filteredStates + ?.sort((a, b) => a.sequence - b.sequence) + ?.map((state) => [ + state.name, + projectIssues?.results.filter((issue) => issue.state === state.id) ?? [], + ]) ?? [] + ); } } diff --git a/apps/app/lib/services/project.service.ts b/apps/app/lib/services/project.service.ts index 9d0ab3d04..b7b4fe318 100644 --- a/apps/app/lib/services/project.service.ts +++ b/apps/app/lib/services/project.service.ts @@ -10,9 +10,12 @@ import { PROJECT_MEMBERS, PROJECT_MEMBER_DETAIL, USER_PROJECT_INVITATIONS, + PROJECT_VIEW_ENDPOINT, } from "constants/api-routes"; // services import APIService from "lib/services/api.service"; +// types +import type { ProjectViewTheme } from "types"; const { NEXT_PUBLIC_API_BASE_URL } = process.env; @@ -177,6 +180,7 @@ class ProjectServices extends APIService { throw error?.response?.data; }); } + async deleteProjectInvitation( workspace_slug: string, project_id: string, @@ -190,6 +194,20 @@ class ProjectServices extends APIService { throw error?.response?.data; }); } + + async setProjectView( + workspace_slug: string, + project_id: string, + data: ProjectViewTheme + ): Promise { + await this.patch(PROJECT_VIEW_ENDPOINT(workspace_slug, project_id), data) + .then((response) => { + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } } export default new ProjectServices(); diff --git a/apps/app/pages/create-workspace.tsx b/apps/app/pages/create-workspace.tsx index 1c5e8dc3e..e996372b6 100644 --- a/apps/app/pages/create-workspace.tsx +++ b/apps/app/pages/create-workspace.tsx @@ -8,6 +8,8 @@ import { useForm } from "react-hook-form"; import workspaceService from "lib/services/workspace.service"; // hooks import useUser from "lib/hooks/useUser"; +// hoc +import withAuth from "lib/hoc/withAuthWrapper"; // layouts import DefaultLayout from "layouts/DefaultLayout"; // ui @@ -144,4 +146,4 @@ const CreateWorkspace: NextPage = () => { ); }; -export default CreateWorkspace; +export default withAuth(CreateWorkspace); diff --git a/apps/app/pages/invitations.tsx b/apps/app/pages/invitations.tsx index 25bb7d324..155128770 100644 --- a/apps/app/pages/invitations.tsx +++ b/apps/app/pages/invitations.tsx @@ -11,6 +11,8 @@ import userService from "lib/services/user.service"; import useUser from "lib/hooks/useUser"; // constants import { USER_WORKSPACE_INVITATIONS } from "constants/api-routes"; +// hoc +import withAuth from "lib/hoc/withAuthWrapper"; // layouts import DefaultLayout from "layouts/DefaultLayout"; // components @@ -204,4 +206,4 @@ const OnBoard: NextPage = () => { ); }; -export default OnBoard; +export default withAuth(OnBoard); diff --git a/apps/app/pages/me/my-issues.tsx b/apps/app/pages/me/my-issues.tsx index 3f3620c66..ee58a0016 100644 --- a/apps/app/pages/me/my-issues.tsx +++ b/apps/app/pages/me/my-issues.tsx @@ -19,6 +19,8 @@ import { classNames } from "constants/common"; // services import userService from "lib/services/user.service"; import issuesServices from "lib/services/issues.services"; +// hoc +import withAuth from "lib/hoc/withAuthWrapper"; // components import ChangeStateDropdown from "components/project/issues/my-issues/ChangeStateDropdown"; // icons @@ -278,4 +280,4 @@ const MyIssues: NextPage = () => { ); }; -export default MyIssues; +export default withAuth(MyIssues); diff --git a/apps/app/pages/me/profile.tsx b/apps/app/pages/me/profile.tsx index 3c3350044..15850d050 100644 --- a/apps/app/pages/me/profile.tsx +++ b/apps/app/pages/me/profile.tsx @@ -8,6 +8,8 @@ import { useForm } from "react-hook-form"; import Dropzone, { useDropzone } from "react-dropzone"; // hooks import useUser from "lib/hooks/useUser"; +// hoc +import withAuth from "lib/hoc/withAuthWrapper"; // layouts import AdminLayout from "layouts/AdminLayout"; // services @@ -307,4 +309,4 @@ const Profile: NextPage = () => { ); }; -export default Profile; +export default withAuth(Profile); diff --git a/apps/app/pages/projects/[projectId]/cycles.tsx b/apps/app/pages/projects/[projectId]/cycles.tsx index 4dd67f129..8aae6f8fa 100644 --- a/apps/app/pages/projects/[projectId]/cycles.tsx +++ b/apps/app/pages/projects/[projectId]/cycles.tsx @@ -11,6 +11,8 @@ import sprintService from "lib/services/cycles.services"; import useUser from "lib/hooks/useUser"; // fetching keys import { CYCLE_ISSUES, CYCLE_LIST } from "constants/fetch-keys"; +// hoc +import withAuth from "lib/hoc/withAuthWrapper"; // layouts import AdminLayout from "layouts/AdminLayout"; // components @@ -258,4 +260,4 @@ const ProjectSprints: NextPage = () => { ); }; -export default ProjectSprints; +export default withAuth(ProjectSprints); diff --git a/apps/app/pages/projects/[projectId]/issues/[issueId].tsx b/apps/app/pages/projects/[projectId]/issues/[issueId].tsx index ef5a6945b..feee4131a 100644 --- a/apps/app/pages/projects/[projectId]/issues/[issueId].tsx +++ b/apps/app/pages/projects/[projectId]/issues/[issueId].tsx @@ -22,6 +22,8 @@ import { } from "constants/fetch-keys"; // hooks import useUser from "lib/hooks/useUser"; +// hoc +import withAuth from "lib/hoc/withAuthWrapper"; // layouts import AdminLayout from "layouts/AdminLayout"; // components @@ -606,4 +608,4 @@ const IssueDetail: NextPage = () => { ); }; -export default IssueDetail; +export default withAuth(IssueDetail); diff --git a/apps/app/pages/projects/[projectId]/members.tsx b/apps/app/pages/projects/[projectId]/members.tsx index f6d0ec9b4..101e3b9f8 100644 --- a/apps/app/pages/projects/[projectId]/members.tsx +++ b/apps/app/pages/projects/[projectId]/members.tsx @@ -14,6 +14,8 @@ import useUser from "lib/hooks/useUser"; import useToast from "lib/hooks/useToast"; // fetching keys import { PROJECT_MEMBERS, PROJECT_INVITATIONS } from "constants/fetch-keys"; +// hoc +import withAuth from "lib/hoc/withAuthWrapper"; // layouts import AdminLayout from "layouts/AdminLayout"; // components @@ -313,4 +315,4 @@ const ProjectMembers: NextPage = () => { ); }; -export default ProjectMembers; +export default withAuth(ProjectMembers); diff --git a/apps/app/pages/projects/[projectId]/settings.tsx b/apps/app/pages/projects/[projectId]/settings.tsx index 264ec1b3a..88645b458 100644 --- a/apps/app/pages/projects/[projectId]/settings.tsx +++ b/apps/app/pages/projects/[projectId]/settings.tsx @@ -10,6 +10,8 @@ import useSWR from "swr"; import { useForm, Controller } from "react-hook-form"; // headless ui import { Listbox, Tab, Transition } from "@headlessui/react"; +// hoc +import withAuth from "lib/hoc/withAuthWrapper"; // layouts import AdminLayout from "layouts/AdminLayout"; // service @@ -20,6 +22,8 @@ import useUser from "lib/hooks/useUser"; import useToast from "lib/hooks/useToast"; // fetch keys import { PROJECT_DETAILS, PROJECTS_LIST, WORKSPACE_MEMBERS } from "constants/fetch-keys"; +// constants +import { NETWORK_CHOICES } from "constants/"; // commons import { addSpaceIfCamelCase, debounce } from "constants/common"; // components @@ -44,8 +48,6 @@ const defaultValues: Partial = { description: "", }; -const NETWORK_CHOICES = { "0": "Secret", "2": "Public" }; - const ProjectSettings: NextPage = () => { const { register, @@ -71,7 +73,7 @@ const ProjectSettings: NextPage = () => { const { setToastAlert } = useToast(); const { data: projectDetails } = useSWR( - activeWorkspace && projectId ? PROJECT_DETAILS : null, + activeWorkspace && projectId ? PROJECT_DETAILS(projectId as string) : null, activeWorkspace ? () => projectServices.getProject(activeWorkspace.slug, projectId as string) : null @@ -99,7 +101,7 @@ const ProjectSettings: NextPage = () => { }, [projectDetails, reset]); const onSubmit = async (formData: IProject) => { - if (!activeWorkspace) return; + if (!activeWorkspace || !projectId) return; const payload: Partial = { name: formData.name, network: formData.network, @@ -111,7 +113,11 @@ const ProjectSettings: NextPage = () => { await projectServices .updateProject(activeWorkspace.slug, projectId as string, payload) .then((res) => { - mutate(PROJECT_DETAILS, (prevData) => ({ ...prevData, ...res }), false); + mutate( + PROJECT_DETAILS(projectId as string), + (prevData) => ({ ...prevData, ...res }), + false + ); mutate( PROJECTS_LIST(activeWorkspace.slug), (prevData) => { @@ -253,9 +259,6 @@ const ProjectSettings: NextPage = () => { register={register} label="Description" placeholder="Enter project description" - validations={{ - required: "Description is required", - }} />
@@ -547,4 +550,4 @@ const ProjectSettings: NextPage = () => { ); }; -export default ProjectSettings; +export default withAuth(ProjectSettings); diff --git a/apps/app/pages/projects/index.tsx b/apps/app/pages/projects/index.tsx index f55b07abb..39cc8d360 100644 --- a/apps/app/pages/projects/index.tsx +++ b/apps/app/pages/projects/index.tsx @@ -3,6 +3,8 @@ import React, { useEffect, useState } from "react"; import type { NextPage } from "next"; // hooks import useUser from "lib/hooks/useUser"; +// hoc +import withAuth from "lib/hoc/withAuthWrapper"; // layouts import AdminLayout from "layouts/AdminLayout"; // components @@ -129,4 +131,4 @@ const Projects: NextPage = () => { ); }; -export default Projects; +export default withAuth(Projects); diff --git a/apps/app/pages/workspace/settings.tsx b/apps/app/pages/workspace/settings.tsx index eaa20d1c9..478d87f65 100644 --- a/apps/app/pages/workspace/settings.tsx +++ b/apps/app/pages/workspace/settings.tsx @@ -8,9 +8,10 @@ import Dropzone from "react-dropzone"; // services import workspaceService from "lib/services/workspace.service"; import fileServices from "lib/services/file.services"; +// hoc +import withAuth from "lib/hoc/withAuthWrapper"; // layouts import AdminLayout from "layouts/AdminLayout"; - // hooks import useUser from "lib/hooks/useUser"; import useToast from "lib/hooks/useToast"; @@ -232,4 +233,4 @@ const WorkspaceSettings = () => { ); }; -export default WorkspaceSettings; +export default withAuth(WorkspaceSettings); diff --git a/apps/app/types/projects.d.ts b/apps/app/types/projects.d.ts index da80eec29..6b421b010 100644 --- a/apps/app/types/projects.d.ts +++ b/apps/app/types/projects.d.ts @@ -15,3 +15,11 @@ export interface IProject { created_by: string; updated_by: string; } + +type ProjectViewTheme = { + collapsed: boolean; + issueView: "list" | "kanban" | null; + groupByProperty: NestedKeyOf | null; + filterIssue: "activeIssue" | "backlogIssue" | null; + orderBy: NestedKeyOf | null; +};