Refactoring Phase 1 (#199)

* style: added cta at the bottom of sidebar, added missing icons as well, showing dynamic workspace member count on workspace dropdown

* refractor: running parallel request,

made create/edit label function to async function

* fix: sidebar dropdown content going below kanban items

outside click detection in need help dropdown

* refractor: making parallel api calls

fix: create state input comes at bottom, create state input gets on focus automatically, form is getting submitted on enter click

* refactoring file structure and signin page

* style: changed text and added spinner for signing in loading

* refractor: removed unused type

* fix: my issue cta in profile page sending to 404 page

* fix: added new s3 bucket url in next.config.js file

increased image modal height

* packaging UI components

* eslint config

* eslint fixes

* refactoring changes

* build fixes

* minor fixes

* adding todo comments for reference

* refactor: cleared unused imports and re ordered imports

* refactor: removed unused imports

* fix: added workspace argument to useissues hook

* refactor: removed api-routes file, unnecessary constants

* refactor: created helpers folder, removed unnecessary constants

* refactor: new context for issue view

* refactoring issues page

* build fixes

* refactoring

* refactor: create issue modal

* refactor: module ui

* fix: sub-issues mutation

* fix: create more option in create issue modal

* description form debounce issue

* refactor: global component for assignees list

* fix: link module interface

* fix: priority icons and sub-issues count added

* fix: cycle mutation in issue details page

* fix: remove issue from cycle mutation

* fix: create issue modal in home page

* fix: removed unnecessary props

* fix: updated create issue form status

* fix: settings auth breaking

* refactor: issue details page

Co-authored-by: Dakshesh Jain <dakshesh.jain14@gmail.com>
Co-authored-by: Dakshesh Jain <65905942+dakshesh14@users.noreply.github.com>
Co-authored-by: venkatesh-soulpage <venkatesh.marreboyina@soulpageit.com>
Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia1001@gmail.com>
This commit is contained in:
sriram veeraghanta
2023-01-26 23:42:20 +05:30
committed by GitHub
parent 9134b0c543
commit 9075f9441c
322 changed files with 14149 additions and 21378 deletions

View File

@@ -0,0 +1,19 @@
import { useState, useEffect } from "react";
const useDebounce = (value: any, milliSeconds: number) => {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, milliSeconds);
return () => {
clearTimeout(handler);
};
}, [value, milliSeconds]);
return debouncedValue;
};
export default useDebounce;

View File

@@ -0,0 +1,96 @@
import { useState, useEffect, useCallback } from "react";
import useSWR from "swr";
// services
import issueServices from "services/issues.service";
// hooks
import useUser from "hooks/use-user";
// types
import { IssuePriorities, Properties } from "types";
const initialValues: Properties = {
key: true,
state: true,
assignee: true,
priority: false,
due_date: false,
cycle: false,
sub_issue_count: false,
};
// TODO: CHECK THIS LOGIC
const useIssuesProperties = (workspaceSlug?: string, projectId?: string) => {
const [properties, setProperties] = useState<Properties>(initialValues);
const { user } = useUser();
const { data: issueProperties, mutate: mutateIssueProperties } = useSWR<IssuePriorities>(
workspaceSlug && projectId
? `/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-properties/`
: null,
workspaceSlug && projectId
? () => issueServices.getIssueProperties(workspaceSlug, projectId)
: null
);
useEffect(() => {
if (!issueProperties || !workspaceSlug || !projectId || !user) return;
setProperties({ ...initialValues, ...issueProperties.properties });
if (Object.keys(issueProperties).length === 0)
issueServices.createIssueProperties(workspaceSlug, projectId, {
properties: { ...initialValues },
user: user.id,
});
else if (Object.keys(issueProperties?.properties).length === 0)
issueServices.patchIssueProperties(workspaceSlug, projectId, issueProperties.id, {
properties: { ...initialValues },
user: user.id,
});
}, [issueProperties, workspaceSlug, projectId, user]);
const updateIssueProperties = useCallback(
(key: keyof Properties) => {
if (!workspaceSlug || !user) return;
setProperties((prev) => ({ ...prev, [key]: !prev[key] }));
if (issueProperties && projectId) {
mutateIssueProperties(
(prev) =>
({
...prev,
properties: { ...prev?.properties, [key]: !prev?.properties?.[key] },
} as IssuePriorities),
false
);
if (Object.keys(issueProperties).length > 0) {
issueServices.patchIssueProperties(workspaceSlug, projectId, issueProperties.id, {
properties: {
...issueProperties.properties,
[key]: !issueProperties.properties[key],
},
user: user.id,
});
} else {
issueServices.createIssueProperties(workspaceSlug, projectId, {
properties: { ...initialValues },
user: user.id,
});
}
}
},
[workspaceSlug, projectId, issueProperties, user, mutateIssueProperties]
);
const newProperties: Properties = {
key: properties.key,
state: properties.state,
assignee: properties.assignee,
priority: properties.priority,
due_date: properties.due_date,
cycle: properties.cycle,
sub_issue_count: properties.sub_issue_count,
};
return [newProperties, updateIssueProperties] as const;
};
export default useIssuesProperties;

View File

@@ -0,0 +1,125 @@
import { useContext } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
// services
import stateService from "services/state.service";
// contexts
import { issueViewContext } from "contexts/issue-view.context";
// helpers
import { groupBy, orderArrayBy } from "helpers/array.helper";
// types
import { IIssue } from "types";
// fetch-keys
import { STATE_LIST } from "constants/fetch-keys";
// constants
import { PRIORITIES } from "constants/";
const useIssueView = (projectIssues: IIssue[]) => {
const {
issueView,
groupByProperty,
setGroupByProperty,
orderBy,
setOrderBy,
filterIssue,
setFilterIssue,
resetFilterToDefault,
setNewFilterDefaultView,
setIssueViewToKanban,
setIssueViewToList,
} = useContext(issueViewContext);
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { data: states } = useSWR(
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
workspaceSlug && projectId
? () => stateService.getStates(workspaceSlug as string, projectId as string)
: null
);
let groupedByIssues: {
[key: string]: IIssue[];
} = {
...(groupByProperty === "state_detail.name"
? Object.fromEntries(
states
?.sort((a, b) => a.sequence - b.sequence)
?.map((state) => [
state.name,
projectIssues.filter((issue) => issue.state === state.name) ?? [],
]) ?? []
)
: groupByProperty === "priority"
? Object.fromEntries(
PRIORITIES.map((priority) => [
priority,
projectIssues.filter((issue) => issue.priority === priority) ?? [],
])
)
: {}),
...groupBy(projectIssues ?? [], groupByProperty ?? ""),
};
if (orderBy) {
groupedByIssues = Object.fromEntries(
Object.entries(groupedByIssues).map(([key, value]) => [
key,
orderArrayBy(value, orderBy, "descending"),
])
);
}
if (filterIssue !== null) {
if (filterIssue === "activeIssue") {
const filteredStates = states?.filter(
(state) => state.group === "started" || state.group === "unstarted"
);
groupedByIssues = Object.fromEntries(
filteredStates
?.sort((a, b) => a.sequence - b.sequence)
?.map((state) => [
state.name,
projectIssues.filter((issue) => issue.state === state.id) ?? [],
]) ?? []
);
} else if (filterIssue === "backlogIssue") {
const filteredStates = states?.filter(
(state) => state.group === "backlog" || state.group === "cancelled"
);
groupedByIssues = Object.fromEntries(
filteredStates
?.sort((a, b) => a.sequence - b.sequence)
?.map((state) => [
state.name,
projectIssues.filter((issue) => issue.state === state.id) ?? [],
]) ?? []
);
}
}
if (groupByProperty === "priority" && orderBy === "priority") {
setOrderBy(null);
}
return {
groupedByIssues,
issueView,
groupByProperty,
setGroupByProperty,
orderBy,
setOrderBy,
filterIssue,
setFilterIssue,
resetFilterToDefault,
setNewFilterDefaultView,
setIssueViewToKanban,
setIssueViewToList,
} as const;
};
export default useIssueView;

View File

@@ -0,0 +1,22 @@
import useSWR from "swr";
// services
import userService from "services/user.service";
// types
import type { IIssue } from "types";
// fetch-keys
import { USER_ISSUE } from "constants/fetch-keys";
const useIssues = (workspaceSlug: string | undefined) => {
// API Fetching
const { data: myIssues, mutate: mutateMyIssues } = useSWR<IIssue[]>(
workspaceSlug ? USER_ISSUE(workspaceSlug as string) : null,
workspaceSlug ? () => userService.userIssues(workspaceSlug as string) : null
);
return {
myIssues: myIssues || [],
mutateMyIssues,
};
};
export default useIssues;

View File

@@ -0,0 +1,39 @@
import { useState, useEffect, useCallback } from "react";
// TODO: No Use of this
const useLocalStorage = <T,>(
key: string,
initialValue?: T extends Function ? never : T | (() => T)
) => {
const [value, setValue] = useState<T | string>("");
useEffect(() => {
const data = window.localStorage.getItem(key);
if (data !== null && data !== "undefined") setValue(JSON.parse(data));
else setValue(typeof initialValue === "function" ? initialValue() : initialValue);
}, [key, initialValue]);
const updateState = useCallback(
(value: T) => {
if (!value) window.localStorage.removeItem(key);
else window.localStorage.setItem(key, JSON.stringify(value));
setValue(value);
window.dispatchEvent(new Event(`local-storage-change-${key}`));
},
[key]
);
const reHydrateState = useCallback(() => {
const data = window.localStorage.getItem(key);
if (data !== null) setValue(JSON.parse(data));
}, [key]);
useEffect(() => {
window.addEventListener(`local-storage-change-${key}`, reHydrateState);
return () => window.removeEventListener(`local-storage-change-${key}`, reHydrateState);
}, [reHydrateState, key]);
return [value, updateState];
};
export default useLocalStorage;

View File

@@ -0,0 +1,122 @@
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
// services
import stateService from "services/state.service";
import userService from "services/user.service";
// hooks
import useUser from "hooks/use-user";
// helpers
import { groupBy } from "helpers/array.helper";
// types
import { Properties, NestedKeyOf, IIssue } from "types";
// fetch-keys
import { STATE_LIST } from "constants/fetch-keys";
// constants
import { PRIORITIES } from "constants/";
const initialValues: Properties = {
key: true,
state: true,
assignee: true,
priority: false,
due_date: false,
cycle: false,
sub_issue_count: false,
};
// TODO: Refactor this logic
const useMyIssuesProperties = (issues?: IIssue[]) => {
const [properties, setProperties] = useState<Properties>(initialValues);
const [groupByProperty, setGroupByProperty] = useState<NestedKeyOf<IIssue> | null>(null);
// FIXME: where this hook is used we may not have project id in the url
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { user } = useUser();
const { data: states } = useSWR(
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
workspaceSlug && projectId
? () => stateService.getStates(workspaceSlug as string, projectId as string)
: null
);
useEffect(() => {
if (!user) return;
setProperties({ ...initialValues, ...user.my_issues_prop?.properties });
setGroupByProperty(user.my_issues_prop?.groupBy ?? null);
}, [user]);
const groupedByIssues: {
[key: string]: IIssue[];
} = {
...(groupByProperty === "state_detail.name"
? Object.fromEntries(
states
?.sort((a, b) => a.sequence - b.sequence)
?.map((state) => [
state.name,
issues?.filter((issue) => issue.state === state.name) ?? [],
]) ?? []
)
: groupByProperty === "priority"
? Object.fromEntries(
PRIORITIES.map((priority) => [
priority,
issues?.filter((issue) => issue.priority === priority) ?? [],
])
)
: {}),
...groupBy(issues ?? [], groupByProperty ?? ""),
};
const setMyIssueProperty = (key: keyof Properties) => {
if (!user) return;
userService.updateUser({ my_issues_prop: { properties, groupBy: groupByProperty } });
setProperties((prevData) => ({
...prevData,
[key]: !prevData[key],
}));
localStorage.setItem(
"my_issues_prop",
JSON.stringify({
properties: {
...properties,
[key]: !properties[key],
},
groupBy: groupByProperty,
})
);
};
const setMyIssueGroupByProperty = (groupByProperty: NestedKeyOf<IIssue> | null) => {
if (!user) return;
userService.updateUser({ my_issues_prop: { properties, groupBy: groupByProperty } });
setGroupByProperty(groupByProperty);
localStorage.setItem(
"my_issues_prop",
JSON.stringify({ properties, groupBy: groupByProperty })
);
};
useEffect(() => {
const viewProps = localStorage.getItem("my_issues_prop");
if (viewProps) {
const { properties, groupBy } = JSON.parse(viewProps);
setProperties(properties);
setGroupByProperty(groupBy);
}
}, []);
return {
filteredIssues: groupedByIssues,
groupByProperty,
properties,
setMyIssueProperty,
setMyIssueGroupByProperty,
} as const;
};
export default useMyIssuesProperties;

View File

@@ -0,0 +1,19 @@
import React, { useEffect } from "react";
const useOutsideClickDetector = (ref: React.RefObject<HTMLElement>, callback: () => void) => {
const handleClick = (event: MouseEvent) => {
if (ref.current && !ref.current.contains(event.target as Node)) {
callback();
}
};
useEffect(() => {
document.addEventListener("click", handleClick);
return () => {
document.removeEventListener("click", handleClick);
};
});
};
export default useOutsideClickDetector;

View File

@@ -0,0 +1,33 @@
import useSWR from "swr";
// services
import projectService from "services/project.service";
// fetch-keys
import { PROJECT_MEMBERS } from "constants/fetch-keys";
// hooks
import useUser from "./use-user";
const useProjectMembers = (workspaceSlug: string, projectId: string) => {
const { user } = useUser();
// fetching project members
const { data: members } = useSWR(PROJECT_MEMBERS(projectId), () =>
projectService.projectMembers(workspaceSlug, projectId)
);
const isMember = members?.some((item: any) => item.member.id === (user as any)?.id);
const canEdit = members?.some(
(item) => (item.member.id === (user as any)?.id && item.role === 20) || item.role === 15
);
const canDelete = members?.some(
(item) => item.member.id === (user as any)?.id && item.role === 20
);
return {
members,
isMember,
canEdit,
canDelete,
};
};
export default useProjectMembers;

View File

@@ -0,0 +1,31 @@
import useSWR from "swr";
import { useRouter } from "next/router";
// types
import { IProject } from "types";
// services
import projectService from "services/project.service";
// constants
import { PROJECTS_LIST } from "constants/fetch-keys";
const useProjects = () => {
// router
const router = useRouter();
const { workspaceSlug } = router.query;
// api fetching
const { data: projects, mutate: mutateProjects } = useSWR<IProject[]>(
workspaceSlug ? PROJECTS_LIST(workspaceSlug as string) : null,
workspaceSlug ? () => projectService.getProjects(workspaceSlug as string) : null
);
const recentProjects = projects
?.sort((a, b) => Date.parse(`${a.updated_at}`) - Date.parse(`${b.updated_at}`))
.filter((_item, index) => index < 3);
return {
projects: projects || [],
recentProjects: recentProjects || [],
mutateProjects,
};
};
export default useProjects;

View File

@@ -0,0 +1,9 @@
import { useContext } from "react";
import { themeContext } from "contexts/theme.context";
const useTheme = () => {
const themeContextData = useContext(themeContext);
return themeContextData;
};
export default useTheme;

View File

@@ -0,0 +1,9 @@
import { useContext } from "react";
import { toastContext } from "contexts/toast.context";
const useToast = () => {
const toastContextData = useContext(toastContext);
return toastContextData;
};
export default useToast;

View File

@@ -0,0 +1,42 @@
import { useContext, useEffect } from "react";
import { useRouter } from "next/router";
// context
import { UserContext } from "contexts/user.context";
interface useUserOptions {
redirectTo?: string;
}
const useUser = (options: useUserOptions = {}) => {
// props
const { redirectTo = null } = options;
// context
const contextData = useContext(UserContext);
// router
const router = useRouter();
/**
* Checks for redirect url and user details from the API.
* if the user is not authenticated, user will be redirected
* to the provided redirectTo route.
*/
useEffect(() => {
if (!contextData?.user || !redirectTo) return;
if (!contextData?.user) {
if (redirectTo) {
router?.pathname !== redirectTo && router.push(redirectTo);
}
router?.pathname !== "/signin" && router.push("/signin");
}
if (contextData?.user) {
if (redirectTo) {
router?.pathname !== redirectTo && router.push(redirectTo);
}
}
}, [contextData?.user, redirectTo, router]);
return { ...contextData };
};
export default useUser;

View File

@@ -0,0 +1,37 @@
import { useEffect } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
// services
import workspaceService from "services/workspace.service";
// fetch-keys
import { WORKSPACE_DETAILS } from "constants/fetch-keys";
const useWorkspaceDetails = () => {
const router = useRouter();
const { workspaceSlug } = router.query;
// Fetching Workspace Details
const {
data: workspaceDetails,
error: workspaceDetailsError,
mutate: mutateWorkspaceDetails,
} = useSWR(
workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug as string) : null,
workspaceSlug ? () => workspaceService.getWorkspace(workspaceSlug as string) : null
);
useEffect(() => {
if (workspaceDetailsError?.status === 404) {
router.push("/404");
} else if (workspaceDetailsError) {
router.push("/error");
}
}, [workspaceDetailsError, router]);
return {
workspaceDetails,
workspaceDetailsError,
mutateWorkspaceDetails,
};
};
export default useWorkspaceDetails;

View File

@@ -0,0 +1,31 @@
import useSWR from "swr";
import { useRouter } from "next/router";
// types
import { IWorkspace } from "types";
// services
import workspaceService from "services/workspace.service";
// constants
import { USER_WORKSPACES } from "constants/fetch-keys";
const useWorkspaces = () => {
// router
const router = useRouter();
const { workspaceSlug } = router.query;
// API to fetch user information
const {
data = [],
error,
mutate,
} = useSWR<IWorkspace[]>(USER_WORKSPACES, () => workspaceService.userWorkspaces());
// active workspace
const activeWorkspace = data?.find((w) => w.slug === workspaceSlug);
return {
workspaces: data,
error,
activeWorkspace,
mutateWorkspaces: mutate,
};
};
export default useWorkspaces;