forked from github/plane
feat: user profile analytics, views and filters (#1698)
* feat: user profile overview * chore: profile sidebar designed * feat: user issues filters and view options * refactor: filters * refactor: mutation logic * fix: percentage calculation logic and sidebar shadow
This commit is contained in:
parent
8930840a76
commit
10f145f85c
@ -46,16 +46,7 @@ import { copyTextToClipboard } from "helpers/string.helper";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, IIssueViewProps, ISubIssueResponse, UserAuth } from "types";
|
||||
// fetch-keys
|
||||
import {
|
||||
CYCLE_DETAILS,
|
||||
CYCLE_ISSUES_WITH_PARAMS,
|
||||
MODULE_DETAILS,
|
||||
MODULE_ISSUES_WITH_PARAMS,
|
||||
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||
SUB_ISSUES,
|
||||
USER_ISSUES,
|
||||
VIEW_ISSUES,
|
||||
} from "constants/fetch-keys";
|
||||
import { CYCLE_DETAILS, MODULE_DETAILS, SUB_ISSUES } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
type?: string;
|
||||
@ -99,10 +90,10 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
|
||||
const actionSectionRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const { groupByProperty: selectedGroup, orderBy, params, properties } = viewProps;
|
||||
const { groupByProperty: selectedGroup, orderBy, properties, mutateIssues } = viewProps;
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
@ -110,16 +101,6 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
(formData: Partial<IIssue>, issue: IIssue) => {
|
||||
if (!workspaceSlug || !issue) return;
|
||||
|
||||
const fetchKey = cycleId
|
||||
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params)
|
||||
: moduleId
|
||||
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params)
|
||||
: viewId
|
||||
? VIEW_ISSUES(viewId.toString(), params)
|
||||
: router.pathname.includes("my-issues")
|
||||
? USER_ISSUES(workspaceSlug.toString(), params)
|
||||
: PROJECT_ISSUES_LIST_WITH_PARAMS(issue.project.toString(), params);
|
||||
|
||||
if (issue.parent) {
|
||||
mutate<ISubIssueResponse>(
|
||||
SUB_ISSUES(issue.parent.toString()),
|
||||
@ -142,13 +123,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
false
|
||||
);
|
||||
} else {
|
||||
mutate<
|
||||
| {
|
||||
[key: string]: IIssue[];
|
||||
}
|
||||
| IIssue[]
|
||||
>(
|
||||
fetchKey,
|
||||
mutateIssues(
|
||||
(prevData) =>
|
||||
handleIssuesMutation(
|
||||
formData,
|
||||
@ -165,7 +140,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
issuesService
|
||||
.patchIssue(workspaceSlug as string, issue.project, issue.id, formData, user)
|
||||
.then(() => {
|
||||
mutate(fetchKey);
|
||||
mutateIssues();
|
||||
|
||||
if (cycleId) mutate(CYCLE_DETAILS(cycleId as string));
|
||||
if (moduleId) mutate(MODULE_DETAILS(moduleId as string));
|
||||
@ -175,13 +150,11 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
workspaceSlug,
|
||||
cycleId,
|
||||
moduleId,
|
||||
viewId,
|
||||
groupTitle,
|
||||
index,
|
||||
selectedGroup,
|
||||
mutateIssues,
|
||||
orderBy,
|
||||
params,
|
||||
router,
|
||||
user,
|
||||
]
|
||||
);
|
||||
|
@ -79,6 +79,7 @@ export const IssuesView: React.FC<Props> = ({
|
||||
|
||||
const {
|
||||
groupedByIssues,
|
||||
mutateIssues,
|
||||
issueView,
|
||||
groupByProperty: selectedGroup,
|
||||
orderBy,
|
||||
@ -525,6 +526,7 @@ export const IssuesView: React.FC<Props> = ({
|
||||
groupedIssues: groupedByIssues,
|
||||
isEmpty,
|
||||
issueView,
|
||||
mutateIssues,
|
||||
orderBy,
|
||||
params,
|
||||
properties,
|
||||
|
@ -35,25 +35,9 @@ import { LayerDiagonalIcon } from "components/icons";
|
||||
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
|
||||
import { handleIssuesMutation } from "constants/issue";
|
||||
// types
|
||||
import {
|
||||
ICurrentUserResponse,
|
||||
IIssue,
|
||||
IIssueViewProps,
|
||||
ISubIssueResponse,
|
||||
Properties,
|
||||
UserAuth,
|
||||
} from "types";
|
||||
import { ICurrentUserResponse, IIssue, IIssueViewProps, ISubIssueResponse, UserAuth } from "types";
|
||||
// fetch-keys
|
||||
import {
|
||||
CYCLE_DETAILS,
|
||||
CYCLE_ISSUES_WITH_PARAMS,
|
||||
MODULE_DETAILS,
|
||||
MODULE_ISSUES_WITH_PARAMS,
|
||||
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||
SUB_ISSUES,
|
||||
USER_ISSUES,
|
||||
VIEW_ISSUES,
|
||||
} from "constants/fetch-keys";
|
||||
import { CYCLE_DETAILS, MODULE_DETAILS, SUB_ISSUES } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
type?: string;
|
||||
@ -89,27 +73,17 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 });
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||
const isArchivedIssues = router.pathname.includes("archived-issues");
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { groupByProperty: selectedGroup, orderBy, params, properties } = viewProps;
|
||||
const { groupByProperty: selectedGroup, orderBy, properties, mutateIssues } = viewProps;
|
||||
|
||||
const partialUpdateIssue = useCallback(
|
||||
(formData: Partial<IIssue>, issue: IIssue) => {
|
||||
if (!workspaceSlug || !issue) return;
|
||||
|
||||
const fetchKey = cycleId
|
||||
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params)
|
||||
: moduleId
|
||||
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params)
|
||||
: viewId
|
||||
? VIEW_ISSUES(viewId.toString(), params)
|
||||
: router.pathname.includes("my-issues")
|
||||
? USER_ISSUES(workspaceSlug.toString(), params)
|
||||
: PROJECT_ISSUES_LIST_WITH_PARAMS(issue.project.toString(), params);
|
||||
|
||||
if (issue.parent) {
|
||||
mutate<ISubIssueResponse>(
|
||||
SUB_ISSUES(issue.parent.toString()),
|
||||
@ -132,13 +106,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
false
|
||||
);
|
||||
} else {
|
||||
mutate<
|
||||
| {
|
||||
[key: string]: IIssue[];
|
||||
}
|
||||
| IIssue[]
|
||||
>(
|
||||
fetchKey,
|
||||
mutateIssues(
|
||||
(prevData) =>
|
||||
handleIssuesMutation(
|
||||
formData,
|
||||
@ -155,7 +123,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
issuesService
|
||||
.patchIssue(workspaceSlug as string, issue.project, issue.id, formData, user)
|
||||
.then(() => {
|
||||
mutate(fetchKey);
|
||||
mutateIssues();
|
||||
|
||||
if (cycleId) mutate(CYCLE_DETAILS(cycleId as string));
|
||||
if (moduleId) mutate(MODULE_DETAILS(moduleId as string));
|
||||
@ -165,13 +133,11 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
workspaceSlug,
|
||||
cycleId,
|
||||
moduleId,
|
||||
viewId,
|
||||
groupTitle,
|
||||
index,
|
||||
selectedGroup,
|
||||
mutateIssues,
|
||||
orderBy,
|
||||
params,
|
||||
router,
|
||||
user,
|
||||
]
|
||||
);
|
||||
|
@ -10,313 +10,14 @@ import issuesService from "services/issues.service";
|
||||
import { CommentCard } from "components/issues/comment";
|
||||
// ui
|
||||
import { Icon, Loader } from "components/ui";
|
||||
// icons
|
||||
import { Squares2X2Icon } from "@heroicons/react/24/outline";
|
||||
import { BlockedIcon, BlockerIcon } from "components/icons";
|
||||
// helpers
|
||||
import { renderShortDateWithYearFormat, timeAgo } from "helpers/date-time.helper";
|
||||
import { capitalizeFirstLetter } from "helpers/string.helper";
|
||||
import { timeAgo } from "helpers/date-time.helper";
|
||||
import { activityDetails } from "helpers/activity.helper";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssueActivity, IIssueComment } from "types";
|
||||
import { ICurrentUserResponse, IIssueComment } from "types";
|
||||
// fetch-keys
|
||||
import { PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
|
||||
|
||||
const activityDetails: {
|
||||
[key: string]: {
|
||||
message: (activity: IIssueActivity) => React.ReactNode;
|
||||
icon: React.ReactNode;
|
||||
};
|
||||
} = {
|
||||
assignees: {
|
||||
message: (activity) => {
|
||||
if (activity.old_value === "")
|
||||
return (
|
||||
<>
|
||||
added a new assignee{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
|
||||
</>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<>
|
||||
removed the assignee{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.old_value}</span>.
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <Icon iconName="group" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
archived_at: {
|
||||
message: (activity) => {
|
||||
if (activity.new_value === "restore") return "restored the issue.";
|
||||
else return "archived the issue.";
|
||||
},
|
||||
icon: <Icon iconName="archive" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
attachment: {
|
||||
message: (activity) => {
|
||||
if (activity.verb === "created")
|
||||
return (
|
||||
<>
|
||||
uploaded a new{" "}
|
||||
<a
|
||||
href={`${activity.new_value}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||
>
|
||||
attachment
|
||||
<Icon iconName="launch" className="!text-xs" />
|
||||
</a>
|
||||
</>
|
||||
);
|
||||
else return "removed an attachment.";
|
||||
},
|
||||
icon: <Icon iconName="attach_file" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
blocking: {
|
||||
message: (activity) => {
|
||||
if (activity.old_value === "")
|
||||
return (
|
||||
<>
|
||||
marked this issue is blocking issue{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
|
||||
</>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<>
|
||||
removed the blocking issue{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.old_value}</span>.
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <BlockerIcon height="12" width="12" color="#6b7280" />,
|
||||
},
|
||||
blocks: {
|
||||
message: (activity) => {
|
||||
if (activity.old_value === "")
|
||||
return (
|
||||
<>
|
||||
marked this issue is being blocked by{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
|
||||
</>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<>
|
||||
removed this issue being blocked by issue{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.old_value}</span>.
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <BlockedIcon height="12" width="12" color="#6b7280" />,
|
||||
},
|
||||
cycles: {
|
||||
message: (activity) => {
|
||||
if (activity.verb === "created")
|
||||
return (
|
||||
<>
|
||||
added this issue to the cycle{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
|
||||
</>
|
||||
);
|
||||
else if (activity.verb === "updated")
|
||||
return (
|
||||
<>
|
||||
set the cycle to{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
|
||||
</>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<>
|
||||
removed the issue from the cycle{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.old_value}</span>.
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <Icon iconName="contrast" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
description: {
|
||||
message: (activity) => "updated the description.",
|
||||
icon: <Icon iconName="chat" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
estimate_point: {
|
||||
message: (activity) => {
|
||||
if (!activity.new_value) return "removed the estimate point.";
|
||||
else
|
||||
return (
|
||||
<>
|
||||
set the estimate point to{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <Icon iconName="change_history" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
labels: {
|
||||
message: (activity) => {
|
||||
if (activity.old_value === "")
|
||||
return (
|
||||
<>
|
||||
added a new label{" "}
|
||||
<span className="inline-flex items-center gap-3 rounded-full border border-custom-border-300 px-2 py-0.5 text-xs">
|
||||
<span
|
||||
className="h-1.5 w-1.5 rounded-full"
|
||||
style={{
|
||||
backgroundColor: "#000000",
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<>
|
||||
removed the label{" "}
|
||||
<span className="inline-flex items-center gap-3 rounded-full border border-custom-border-300 px-2 py-0.5 text-xs">
|
||||
<span
|
||||
className="h-1.5 w-1.5 rounded-full"
|
||||
style={{
|
||||
backgroundColor: "#000000",
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="font-medium text-custom-text-100">{activity.old_value}</span>
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <Icon iconName="sell" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
link: {
|
||||
message: (activity) => {
|
||||
if (activity.verb === "created")
|
||||
return (
|
||||
<>
|
||||
added this{" "}
|
||||
<a
|
||||
href={`${activity.new_value}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||
>
|
||||
link
|
||||
<Icon iconName="launch" className="!text-xs" />
|
||||
</a>{" "}
|
||||
to the issue.
|
||||
</>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<>
|
||||
removed this{" "}
|
||||
<a
|
||||
href={`${activity.old_value}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||
>
|
||||
link
|
||||
<Icon iconName="launch" className="!text-xs" />
|
||||
</a>{" "}
|
||||
from the issue.
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <Icon iconName="link" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
modules: {
|
||||
message: (activity) => {
|
||||
if (activity.verb === "created")
|
||||
return (
|
||||
<>
|
||||
added this issue to the module{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
|
||||
</>
|
||||
);
|
||||
else if (activity.verb === "updated")
|
||||
return (
|
||||
<>
|
||||
set the module to{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
|
||||
</>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<>
|
||||
removed the issue from the module{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.old_value}</span>.
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <Icon iconName="dataset" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
name: {
|
||||
message: (activity) => `set the name to ${activity.new_value}.`,
|
||||
icon: <Icon iconName="chat" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
parent: {
|
||||
message: (activity) => {
|
||||
if (!activity.new_value)
|
||||
return (
|
||||
<>
|
||||
removed the parent{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.old_value}</span>.
|
||||
</>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<>
|
||||
set the parent to{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <Icon iconName="supervised_user_circle" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
priority: {
|
||||
message: (activity) => (
|
||||
<>
|
||||
set the priority to{" "}
|
||||
<span className="font-medium text-custom-text-100">
|
||||
{activity.new_value ? capitalizeFirstLetter(activity.new_value) : "None"}
|
||||
</span>
|
||||
.
|
||||
</>
|
||||
),
|
||||
icon: <Icon iconName="signal_cellular_alt" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
state: {
|
||||
message: (activity) => (
|
||||
<>
|
||||
set the state to{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
|
||||
</>
|
||||
),
|
||||
icon: <Squares2X2Icon className="h-3 w-3" aria-hidden="true" />,
|
||||
},
|
||||
target_date: {
|
||||
message: (activity) => {
|
||||
if (!activity.new_value) return "removed the due date.";
|
||||
else
|
||||
return (
|
||||
<>
|
||||
set the due date to{" "}
|
||||
<span className="font-medium text-custom-text-100">
|
||||
{renderShortDateWithYearFormat(activity.new_value)}
|
||||
</span>
|
||||
.
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <Icon iconName="calendar_today" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
};
|
||||
|
||||
type Props = {
|
||||
issueId: string;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
|
@ -25,7 +25,6 @@ import type { IIssue } from "types";
|
||||
// fetch-keys
|
||||
import {
|
||||
PROJECT_ISSUES_DETAILS,
|
||||
PROJECT_ISSUES_LIST,
|
||||
USER_ISSUE,
|
||||
SUB_ISSUES,
|
||||
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||
@ -40,11 +39,11 @@ import {
|
||||
import { INBOX_ISSUE_SOURCE } from "constants/inbox";
|
||||
|
||||
export interface IssuesModalProps {
|
||||
isOpen: boolean;
|
||||
handleClose: () => void;
|
||||
data?: IIssue | null;
|
||||
prePopulateData?: Partial<IIssue>;
|
||||
handleClose: () => void;
|
||||
isOpen: boolean;
|
||||
isUpdatingSingleIssue?: boolean;
|
||||
prePopulateData?: Partial<IIssue>;
|
||||
fieldsToShow?: (
|
||||
| "project"
|
||||
| "name"
|
||||
@ -58,15 +57,17 @@ export interface IssuesModalProps {
|
||||
| "parent"
|
||||
| "all"
|
||||
)[];
|
||||
onSubmit?: (data: Partial<IIssue>) => Promise<void>;
|
||||
}
|
||||
|
||||
export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
||||
isOpen,
|
||||
handleClose,
|
||||
data,
|
||||
prePopulateData,
|
||||
handleClose,
|
||||
isOpen,
|
||||
isUpdatingSingleIssue = false,
|
||||
prePopulateData,
|
||||
fieldsToShow = ["all"],
|
||||
onSubmit,
|
||||
}) => {
|
||||
// states
|
||||
const [createMore, setCreateMore] = useState(false);
|
||||
@ -95,9 +96,14 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (data && data.project) {
|
||||
setActiveProject(data.project);
|
||||
return;
|
||||
}
|
||||
|
||||
if (projects && projects.length > 0 && !activeProject)
|
||||
setActiveProject(projects?.find((p) => p.id === projectId)?.id ?? projects?.[0].id ?? null);
|
||||
}, [activeProject, projectId, projects]);
|
||||
}, [activeProject, data, projectId, projects]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
@ -306,6 +312,8 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
||||
|
||||
if (!data) await createIssue(payload);
|
||||
else await updateIssue(payload);
|
||||
|
||||
if (onSubmit) await onSubmit(payload);
|
||||
};
|
||||
|
||||
if (!projects || projects.length === 0) return null;
|
||||
|
@ -15,12 +15,12 @@ import useUserAuth from "hooks/use-user-auth";
|
||||
// components
|
||||
import { AllViews, FiltersList } from "components/core";
|
||||
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||
import { CreateUpdateViewModal } from "components/views";
|
||||
// helpers
|
||||
import { orderArrayBy } from "helpers/array.helper";
|
||||
// types
|
||||
import { IIssue, IIssueFilterOptions } from "types";
|
||||
// fetch-keys
|
||||
import { USER_ISSUES, WORKSPACE_LABELS } from "constants/fetch-keys";
|
||||
import { orderArrayBy } from "helpers/array.helper";
|
||||
|
||||
type Props = {
|
||||
openIssuesListModal?: () => void;
|
||||
@ -33,7 +33,6 @@ export const MyIssuesView: React.FC<Props> = ({
|
||||
}) => {
|
||||
// create issue modal
|
||||
const [createIssueModal, setCreateIssueModal] = useState(false);
|
||||
const [createViewModal, setCreateViewModal] = useState<any>(null);
|
||||
const [preloadedData, setPreloadedData] = useState<
|
||||
(Partial<IIssue> & { actionType: "createIssue" | "edit" | "delete" }) | undefined
|
||||
>(undefined);
|
||||
@ -56,15 +55,15 @@ export const MyIssuesView: React.FC<Props> = ({
|
||||
|
||||
const { user } = useUserAuth();
|
||||
|
||||
const { groupedIssues, isEmpty, params } = useMyIssues(workspaceSlug?.toString());
|
||||
const { groupedIssues, mutateMyIssues, isEmpty, params } = useMyIssues(workspaceSlug?.toString());
|
||||
const { filters, setFilters, issueView, groupBy, orderBy, properties, showEmptyGroups } =
|
||||
useMyIssuesFilters(workspaceSlug?.toString());
|
||||
|
||||
const { data: labels } = useSWR(
|
||||
workspaceSlug && (filters.labels ?? []).length > 0
|
||||
workspaceSlug && (filters?.labels ?? []).length > 0
|
||||
? WORKSPACE_LABELS(workspaceSlug.toString())
|
||||
: null,
|
||||
workspaceSlug && (filters.labels ?? []).length > 0
|
||||
workspaceSlug && (filters?.labels ?? []).length > 0
|
||||
? () => issuesService.getWorkspaceLabels(workspaceSlug.toString())
|
||||
: null
|
||||
);
|
||||
@ -80,7 +79,6 @@ export const MyIssuesView: React.FC<Props> = ({
|
||||
const handleOnDragEnd = useCallback(
|
||||
async (result: DropResult) => {
|
||||
setTrashBox(false);
|
||||
console.log(result);
|
||||
|
||||
if (!result.destination || !workspaceSlug || !groupedIssues || groupBy !== "priority") return;
|
||||
|
||||
@ -212,23 +210,23 @@ export const MyIssuesView: React.FC<Props> = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateUpdateViewModal
|
||||
isOpen={createViewModal !== null}
|
||||
handleClose={() => setCreateViewModal(null)}
|
||||
preLoadedData={createViewModal}
|
||||
user={user}
|
||||
/>
|
||||
<CreateUpdateIssueModal
|
||||
isOpen={createIssueModal && preloadedData?.actionType === "createIssue"}
|
||||
handleClose={() => setCreateIssueModal(false)}
|
||||
prePopulateData={{
|
||||
...preloadedData,
|
||||
}}
|
||||
onSubmit={async () => {
|
||||
mutateMyIssues();
|
||||
}}
|
||||
/>
|
||||
<CreateUpdateIssueModal
|
||||
isOpen={editIssueModal && issueToEdit?.actionType !== "delete"}
|
||||
handleClose={() => setEditIssueModal(false)}
|
||||
data={issueToEdit}
|
||||
onSubmit={async () => {
|
||||
mutateMyIssues();
|
||||
}}
|
||||
/>
|
||||
<DeleteIssueModal
|
||||
handleClose={() => setDeleteIssueModal(false)}
|
||||
@ -275,6 +273,7 @@ export const MyIssuesView: React.FC<Props> = ({
|
||||
groupedIssues,
|
||||
isEmpty,
|
||||
issueView,
|
||||
mutateIssues: mutateMyIssues,
|
||||
orderBy,
|
||||
params,
|
||||
properties,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
@ -15,6 +15,7 @@ import { UserGroupIcon } from "@heroicons/react/24/outline";
|
||||
import { ICurrentUserResponse, IIssue } from "types";
|
||||
// fetch-keys
|
||||
import { PROJECT_MEMBERS } from "constants/fetch-keys";
|
||||
import useProjectMembers from "hooks/use-project-members";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
@ -37,15 +38,12 @@ export const ViewAssigneeSelect: React.FC<Props> = ({
|
||||
isNotAllowed,
|
||||
customButton = false,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
const [fetchAssignees, setFetchAssignees] = useState(false);
|
||||
|
||||
const { data: members } = useSWR(
|
||||
projectId ? PROJECT_MEMBERS(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => projectService.projectMembers(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
);
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { members } = useProjectMembers(workspaceSlug?.toString(), issue.project, fetchAssignees);
|
||||
|
||||
const options = members?.map((member) => ({
|
||||
value: member.member.id,
|
||||
@ -129,6 +127,7 @@ export const ViewAssigneeSelect: React.FC<Props> = ({
|
||||
noChevron
|
||||
position={position}
|
||||
disabled={isNotAllowed}
|
||||
onOpen={() => setFetchAssignees(true)}
|
||||
selfPositioned={selfPositioned}
|
||||
width="w-full min-w-[12rem]"
|
||||
/>
|
||||
|
5
apps/app/components/profile/index.ts
Normal file
5
apps/app/components/profile/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export * from "./overview";
|
||||
export * from "./navbar";
|
||||
export * from "./profile-issues-view-options";
|
||||
export * from "./profile-issues-view";
|
||||
export * from "./sidebar";
|
54
apps/app/components/profile/navbar.tsx
Normal file
54
apps/app/components/profile/navbar.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
import { useRouter } from "next/router";
|
||||
import Link from "next/link";
|
||||
|
||||
// components
|
||||
import { ProfileIssuesViewOptions } from "components/profile";
|
||||
|
||||
const tabsList = [
|
||||
{
|
||||
route: "",
|
||||
label: "Overview",
|
||||
selected: "/[workspaceSlug]/profile/[userId]",
|
||||
},
|
||||
{
|
||||
route: "assigned",
|
||||
label: "Assigned",
|
||||
selected: "/[workspaceSlug]/profile/[userId]/assigned",
|
||||
},
|
||||
{
|
||||
route: "created",
|
||||
label: "Created",
|
||||
selected: "/[workspaceSlug]/profile/[userId]/created",
|
||||
},
|
||||
{
|
||||
route: "subscribed",
|
||||
label: "Subscribed",
|
||||
selected: "/[workspaceSlug]/profile/[userId]/subscribed",
|
||||
},
|
||||
];
|
||||
|
||||
export const ProfileNavbar = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, userId } = router.query;
|
||||
|
||||
return (
|
||||
<div className="px-4 sm:px-5 flex items-center justify-between gap-4 border-b border-custom-border-300">
|
||||
<div className="flex items-center overflow-x-scroll">
|
||||
{tabsList.map((tab) => (
|
||||
<Link key={tab.route} href={`/${workspaceSlug}/profile/${userId}/${tab.route}`}>
|
||||
<a
|
||||
className={`border-b-2 p-4 text-sm font-medium outline-none whitespace-nowrap ${
|
||||
router.pathname === tab.selected
|
||||
? "border-custom-primary-100 text-custom-primary-100"
|
||||
: "border-transparent"
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</a>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<ProfileIssuesViewOptions />
|
||||
</div>
|
||||
);
|
||||
};
|
4
apps/app/components/profile/overview/index.ts
Normal file
4
apps/app/components/profile/overview/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from "./priority-distribution";
|
||||
export * from "./state-distribution";
|
||||
export * from "./stats";
|
||||
export * from "./workload";
|
@ -0,0 +1,75 @@
|
||||
// ui
|
||||
import { BarGraph, Loader } from "components/ui";
|
||||
// helpers
|
||||
import { capitalizeFirstLetter } from "helpers/string.helper";
|
||||
// types
|
||||
import { IUserProfileData } from "types";
|
||||
|
||||
type Props = {
|
||||
userProfile: IUserProfileData | undefined;
|
||||
};
|
||||
|
||||
export const ProfilePriorityDistribution: React.FC<Props> = ({ userProfile }) => (
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-lg font-medium">Issues by Priority</h3>
|
||||
{userProfile ? (
|
||||
<div className="border border-custom-border-100 rounded">
|
||||
<BarGraph
|
||||
data={userProfile.priority_distribution.map((priority) => ({
|
||||
priority: capitalizeFirstLetter(priority.priority ?? "None"),
|
||||
value: priority.priority_count,
|
||||
}))}
|
||||
height="300px"
|
||||
indexBy="priority"
|
||||
keys={["value"]}
|
||||
borderRadius={4}
|
||||
padding={0.7}
|
||||
customYAxisTickValues={userProfile.priority_distribution.map((p) => p.priority_count)}
|
||||
tooltip={(datum) => (
|
||||
<div className="flex items-center gap-2 rounded-md border border-custom-border-200 bg-custom-background-80 p-2 text-xs">
|
||||
<span
|
||||
className="h-3 w-3 rounded"
|
||||
style={{
|
||||
backgroundColor: datum.color,
|
||||
}}
|
||||
/>
|
||||
<span className="font-medium text-custom-text-200">{datum.data.priority}:</span>
|
||||
<span>{datum.value}</span>
|
||||
</div>
|
||||
)}
|
||||
colors={(datum) => {
|
||||
if (datum.data.priority === "Urgent") return "#991b1b";
|
||||
else if (datum.data.priority === "High") return "#ef4444";
|
||||
else if (datum.data.priority === "Medium") return "#f59e0b";
|
||||
else if (datum.data.priority === "Low") return "#16a34a";
|
||||
else return "#e5e5e5";
|
||||
}}
|
||||
theme={{
|
||||
axis: {
|
||||
domain: {
|
||||
line: {
|
||||
stroke: "transparent",
|
||||
},
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
line: {
|
||||
stroke: "transparent",
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid place-items-center p-7">
|
||||
<Loader className="flex items-end gap-12">
|
||||
<Loader.Item width="30px" height="200px" />
|
||||
<Loader.Item width="30px" height="150px" />
|
||||
<Loader.Item width="30px" height="250px" />
|
||||
<Loader.Item width="30px" height="150px" />
|
||||
<Loader.Item width="30px" height="100px" />
|
||||
</Loader>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
81
apps/app/components/profile/overview/state-distribution.tsx
Normal file
81
apps/app/components/profile/overview/state-distribution.tsx
Normal file
@ -0,0 +1,81 @@
|
||||
// ui
|
||||
import { PieGraph } from "components/ui";
|
||||
// types
|
||||
import { IUserProfileData, IUserStateDistribution } from "types";
|
||||
// constants
|
||||
import { STATE_GROUP_COLORS } from "constants/state";
|
||||
|
||||
type Props = {
|
||||
stateDistribution: IUserStateDistribution[];
|
||||
userProfile: IUserProfileData | undefined;
|
||||
};
|
||||
|
||||
export const ProfileStateDistribution: React.FC<Props> = ({ stateDistribution, userProfile }) => {
|
||||
if (!userProfile) return null;
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-lg font-medium">Issues by State</h3>
|
||||
<div className="border border-custom-border-100 rounded p-7">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-6">
|
||||
<div>
|
||||
<PieGraph
|
||||
data={
|
||||
userProfile.state_distribution.map((group) => ({
|
||||
id: group.state_group,
|
||||
label: group.state_group,
|
||||
value: group.state_count,
|
||||
color: STATE_GROUP_COLORS[group.state_group],
|
||||
})) ?? []
|
||||
}
|
||||
height="250px"
|
||||
innerRadius={0.6}
|
||||
cornerRadius={5}
|
||||
padAngle={2}
|
||||
enableArcLabels
|
||||
arcLabelsTextColor="#000000"
|
||||
enableArcLinkLabels={false}
|
||||
activeInnerRadiusOffset={5}
|
||||
colors={(datum) => datum.data.color}
|
||||
tooltip={(datum) => (
|
||||
<div className="flex items-center gap-2 rounded-md border border-custom-border-200 bg-custom-background-90 p-2 text-xs">
|
||||
<span className="text-custom-text-200 capitalize">
|
||||
{datum.datum.label} issues:
|
||||
</span>{" "}
|
||||
{datum.datum.value}
|
||||
</div>
|
||||
)}
|
||||
margin={{
|
||||
top: 32,
|
||||
right: 0,
|
||||
bottom: 32,
|
||||
left: 0,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="space-y-4 w-full">
|
||||
{stateDistribution.map((group) => (
|
||||
<div
|
||||
key={group.state_group}
|
||||
className="flex items-center justify-between gap-2 text-xs"
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div
|
||||
className="h-2.5 w-2.5 rounded-sm"
|
||||
style={{
|
||||
backgroundColor: STATE_GROUP_COLORS[group.state_group],
|
||||
}}
|
||||
/>
|
||||
<div className="capitalize whitespace-nowrap">{group.state_group}</div>
|
||||
</div>
|
||||
<div>{group.state_count}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
66
apps/app/components/profile/overview/stats.tsx
Normal file
66
apps/app/components/profile/overview/stats.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
import { useRouter } from "next/router";
|
||||
import Link from "next/link";
|
||||
|
||||
// ui
|
||||
import { Icon, Loader } from "components/ui";
|
||||
// types
|
||||
import { IUserProfileData } from "types";
|
||||
|
||||
type Props = {
|
||||
userProfile: IUserProfileData | undefined;
|
||||
};
|
||||
|
||||
export const ProfileStats: React.FC<Props> = ({ userProfile }) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, userId } = router.query;
|
||||
|
||||
const overviewCards = [
|
||||
{
|
||||
icon: "new_window",
|
||||
route: "created",
|
||||
title: "Issues created",
|
||||
value: userProfile?.created_issues ?? "...",
|
||||
},
|
||||
{
|
||||
icon: "account_circle",
|
||||
route: "assigned",
|
||||
title: "Issues assigned",
|
||||
value: userProfile?.assigned_issues ?? "...",
|
||||
},
|
||||
{
|
||||
icon: "subscriptions",
|
||||
route: "subscribed",
|
||||
title: "Issues subscribed",
|
||||
value: userProfile?.subscribed_issues ?? "...",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-lg font-medium">Overview</h3>
|
||||
{userProfile ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{overviewCards.map((card) => (
|
||||
<Link key={card.route} href={`/${workspaceSlug}/profile/${userId}/${card.route}`}>
|
||||
<a className="flex items-center gap-3 p-4 rounded border border-custom-border-100 whitespace-nowrap">
|
||||
<div className="h-11 w-11 bg-custom-background-90 rounded grid place-items-center">
|
||||
<Icon iconName={card.icon} className="!text-xl" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-custom-text-400 text-sm">{card.title}</p>
|
||||
<p className="text-xl font-semibold">{card.value}</p>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Loader className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<Loader.Item height="80px" />
|
||||
<Loader.Item height="80px" />
|
||||
<Loader.Item height="80px" />
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
32
apps/app/components/profile/overview/workload.tsx
Normal file
32
apps/app/components/profile/overview/workload.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
// types
|
||||
import { IUserStateDistribution } from "types";
|
||||
// constants
|
||||
import { STATE_GROUP_COLORS } from "constants/state";
|
||||
|
||||
type Props = {
|
||||
stateDistribution: IUserStateDistribution[];
|
||||
};
|
||||
|
||||
export const ProfileWorkload: React.FC<Props> = ({ stateDistribution }) => (
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-lg font-medium">Workload</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 xl:grid-cols-5 gap-4 justify-stretch">
|
||||
{stateDistribution.map((group) => (
|
||||
<div key={group.state_group}>
|
||||
<a className="flex gap-2 p-4 rounded border border-custom-border-100 whitespace-nowrap">
|
||||
<div
|
||||
className="h-3 w-3 rounded-sm"
|
||||
style={{
|
||||
backgroundColor: STATE_GROUP_COLORS[group.state_group],
|
||||
}}
|
||||
/>
|
||||
<div className="space-y-1 -mt-1">
|
||||
<p className="text-custom-text-400 text-sm capitalize">{group.state_group}</p>
|
||||
<p className="text-xl font-semibold">{group.state_count}</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
298
apps/app/components/profile/profile-issues-view-options.tsx
Normal file
298
apps/app/components/profile/profile-issues-view-options.tsx
Normal file
@ -0,0 +1,298 @@
|
||||
import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// headless ui
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
// hooks
|
||||
import useProfileIssues from "hooks/use-profile-issues";
|
||||
import useEstimateOption from "hooks/use-estimate-option";
|
||||
// components
|
||||
import { MyIssuesSelectFilters } from "components/issues";
|
||||
// ui
|
||||
import { CustomMenu, ToggleSwitch, Tooltip } from "components/ui";
|
||||
// icons
|
||||
import { ChevronDownIcon } from "@heroicons/react/24/outline";
|
||||
import { FormatListBulletedOutlined, GridViewOutlined } from "@mui/icons-material";
|
||||
// helpers
|
||||
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||
import { checkIfArraysHaveSameElements } from "helpers/array.helper";
|
||||
// types
|
||||
import { Properties, TIssueViewOptions } from "types";
|
||||
// constants
|
||||
import { GROUP_BY_OPTIONS, ORDER_BY_OPTIONS, FILTER_ISSUE_OPTIONS } from "constants/issue";
|
||||
|
||||
const issueViewOptions: { type: TIssueViewOptions; Icon: any }[] = [
|
||||
{
|
||||
type: "list",
|
||||
Icon: FormatListBulletedOutlined,
|
||||
},
|
||||
{
|
||||
type: "kanban",
|
||||
Icon: GridViewOutlined,
|
||||
},
|
||||
];
|
||||
|
||||
export const ProfileIssuesViewOptions: React.FC = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, userId } = router.query;
|
||||
|
||||
const {
|
||||
issueView,
|
||||
setIssueView,
|
||||
groupByProperty,
|
||||
setGroupByProperty,
|
||||
orderBy,
|
||||
setOrderBy,
|
||||
showEmptyGroups,
|
||||
setShowEmptyGroups,
|
||||
filters,
|
||||
properties,
|
||||
setProperties,
|
||||
setFilters,
|
||||
} = useProfileIssues(workspaceSlug?.toString(), userId?.toString());
|
||||
|
||||
const { isEstimateActive } = useEstimateOption();
|
||||
|
||||
if (
|
||||
!router.pathname.includes("assigned") &&
|
||||
!router.pathname.includes("created") &&
|
||||
!router.pathname.includes("subscribed")
|
||||
)
|
||||
return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-x-1">
|
||||
{issueViewOptions.map((option) => (
|
||||
<Tooltip
|
||||
key={option.type}
|
||||
tooltipContent={
|
||||
<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} View</span>
|
||||
}
|
||||
position="bottom"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80 duration-300 ${
|
||||
issueView === option.type
|
||||
? "bg-custom-sidebar-background-80"
|
||||
: "text-custom-sidebar-text-200"
|
||||
}`}
|
||||
onClick={() => setIssueView(option.type)}
|
||||
>
|
||||
<option.Icon
|
||||
sx={{
|
||||
fontSize: 16,
|
||||
}}
|
||||
className={option.type === "gantt_chart" ? "rotate-90" : ""}
|
||||
/>
|
||||
</button>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
<MyIssuesSelectFilters
|
||||
filters={filters}
|
||||
onSelect={(option) => {
|
||||
const key = option.key as keyof typeof filters;
|
||||
|
||||
if (key === "target_date") {
|
||||
const valueExists = checkIfArraysHaveSameElements(
|
||||
filters?.target_date ?? [],
|
||||
option.value
|
||||
);
|
||||
|
||||
setFilters({
|
||||
target_date: valueExists ? null : option.value,
|
||||
});
|
||||
} else {
|
||||
const valueExists = filters[key]?.includes(option.value);
|
||||
|
||||
if (valueExists)
|
||||
setFilters({
|
||||
[option.key]: ((filters[key] ?? []) as any[])?.filter(
|
||||
(val) => val !== option.value
|
||||
),
|
||||
});
|
||||
else
|
||||
setFilters({
|
||||
[option.key]: [...((filters[key] ?? []) as any[]), option.value],
|
||||
});
|
||||
}
|
||||
}}
|
||||
direction="left"
|
||||
height="rg"
|
||||
/>
|
||||
<Popover className="relative">
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Popover.Button
|
||||
className={`group flex items-center gap-2 rounded-md border border-custom-sidebar-border-200 bg-transparent px-3 py-1.5 text-xs hover:bg-custom-sidebar-background-90 hover:text-custom-sidebar-text-100 focus:outline-none duration-300 ${
|
||||
open
|
||||
? "bg-custom-sidebar-background-90 text-custom-sidebar-text-100"
|
||||
: "text-custom-sidebar-text-200"
|
||||
}`}
|
||||
>
|
||||
View
|
||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||
</Popover.Button>
|
||||
|
||||
<Transition
|
||||
as={React.Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Popover.Panel className="absolute right-0 z-30 mt-1 w-screen max-w-xs transform rounded-lg border border-custom-border-200 bg-custom-background-90 p-3 shadow-lg">
|
||||
<div className="relative divide-y-2 divide-custom-border-200">
|
||||
<div className="space-y-4 pb-3 text-xs">
|
||||
{issueView !== "calendar" && issueView !== "spreadsheet" && (
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Group by</h4>
|
||||
<CustomMenu
|
||||
label={
|
||||
groupByProperty === "project"
|
||||
? "Project"
|
||||
: GROUP_BY_OPTIONS.find((option) => option.key === groupByProperty)
|
||||
?.name ?? "Select"
|
||||
}
|
||||
>
|
||||
{GROUP_BY_OPTIONS.map((option) => {
|
||||
if (issueView === "kanban" && option.key === null) return null;
|
||||
if (option.key === "state" || option.key === "created_by")
|
||||
return null;
|
||||
|
||||
return (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => setGroupByProperty(option.key)}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
);
|
||||
})}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Order by</h4>
|
||||
<CustomMenu
|
||||
label={
|
||||
ORDER_BY_OPTIONS.find((option) => option.key === orderBy)?.name ??
|
||||
"Select"
|
||||
}
|
||||
>
|
||||
{ORDER_BY_OPTIONS.map((option) => {
|
||||
if (groupByProperty === "priority" && option.key === "priority")
|
||||
return null;
|
||||
if (option.key === "sort_order") return null;
|
||||
|
||||
return (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => {
|
||||
setOrderBy(option.key);
|
||||
}}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
);
|
||||
})}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Issue type</h4>
|
||||
<CustomMenu
|
||||
label={
|
||||
FILTER_ISSUE_OPTIONS.find((option) => option.key === filters?.type)
|
||||
?.name ?? "Select"
|
||||
}
|
||||
>
|
||||
{FILTER_ISSUE_OPTIONS.map((option) => (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() =>
|
||||
setFilters({
|
||||
type: option.key,
|
||||
})
|
||||
}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
|
||||
{issueView !== "calendar" && issueView !== "spreadsheet" && (
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Show empty states</h4>
|
||||
<ToggleSwitch value={showEmptyGroups} onChange={setShowEmptyGroups} />
|
||||
</div>
|
||||
{/* <div className="relative flex justify-end gap-x-3">
|
||||
<button type="button" onClick={() => resetFilterToDefault()}>
|
||||
Reset to default
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="font-medium text-custom-primary"
|
||||
onClick={() => setNewFilterDefaultView()}
|
||||
>
|
||||
Set as default
|
||||
</button>
|
||||
</div> */}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 py-3">
|
||||
<h4 className="text-sm text-custom-text-200">Display Properties</h4>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{Object.keys(properties).map((key) => {
|
||||
if (key === "estimate" && !isEstimateActive) return null;
|
||||
|
||||
if (
|
||||
issueView === "spreadsheet" &&
|
||||
(key === "attachment_count" ||
|
||||
key === "link" ||
|
||||
key === "sub_issue_count")
|
||||
)
|
||||
return null;
|
||||
|
||||
if (
|
||||
issueView !== "spreadsheet" &&
|
||||
(key === "created_on" || key === "updated_on")
|
||||
)
|
||||
return null;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={key}
|
||||
type="button"
|
||||
className={`rounded border px-2 py-1 text-xs capitalize ${
|
||||
properties[key as keyof Properties]
|
||||
? "border-custom-primary bg-custom-primary text-white"
|
||||
: "border-custom-border-200"
|
||||
}`}
|
||||
onClick={() => setProperties(key as keyof Properties)}
|
||||
>
|
||||
{key === "key" ? "ID" : replaceUnderscoreIfSnakeCase(key)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
};
|
273
apps/app/components/profile/profile-issues-view.tsx
Normal file
273
apps/app/components/profile/profile-issues-view.tsx
Normal file
@ -0,0 +1,273 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// react-beautiful-dnd
|
||||
import { DropResult } from "react-beautiful-dnd";
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
// hooks
|
||||
import useProfileIssues from "hooks/use-profile-issues";
|
||||
import useUser from "hooks/use-user";
|
||||
// components
|
||||
import { AllViews, FiltersList } from "components/core";
|
||||
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||
// helpers
|
||||
import { orderArrayBy } from "helpers/array.helper";
|
||||
// types
|
||||
import { IIssue, IIssueFilterOptions } from "types";
|
||||
// fetch-keys
|
||||
import { WORKSPACE_LABELS } from "constants/fetch-keys";
|
||||
|
||||
export const ProfileIssuesView = () => {
|
||||
// create issue modal
|
||||
const [createIssueModal, setCreateIssueModal] = useState(false);
|
||||
const [preloadedData, setPreloadedData] = useState<
|
||||
(Partial<IIssue> & { actionType: "createIssue" | "edit" | "delete" }) | undefined
|
||||
>(undefined);
|
||||
|
||||
// update issue modal
|
||||
const [editIssueModal, setEditIssueModal] = useState(false);
|
||||
const [issueToEdit, setIssueToEdit] = useState<
|
||||
(IIssue & { actionType: "edit" | "delete" }) | undefined
|
||||
>(undefined);
|
||||
|
||||
// delete issue modal
|
||||
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
||||
const [issueToDelete, setIssueToDelete] = useState<IIssue | null>(null);
|
||||
|
||||
// trash box
|
||||
const [trashBox, setTrashBox] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, userId } = router.query;
|
||||
|
||||
const { user } = useUser();
|
||||
|
||||
const {
|
||||
groupedIssues,
|
||||
mutateProfileIssues,
|
||||
issueView,
|
||||
groupByProperty,
|
||||
orderBy,
|
||||
isEmpty,
|
||||
showEmptyGroups,
|
||||
filters,
|
||||
setFilters,
|
||||
properties,
|
||||
params,
|
||||
} = useProfileIssues(workspaceSlug?.toString(), userId?.toString());
|
||||
|
||||
const { data: labels } = useSWR(
|
||||
workspaceSlug && (filters?.labels ?? []).length > 0
|
||||
? WORKSPACE_LABELS(workspaceSlug.toString())
|
||||
: null,
|
||||
workspaceSlug && (filters?.labels ?? []).length > 0
|
||||
? () => issuesService.getWorkspaceLabels(workspaceSlug.toString())
|
||||
: null
|
||||
);
|
||||
|
||||
const handleDeleteIssue = useCallback(
|
||||
(issue: IIssue) => {
|
||||
setDeleteIssueModal(true);
|
||||
setIssueToDelete(issue);
|
||||
},
|
||||
[setDeleteIssueModal, setIssueToDelete]
|
||||
);
|
||||
|
||||
const handleOnDragEnd = useCallback(
|
||||
async (result: DropResult) => {
|
||||
setTrashBox(false);
|
||||
|
||||
if (!result.destination || !workspaceSlug || !groupedIssues || groupByProperty !== "priority")
|
||||
return;
|
||||
|
||||
const { source, destination } = result;
|
||||
|
||||
if (source.droppableId === destination.droppableId) return;
|
||||
|
||||
const draggedItem = groupedIssues[source.droppableId][source.index];
|
||||
|
||||
if (!draggedItem) return;
|
||||
|
||||
if (destination.droppableId === "trashBox") handleDeleteIssue(draggedItem);
|
||||
else {
|
||||
const sourceGroup = source.droppableId;
|
||||
const destinationGroup = destination.droppableId;
|
||||
|
||||
draggedItem[groupByProperty] = destinationGroup;
|
||||
|
||||
mutateProfileIssues((prevData) => {
|
||||
if (!prevData) return prevData;
|
||||
|
||||
const sourceGroupArray = [...groupedIssues[sourceGroup]];
|
||||
const destinationGroupArray = [...groupedIssues[destinationGroup]];
|
||||
|
||||
sourceGroupArray.splice(source.index, 1);
|
||||
destinationGroupArray.splice(destination.index, 0, draggedItem);
|
||||
|
||||
return {
|
||||
...prevData,
|
||||
[sourceGroup]: orderArrayBy(sourceGroupArray, orderBy),
|
||||
[destinationGroup]: orderArrayBy(destinationGroupArray, orderBy),
|
||||
};
|
||||
}, false);
|
||||
|
||||
// patch request
|
||||
issuesService
|
||||
.patchIssue(
|
||||
workspaceSlug as string,
|
||||
draggedItem.project,
|
||||
draggedItem.id,
|
||||
{
|
||||
priority: draggedItem.priority,
|
||||
},
|
||||
user
|
||||
)
|
||||
.catch(() => mutateProfileIssues());
|
||||
}
|
||||
},
|
||||
[
|
||||
groupByProperty,
|
||||
groupedIssues,
|
||||
handleDeleteIssue,
|
||||
mutateProfileIssues,
|
||||
orderBy,
|
||||
user,
|
||||
workspaceSlug,
|
||||
]
|
||||
);
|
||||
|
||||
const addIssueToGroup = useCallback((groupTitle: string) => {
|
||||
setCreateIssueModal(true);
|
||||
return;
|
||||
}, []);
|
||||
|
||||
const addIssueToDate = useCallback(
|
||||
(date: string) => {
|
||||
setCreateIssueModal(true);
|
||||
setPreloadedData({
|
||||
target_date: date,
|
||||
actionType: "createIssue",
|
||||
});
|
||||
},
|
||||
[setCreateIssueModal, setPreloadedData]
|
||||
);
|
||||
|
||||
const makeIssueCopy = useCallback(
|
||||
(issue: IIssue) => {
|
||||
setCreateIssueModal(true);
|
||||
|
||||
setPreloadedData({ ...issue, name: `${issue.name} (Copy)`, actionType: "createIssue" });
|
||||
},
|
||||
[setCreateIssueModal, setPreloadedData]
|
||||
);
|
||||
|
||||
const handleEditIssue = useCallback(
|
||||
(issue: IIssue) => {
|
||||
setEditIssueModal(true);
|
||||
setIssueToEdit({
|
||||
...issue,
|
||||
actionType: "edit",
|
||||
cycle: issue.issue_cycle ? issue.issue_cycle.cycle : null,
|
||||
module: issue.issue_module ? issue.issue_module.module : null,
|
||||
});
|
||||
},
|
||||
[setEditIssueModal, setIssueToEdit]
|
||||
);
|
||||
|
||||
const handleIssueAction = useCallback(
|
||||
(issue: IIssue, action: "copy" | "edit" | "delete") => {
|
||||
if (action === "copy") makeIssueCopy(issue);
|
||||
else if (action === "edit") handleEditIssue(issue);
|
||||
else if (action === "delete") handleDeleteIssue(issue);
|
||||
},
|
||||
[makeIssueCopy, handleEditIssue, handleDeleteIssue]
|
||||
);
|
||||
|
||||
const filtersToDisplay = { ...filters, assignees: null, created_by: null, subscriber: null };
|
||||
|
||||
const nullFilters = Object.keys(filtersToDisplay).filter(
|
||||
(key) => filtersToDisplay[key as keyof IIssueFilterOptions] === null
|
||||
);
|
||||
const areFiltersApplied =
|
||||
Object.keys(filtersToDisplay).length > 0 &&
|
||||
nullFilters.length !== Object.keys(filtersToDisplay).length;
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateUpdateIssueModal
|
||||
isOpen={createIssueModal && preloadedData?.actionType === "createIssue"}
|
||||
handleClose={() => setCreateIssueModal(false)}
|
||||
prePopulateData={{
|
||||
...preloadedData,
|
||||
}}
|
||||
onSubmit={async () => {
|
||||
mutateProfileIssues();
|
||||
}}
|
||||
/>
|
||||
<CreateUpdateIssueModal
|
||||
isOpen={editIssueModal && issueToEdit?.actionType !== "delete"}
|
||||
handleClose={() => setEditIssueModal(false)}
|
||||
data={issueToEdit}
|
||||
onSubmit={async () => {
|
||||
mutateProfileIssues();
|
||||
}}
|
||||
/>
|
||||
<DeleteIssueModal
|
||||
handleClose={() => setDeleteIssueModal(false)}
|
||||
isOpen={deleteIssueModal}
|
||||
data={issueToDelete}
|
||||
user={user}
|
||||
/>
|
||||
{areFiltersApplied && (
|
||||
<>
|
||||
<div className="flex items-center justify-between gap-2 px-5 pt-3 pb-0">
|
||||
<FiltersList
|
||||
filters={filtersToDisplay}
|
||||
setFilters={setFilters}
|
||||
labels={labels}
|
||||
members={undefined}
|
||||
states={undefined}
|
||||
clearAllFilters={() =>
|
||||
setFilters({
|
||||
labels: null,
|
||||
priority: null,
|
||||
state_group: null,
|
||||
target_date: null,
|
||||
type: null,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{<div className="mt-3 border-t border-custom-border-200" />}
|
||||
</>
|
||||
)}
|
||||
<AllViews
|
||||
addIssueToDate={addIssueToDate}
|
||||
addIssueToGroup={addIssueToGroup}
|
||||
disableUserActions={false}
|
||||
dragDisabled={groupByProperty !== "priority"}
|
||||
handleOnDragEnd={handleOnDragEnd}
|
||||
handleIssueAction={handleIssueAction}
|
||||
openIssuesListModal={null}
|
||||
removeIssue={null}
|
||||
trashBox={trashBox}
|
||||
setTrashBox={setTrashBox}
|
||||
viewProps={{
|
||||
groupByProperty,
|
||||
groupedIssues,
|
||||
isEmpty,
|
||||
issueView,
|
||||
mutateIssues: mutateProfileIssues,
|
||||
orderBy,
|
||||
params,
|
||||
properties,
|
||||
showEmptyGroups,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
256
apps/app/components/profile/sidebar.tsx
Normal file
256
apps/app/components/profile/sidebar.tsx
Normal file
@ -0,0 +1,256 @@
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// next-themes
|
||||
import { useTheme } from "next-themes";
|
||||
// headless ui
|
||||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
// services
|
||||
import userService from "services/user.service";
|
||||
// ui
|
||||
import { Icon, Loader } from "components/ui";
|
||||
// helpers
|
||||
import { renderLongDetailDateFormat } from "helpers/date-time.helper";
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
// fetch-keys
|
||||
import { USER_PROFILE_PROJECT_SEGREGATION } from "constants/fetch-keys";
|
||||
|
||||
export const ProfileSidebar = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, userId } = router.query;
|
||||
|
||||
const { theme } = useTheme();
|
||||
|
||||
const { data: userProjectsData } = useSWR(
|
||||
workspaceSlug && userId
|
||||
? USER_PROFILE_PROJECT_SEGREGATION(workspaceSlug.toString(), userId.toString())
|
||||
: null,
|
||||
workspaceSlug && userId
|
||||
? () =>
|
||||
userService.getUserProfileProjectsSegregation(workspaceSlug.toString(), userId.toString())
|
||||
: null
|
||||
);
|
||||
|
||||
const userDetails = [
|
||||
{
|
||||
label: "Username",
|
||||
value: "",
|
||||
},
|
||||
{
|
||||
label: "Joined on",
|
||||
value: renderLongDetailDateFormat(userProjectsData?.user_data.date_joined ?? ""),
|
||||
},
|
||||
{
|
||||
label: "Timezone",
|
||||
value: userProjectsData?.user_data.user_timezone,
|
||||
},
|
||||
{
|
||||
label: "Status",
|
||||
value: "Online",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex-shrink-0 h-full w-80 overflow-y-auto"
|
||||
style={{
|
||||
boxShadow:
|
||||
theme === "light"
|
||||
? "0px 1px 4px 0px rgba(0, 0, 0, 0.01), 0px 4px 8px 0px rgba(0, 0, 0, 0.02), 0px 1px 12px 0px rgba(0, 0, 0, 0.12)"
|
||||
: "0px 0px 4px 0px rgba(0, 0, 0, 0.20), 0px 2px 6px 0px rgba(0, 0, 0, 0.50)",
|
||||
}}
|
||||
>
|
||||
{userProjectsData ? (
|
||||
<>
|
||||
<div className="relative h-32">
|
||||
<img
|
||||
src={
|
||||
userProjectsData.user_data.cover_image ??
|
||||
"https://images.unsplash.com/photo-1672243775941-10d763d9adef?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80"
|
||||
}
|
||||
alt={userProjectsData.user_data.first_name}
|
||||
className="h-32 w-full object-cover"
|
||||
/>
|
||||
<div className="absolute -bottom-[26px] left-5 h-[52px] w-[52px] rounded">
|
||||
{userProjectsData.user_data.avatar && userProjectsData.user_data.avatar !== "" ? (
|
||||
<img
|
||||
src={userProjectsData.user_data.avatar}
|
||||
alt={userProjectsData.user_data.first_name}
|
||||
className="rounded"
|
||||
/>
|
||||
) : (
|
||||
<div className="bg-custom-background-90 text-custom-text-100">
|
||||
{userProjectsData.user_data.first_name[0]}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-5">
|
||||
<div className="mt-[38px]">
|
||||
<h4 className="text-lg font-semibold">
|
||||
{userProjectsData.user_data.first_name} {userProjectsData.user_data.last_name}
|
||||
</h4>
|
||||
<h6 className="text-custom-text-200 text-sm">{userProjectsData.user_data.email}</h6>
|
||||
</div>
|
||||
<div className="mt-6 space-y-5">
|
||||
{userDetails.map((detail) => (
|
||||
<div key={detail.label} className="flex items-center gap-4 text-sm">
|
||||
<div className="text-custom-text-200 w-2/5">{detail.label}</div>
|
||||
<div className="font-medium">{detail.value}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-9 divide-y divide-custom-border-100">
|
||||
{userProjectsData.project_data.map((project, index) => {
|
||||
const totalIssues =
|
||||
project.created_issues +
|
||||
project.assigned_issues +
|
||||
project.pending_issues +
|
||||
project.completed_issues;
|
||||
const totalAssignedIssues = totalIssues - project.created_issues;
|
||||
|
||||
const completedIssuePercentage =
|
||||
totalAssignedIssues === 0
|
||||
? 0
|
||||
: Math.round((project.completed_issues / totalAssignedIssues) * 100);
|
||||
|
||||
return (
|
||||
<Disclosure
|
||||
key={project.id}
|
||||
as="div"
|
||||
className={`${index === 0 ? "pb-3" : "py-3"}`}
|
||||
>
|
||||
{({ open }) => (
|
||||
<div className="w-full">
|
||||
<Disclosure.Button className="flex items-center justify-between gap-2 w-full">
|
||||
<div className="flex items-center gap-2 w-3/4">
|
||||
{project.emoji ? (
|
||||
<div className="flex-shrink-0 grid h-7 w-7 place-items-center">
|
||||
{renderEmoji(project.emoji)}
|
||||
</div>
|
||||
) : project.icon_prop ? (
|
||||
<div className="flex-shrink-0 h-7 w-7 grid place-items-center">
|
||||
{renderEmoji(project.icon_prop)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-shrink-0 grid place-items-center h-7 w-7 rounded bg-custom-background-90 uppercase text-custom-text-100 text-xs">
|
||||
{project?.name.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-sm font-medium truncate break-words">
|
||||
{project.name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-shrink-0 flex items-center gap-2">
|
||||
<div
|
||||
className={`px-1 py-0.5 text-xs font-medium rounded ${
|
||||
completedIssuePercentage <= 35
|
||||
? "bg-red-500/10 text-red-500"
|
||||
: completedIssuePercentage <= 70
|
||||
? "bg-yellow-500/10 text-yellow-500"
|
||||
: "bg-green-500/10 text-green-500"
|
||||
}`}
|
||||
>
|
||||
{completedIssuePercentage}%
|
||||
</div>
|
||||
<Icon iconName="arrow_drop_down" className="!text-lg" />
|
||||
</div>
|
||||
</Disclosure.Button>
|
||||
<Transition
|
||||
show={open}
|
||||
enter="transition duration-100 ease-out"
|
||||
enterFrom="transform opacity-0"
|
||||
enterTo="transform opacity-100"
|
||||
leave="transition duration-75 ease-out"
|
||||
leaveFrom="transform opacity-100"
|
||||
leaveTo="transform opacity-0"
|
||||
>
|
||||
<Disclosure.Panel className="pl-9 mt-5">
|
||||
{totalIssues > 0 && (
|
||||
<div className="flex items-center gap-0.5">
|
||||
<div
|
||||
className="h-1 rounded"
|
||||
style={{
|
||||
backgroundColor: "#203b80",
|
||||
width: `${(project.created_issues / totalIssues) * 100}%`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="h-1 rounded"
|
||||
style={{
|
||||
backgroundColor: "#3f76ff",
|
||||
width: `${(project.assigned_issues / totalIssues) * 100}%`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="h-1 rounded"
|
||||
style={{
|
||||
backgroundColor: "#f59e0b",
|
||||
width: `${(project.pending_issues / totalIssues) * 100}%`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="h-1 rounded"
|
||||
style={{
|
||||
backgroundColor: "#16a34a",
|
||||
width: `${(project.completed_issues / totalIssues) * 100}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-7 space-y-5 text-sm text-custom-text-200">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-2.5 w-2.5 bg-[#203b80] rounded-sm" />
|
||||
Created
|
||||
</div>
|
||||
<div className="font-medium">{project.created_issues} Issues</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-2.5 w-2.5 bg-[#3f76ff] rounded-sm" />
|
||||
Assigned
|
||||
</div>
|
||||
<div className="font-medium">{project.assigned_issues} Issues</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-2.5 w-2.5 bg-[#f59e0b] rounded-sm" />
|
||||
Due
|
||||
</div>
|
||||
<div className="font-medium">{project.pending_issues} Issues</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-2.5 w-2.5 bg-[#16a34a] rounded-sm" />
|
||||
Completed
|
||||
</div>
|
||||
<div className="font-medium">{project.completed_issues} Issues</div>
|
||||
</div>
|
||||
</div>
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
</div>
|
||||
)}
|
||||
</Disclosure>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<Loader className="px-5 space-y-7">
|
||||
<Loader.Item height="130px" />
|
||||
<div className="space-y-5">
|
||||
<Loader.Item height="20px" />
|
||||
<Loader.Item height="20px" />
|
||||
<Loader.Item height="20px" />
|
||||
<Loader.Item height="20px" />
|
||||
<Loader.Item height="20px" />
|
||||
</div>
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -42,6 +42,7 @@ const restrictedUrls = [
|
||||
"invitations",
|
||||
"magic-sign-in",
|
||||
"onboarding",
|
||||
"profile",
|
||||
"reset-password",
|
||||
"sign-up",
|
||||
"workspace-member-invitation",
|
||||
|
@ -151,11 +151,9 @@ export const STATES_LIST = (projectId: string) => `STATES_LIST_${projectId.toUpp
|
||||
|
||||
export const USER_ISSUE = (workspaceSlug: string) => `USER_ISSUE_${workspaceSlug.toUpperCase()}`;
|
||||
export const USER_ISSUES = (workspaceSlug: string, params: any) => {
|
||||
if (!params) return `USER_ISSUES_${workspaceSlug.toUpperCase()}`;
|
||||
|
||||
const paramsKey = myIssuesParamsToKey(params);
|
||||
|
||||
return `USER_ISSUES_${paramsKey}`;
|
||||
return `USER_ISSUES_${workspaceSlug.toUpperCase()}_${paramsKey}`;
|
||||
};
|
||||
export const USER_ACTIVITY = "USER_ACTIVITY";
|
||||
export const USER_WORKSPACE_DASHBOARD = (workspaceSlug: string) =>
|
||||
@ -302,9 +300,24 @@ export const getPaginatedNotificationKey = (
|
||||
})}`;
|
||||
};
|
||||
|
||||
// profile
|
||||
export const USER_PROFILE_DATA = (workspaceSlug: string, userId: string) =>
|
||||
`USER_PROFILE_ACTIVITY_${workspaceSlug.toUpperCase()}_${userId.toUpperCase()}`;
|
||||
export const USER_PROFILE_ACTIVITY = (workspaceSlug: string, userId: string) =>
|
||||
`USER_WORKSPACE_PROFILE_ACTIVITY_${workspaceSlug.toUpperCase()}_${userId.toUpperCase()}`;
|
||||
export const USER_PROFILE_PROJECT_SEGREGATION = (workspaceSlug: string, userId: string) =>
|
||||
`USER_PROFILE_PROJECT_SEGREGATION_${workspaceSlug.toUpperCase()}_${userId.toUpperCase()}`;
|
||||
export const USER_PROFILE_ISSUES = (workspaceSlug: string, userId: string, params: any) => {
|
||||
const paramsKey = myIssuesParamsToKey(params);
|
||||
|
||||
const subscriberKey = params.subscriber ? params.subscriber.toUpperCase() : "NULL";
|
||||
|
||||
return `USER_PROFILE_ISSUES_${workspaceSlug.toUpperCase()}_${userId.toUpperCase()}_${paramsKey}_${subscriberKey}`;
|
||||
};
|
||||
|
||||
// reactions
|
||||
export const ISSUE_REACTION_LIST = (workspaceSlug: string, projectId: string, issueId: string) =>
|
||||
`ISSUE_REACTION_LIST_${workspaceSlug.toUpperCase()}_${projectId.toUpperCase()}_${issueId.toUpperCase()}`;
|
||||
|
||||
export const COMMENT_REACTION_LIST = (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
|
@ -1,9 +1,9 @@
|
||||
export const STATE_GROUP_COLORS: {
|
||||
[key: string]: string;
|
||||
} = {
|
||||
backlog: "#ced4da",
|
||||
unstarted: "#26b5ce",
|
||||
started: "#f7ae59",
|
||||
cancelled: "#d687ff",
|
||||
completed: "#09a953",
|
||||
backlog: "#d9d9d9",
|
||||
unstarted: "#3f76ff",
|
||||
started: "#f59e0b",
|
||||
completed: "#16a34a",
|
||||
cancelled: "#dc2626",
|
||||
};
|
||||
|
310
apps/app/contexts/profile-issues-context.tsx
Normal file
310
apps/app/contexts/profile-issues-context.tsx
Normal file
@ -0,0 +1,310 @@
|
||||
import { createContext, useCallback, useReducer } from "react";
|
||||
|
||||
// components
|
||||
import ToastAlert from "components/toast-alert";
|
||||
// types
|
||||
import {
|
||||
IIssueFilterOptions,
|
||||
TIssueViewOptions,
|
||||
TIssueGroupByOptions,
|
||||
TIssueOrderByOptions,
|
||||
Properties,
|
||||
} from "types";
|
||||
|
||||
export const profileIssuesContext = createContext<ContextType>({} as ContextType);
|
||||
|
||||
type IssueViewProps = {
|
||||
issueView: TIssueViewOptions;
|
||||
groupByProperty: TIssueGroupByOptions;
|
||||
orderBy: TIssueOrderByOptions;
|
||||
showEmptyGroups: boolean;
|
||||
showSubIssues: boolean;
|
||||
filters: IIssueFilterOptions & { subscriber: string | null };
|
||||
properties: Properties;
|
||||
};
|
||||
|
||||
type ReducerActionType = {
|
||||
type:
|
||||
| "SET_ISSUE_VIEW"
|
||||
| "SET_ORDER_BY_PROPERTY"
|
||||
| "SET_SHOW_EMPTY_STATES"
|
||||
| "SET_FILTERS"
|
||||
| "SET_PROPERTIES"
|
||||
| "SET_GROUP_BY_PROPERTY"
|
||||
| "RESET_TO_DEFAULT"
|
||||
| "SET_SHOW_SUB_ISSUES";
|
||||
payload?: Partial<IssueViewProps>;
|
||||
};
|
||||
|
||||
type ContextType = IssueViewProps & {
|
||||
setGroupByProperty: (property: TIssueGroupByOptions) => void;
|
||||
setOrderBy: (property: TIssueOrderByOptions) => void;
|
||||
setShowEmptyGroups: (property: boolean) => void;
|
||||
setShowSubIssues: (value: boolean) => void;
|
||||
setFilters: (filters: Partial<IIssueFilterOptions & { subscriber: string | null }>) => void;
|
||||
setProperties: (key: keyof Properties) => void;
|
||||
setIssueView: (property: TIssueViewOptions) => void;
|
||||
};
|
||||
|
||||
type StateType = {
|
||||
issueView: TIssueViewOptions;
|
||||
groupByProperty: TIssueGroupByOptions;
|
||||
orderBy: TIssueOrderByOptions;
|
||||
showEmptyGroups: boolean;
|
||||
showSubIssues: boolean;
|
||||
filters: IIssueFilterOptions & { subscriber: string | null };
|
||||
properties: Properties;
|
||||
};
|
||||
type ReducerFunctionType = (state: StateType, action: ReducerActionType) => StateType;
|
||||
|
||||
export const initialState: StateType = {
|
||||
issueView: "list",
|
||||
groupByProperty: null,
|
||||
orderBy: "-created_at",
|
||||
showEmptyGroups: true,
|
||||
showSubIssues: true,
|
||||
filters: {
|
||||
type: null,
|
||||
priority: null,
|
||||
assignees: null,
|
||||
labels: null,
|
||||
state: null,
|
||||
state_group: null,
|
||||
subscriber: null,
|
||||
created_by: null,
|
||||
target_date: null,
|
||||
},
|
||||
properties: {
|
||||
assignee: true,
|
||||
attachment_count: true,
|
||||
created_on: true,
|
||||
due_date: true,
|
||||
estimate: true,
|
||||
key: true,
|
||||
labels: true,
|
||||
link: true,
|
||||
priority: true,
|
||||
state: true,
|
||||
sub_issue_count: true,
|
||||
updated_on: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const reducer: ReducerFunctionType = (state, action) => {
|
||||
const { type, payload } = action;
|
||||
|
||||
switch (type) {
|
||||
case "SET_ISSUE_VIEW": {
|
||||
const newState = {
|
||||
...state,
|
||||
issueView: payload?.issueView || "list",
|
||||
};
|
||||
|
||||
return {
|
||||
...state,
|
||||
...newState,
|
||||
};
|
||||
}
|
||||
|
||||
case "SET_GROUP_BY_PROPERTY": {
|
||||
const newState = {
|
||||
...state,
|
||||
groupByProperty: payload?.groupByProperty || null,
|
||||
};
|
||||
|
||||
return {
|
||||
...state,
|
||||
...newState,
|
||||
};
|
||||
}
|
||||
|
||||
case "SET_ORDER_BY_PROPERTY": {
|
||||
const newState = {
|
||||
...state,
|
||||
orderBy: payload?.orderBy || "-created_at",
|
||||
};
|
||||
|
||||
return {
|
||||
...state,
|
||||
...newState,
|
||||
};
|
||||
}
|
||||
|
||||
case "SET_SHOW_EMPTY_STATES": {
|
||||
const newState = {
|
||||
...state,
|
||||
showEmptyGroups: payload?.showEmptyGroups || true,
|
||||
};
|
||||
|
||||
return {
|
||||
...state,
|
||||
...newState,
|
||||
};
|
||||
}
|
||||
|
||||
case "SET_SHOW_SUB_ISSUES": {
|
||||
const newState = {
|
||||
...state,
|
||||
showSubIssues: payload?.showSubIssues || true,
|
||||
};
|
||||
|
||||
return {
|
||||
...state,
|
||||
...newState,
|
||||
};
|
||||
}
|
||||
|
||||
case "SET_FILTERS": {
|
||||
const newState = {
|
||||
...state,
|
||||
filters: {
|
||||
...state.filters,
|
||||
...payload?.filters,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
...state,
|
||||
...newState,
|
||||
};
|
||||
}
|
||||
|
||||
case "SET_PROPERTIES": {
|
||||
const newState = {
|
||||
...state,
|
||||
properties: {
|
||||
...state.properties,
|
||||
...payload?.properties,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
...state,
|
||||
...newState,
|
||||
};
|
||||
}
|
||||
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const ProfileIssuesContextProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
children,
|
||||
}) => {
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
|
||||
const setIssueView = useCallback((property: TIssueViewOptions) => {
|
||||
dispatch({
|
||||
type: "SET_ISSUE_VIEW",
|
||||
payload: {
|
||||
issueView: property,
|
||||
},
|
||||
});
|
||||
|
||||
if (property === "kanban") {
|
||||
dispatch({
|
||||
type: "SET_GROUP_BY_PROPERTY",
|
||||
payload: {
|
||||
groupByProperty: "state_detail.group",
|
||||
},
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const setGroupByProperty = useCallback((property: TIssueGroupByOptions) => {
|
||||
dispatch({
|
||||
type: "SET_GROUP_BY_PROPERTY",
|
||||
payload: {
|
||||
groupByProperty: property,
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
const setOrderBy = useCallback((property: TIssueOrderByOptions) => {
|
||||
dispatch({
|
||||
type: "SET_ORDER_BY_PROPERTY",
|
||||
payload: {
|
||||
orderBy: property,
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
const setShowEmptyGroups = useCallback((property: boolean) => {
|
||||
dispatch({
|
||||
type: "SET_SHOW_EMPTY_STATES",
|
||||
payload: {
|
||||
showEmptyGroups: property,
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
const setShowSubIssues = useCallback((property: boolean) => {
|
||||
dispatch({
|
||||
type: "SET_SHOW_SUB_ISSUES",
|
||||
payload: {
|
||||
showSubIssues: property,
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
const setFilters = useCallback(
|
||||
(property: Partial<IIssueFilterOptions>) => {
|
||||
Object.keys(property).forEach((key) => {
|
||||
if (property[key as keyof typeof property]?.length === 0)
|
||||
property[key as keyof typeof property] = null;
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: "SET_FILTERS",
|
||||
payload: {
|
||||
filters: {
|
||||
...state.filters,
|
||||
...property,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
[state]
|
||||
);
|
||||
|
||||
const setProperties = useCallback(
|
||||
(key: keyof Properties) => {
|
||||
dispatch({
|
||||
type: "SET_PROPERTIES",
|
||||
payload: {
|
||||
properties: {
|
||||
...state.properties,
|
||||
[key]: !state.properties[key],
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
[state]
|
||||
);
|
||||
|
||||
return (
|
||||
<profileIssuesContext.Provider
|
||||
value={{
|
||||
issueView: state.issueView,
|
||||
setIssueView,
|
||||
groupByProperty: state.groupByProperty,
|
||||
setGroupByProperty,
|
||||
orderBy: state.orderBy,
|
||||
setOrderBy,
|
||||
showEmptyGroups: state.showEmptyGroups,
|
||||
setShowEmptyGroups,
|
||||
showSubIssues: state.showSubIssues,
|
||||
setShowSubIssues,
|
||||
filters: state.filters,
|
||||
setFilters,
|
||||
properties: state.properties,
|
||||
setProperties,
|
||||
}}
|
||||
>
|
||||
<ToastAlert />
|
||||
{children}
|
||||
</profileIssuesContext.Provider>
|
||||
);
|
||||
};
|
312
apps/app/helpers/activity.helper.tsx
Normal file
312
apps/app/helpers/activity.helper.tsx
Normal file
@ -0,0 +1,312 @@
|
||||
// icons
|
||||
import { Icon } from "components/ui";
|
||||
import { Squares2X2Icon } from "@heroicons/react/24/outline";
|
||||
import { BlockedIcon, BlockerIcon } from "components/icons";
|
||||
// helpers
|
||||
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||
import { capitalizeFirstLetter } from "helpers/string.helper";
|
||||
// types
|
||||
import { IIssueActivity } from "types";
|
||||
|
||||
export const activityDetails: {
|
||||
[key: string]: {
|
||||
message: (activity: IIssueActivity) => React.ReactNode;
|
||||
icon: React.ReactNode;
|
||||
};
|
||||
} = {
|
||||
assignees: {
|
||||
message: (activity) => {
|
||||
if (activity.old_value === "")
|
||||
return (
|
||||
<>
|
||||
added a new assignee{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
|
||||
</>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<>
|
||||
removed the assignee{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.old_value}</span>.
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <Icon iconName="group" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
archived_at: {
|
||||
message: (activity) => {
|
||||
if (activity.new_value === "restore") return "restored the issue.";
|
||||
else return "archived the issue.";
|
||||
},
|
||||
icon: <Icon iconName="archive" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
attachment: {
|
||||
message: (activity) => {
|
||||
if (activity.verb === "created")
|
||||
return (
|
||||
<>
|
||||
uploaded a new{" "}
|
||||
<a
|
||||
href={`${activity.new_value}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||
>
|
||||
attachment
|
||||
<Icon iconName="launch" className="!text-xs" />
|
||||
</a>
|
||||
</>
|
||||
);
|
||||
else return "removed an attachment.";
|
||||
},
|
||||
icon: <Icon iconName="attach_file" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
blocking: {
|
||||
message: (activity) => {
|
||||
if (activity.old_value === "")
|
||||
return (
|
||||
<>
|
||||
marked this issue is blocking issue{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
|
||||
</>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<>
|
||||
removed the blocking issue{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.old_value}</span>.
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <BlockerIcon height="12" width="12" color="#6b7280" />,
|
||||
},
|
||||
blocks: {
|
||||
message: (activity) => {
|
||||
if (activity.old_value === "")
|
||||
return (
|
||||
<>
|
||||
marked this issue is being blocked by{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
|
||||
</>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<>
|
||||
removed this issue being blocked by issue{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.old_value}</span>.
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <BlockedIcon height="12" width="12" color="#6b7280" />,
|
||||
},
|
||||
cycles: {
|
||||
message: (activity) => {
|
||||
if (activity.verb === "created")
|
||||
return (
|
||||
<>
|
||||
added this issue to the cycle{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
|
||||
</>
|
||||
);
|
||||
else if (activity.verb === "updated")
|
||||
return (
|
||||
<>
|
||||
set the cycle to{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
|
||||
</>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<>
|
||||
removed the issue from the cycle{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.old_value}</span>.
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <Icon iconName="contrast" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
description: {
|
||||
message: (activity) => "updated the description.",
|
||||
icon: <Icon iconName="chat" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
estimate_point: {
|
||||
message: (activity) => {
|
||||
if (!activity.new_value) return "removed the estimate point.";
|
||||
else
|
||||
return (
|
||||
<>
|
||||
set the estimate point to{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <Icon iconName="change_history" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
issue: {
|
||||
message: (activity) => {
|
||||
if (activity.verb === "created") return "created the issue.";
|
||||
else return "deleted an issue.";
|
||||
},
|
||||
icon: <Icon iconName="stack" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
labels: {
|
||||
message: (activity) => {
|
||||
if (activity.old_value === "")
|
||||
return (
|
||||
<>
|
||||
added a new label{" "}
|
||||
<span className="inline-flex items-center gap-3 rounded-full border border-custom-border-300 px-2 py-0.5 text-xs">
|
||||
<span
|
||||
className="h-1.5 w-1.5 rounded-full"
|
||||
style={{
|
||||
backgroundColor: "#000000",
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<>
|
||||
removed the label{" "}
|
||||
<span className="inline-flex items-center gap-3 rounded-full border border-custom-border-300 px-2 py-0.5 text-xs">
|
||||
<span
|
||||
className="h-1.5 w-1.5 rounded-full"
|
||||
style={{
|
||||
backgroundColor: "#000000",
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="font-medium text-custom-text-100">{activity.old_value}</span>
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <Icon iconName="sell" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
link: {
|
||||
message: (activity) => {
|
||||
if (activity.verb === "created")
|
||||
return (
|
||||
<>
|
||||
added this{" "}
|
||||
<a
|
||||
href={`${activity.new_value}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||
>
|
||||
link
|
||||
<Icon iconName="launch" className="!text-xs" />
|
||||
</a>{" "}
|
||||
to the issue.
|
||||
</>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<>
|
||||
removed this{" "}
|
||||
<a
|
||||
href={`${activity.old_value}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||
>
|
||||
link
|
||||
<Icon iconName="launch" className="!text-xs" />
|
||||
</a>{" "}
|
||||
from the issue.
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <Icon iconName="link" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
modules: {
|
||||
message: (activity) => {
|
||||
if (activity.verb === "created")
|
||||
return (
|
||||
<>
|
||||
added this issue to the module{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
|
||||
</>
|
||||
);
|
||||
else if (activity.verb === "updated")
|
||||
return (
|
||||
<>
|
||||
set the module to{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
|
||||
</>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<>
|
||||
removed the issue from the module{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.old_value}</span>.
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <Icon iconName="dataset" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
name: {
|
||||
message: (activity) => `set the name to ${activity.new_value}.`,
|
||||
icon: <Icon iconName="chat" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
parent: {
|
||||
message: (activity) => {
|
||||
if (!activity.new_value)
|
||||
return (
|
||||
<>
|
||||
removed the parent{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.old_value}</span>.
|
||||
</>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<>
|
||||
set the parent to{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <Icon iconName="supervised_user_circle" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
priority: {
|
||||
message: (activity) => (
|
||||
<>
|
||||
set the priority to{" "}
|
||||
<span className="font-medium text-custom-text-100">
|
||||
{activity.new_value ? capitalizeFirstLetter(activity.new_value) : "None"}
|
||||
</span>
|
||||
.
|
||||
</>
|
||||
),
|
||||
icon: <Icon iconName="signal_cellular_alt" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
state: {
|
||||
message: (activity) => (
|
||||
<>
|
||||
set the state to{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
|
||||
</>
|
||||
),
|
||||
icon: <Squares2X2Icon className="h-3 w-3" aria-hidden="true" />,
|
||||
},
|
||||
target_date: {
|
||||
message: (activity) => {
|
||||
if (!activity.new_value) return "removed the due date.";
|
||||
else
|
||||
return (
|
||||
<>
|
||||
set the due date to{" "}
|
||||
<span className="font-medium text-custom-text-100">
|
||||
{renderShortDateWithYearFormat(activity.new_value)}
|
||||
</span>
|
||||
.
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <Icon iconName="calendar_today" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
};
|
@ -62,7 +62,7 @@ const useIssuesView = () => {
|
||||
sub_issue: showSubIssues,
|
||||
};
|
||||
|
||||
const { data: projectIssues } = useSWR(
|
||||
const { data: projectIssues, mutate: mutateProjectIssues } = useSWR(
|
||||
workspaceSlug && projectId && params
|
||||
? PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string, params)
|
||||
: null,
|
||||
@ -72,7 +72,7 @@ const useIssuesView = () => {
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: projectArchivedIssues } = useSWR(
|
||||
const { data: projectArchivedIssues, mutate: mutateProjectArchivedIssues } = useSWR(
|
||||
workspaceSlug && projectId && params && isArchivedIssues && !archivedIssueId
|
||||
? PROJECT_ARCHIVED_ISSUES_LIST_WITH_PARAMS(projectId as string, params)
|
||||
: null,
|
||||
@ -81,7 +81,7 @@ const useIssuesView = () => {
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: cycleIssues } = useSWR(
|
||||
const { data: cycleIssues, mutate: mutateCycleIssues } = useSWR(
|
||||
workspaceSlug && projectId && cycleId && params
|
||||
? CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params)
|
||||
: null,
|
||||
@ -96,7 +96,7 @@ const useIssuesView = () => {
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: moduleIssues } = useSWR(
|
||||
const { data: moduleIssues, mutate: mutateModuleIssues } = useSWR(
|
||||
workspaceSlug && projectId && moduleId && params
|
||||
? MODULE_ISSUES_WITH_PARAMS(moduleId as string, params)
|
||||
: null,
|
||||
@ -111,7 +111,7 @@ const useIssuesView = () => {
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: viewIssues } = useSWR(
|
||||
const { data: viewIssues, mutate: mutateViewIssues } = useSWR(
|
||||
workspaceSlug && projectId && viewId && params ? VIEW_ISSUES(viewId.toString(), params) : null,
|
||||
workspaceSlug && projectId && viewId && params
|
||||
? () =>
|
||||
@ -187,6 +187,15 @@ const useIssuesView = () => {
|
||||
|
||||
return {
|
||||
groupedByIssues,
|
||||
mutateIssues: cycleId
|
||||
? mutateCycleIssues
|
||||
: moduleId
|
||||
? mutateModuleIssues
|
||||
: viewId
|
||||
? mutateViewIssues
|
||||
: isArchivedIssues
|
||||
? mutateProjectArchivedIssues
|
||||
: mutateProjectIssues,
|
||||
issueView: isArchivedIssues ? "list" : issueView,
|
||||
groupByProperty,
|
||||
setGroupByProperty,
|
||||
|
126
apps/app/hooks/use-profile-issues.tsx
Normal file
126
apps/app/hooks/use-profile-issues.tsx
Normal file
@ -0,0 +1,126 @@
|
||||
import { useContext, useEffect, useMemo } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// services
|
||||
import userService from "services/user.service";
|
||||
// contexts
|
||||
import { profileIssuesContext } from "contexts/profile-issues-context";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
// fetch-keys
|
||||
import { USER_PROFILE_ISSUES } from "constants/fetch-keys";
|
||||
|
||||
const useProfileIssues = (workspaceSlug: string | undefined, userId: string | undefined) => {
|
||||
const {
|
||||
issueView,
|
||||
setIssueView,
|
||||
groupByProperty,
|
||||
setGroupByProperty,
|
||||
orderBy,
|
||||
setOrderBy,
|
||||
showEmptyGroups,
|
||||
setShowEmptyGroups,
|
||||
showSubIssues,
|
||||
setShowSubIssues,
|
||||
filters,
|
||||
setFilters,
|
||||
properties,
|
||||
setProperties,
|
||||
} = useContext(profileIssuesContext);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const params: any = {
|
||||
assignees: filters?.assignees ? filters?.assignees.join(",") : undefined,
|
||||
created_by: filters?.created_by ? filters?.created_by.join(",") : undefined,
|
||||
group_by: groupByProperty,
|
||||
labels: filters?.labels ? filters?.labels.join(",") : undefined,
|
||||
order_by: orderBy,
|
||||
priority: filters?.priority ? filters?.priority.join(",") : undefined,
|
||||
state_group: filters?.state_group ? filters?.state_group.join(",") : undefined,
|
||||
target_date: filters?.target_date ? filters?.target_date.join(",") : undefined,
|
||||
type: filters?.type ? filters?.type : undefined,
|
||||
subscriber: filters?.subscriber ? filters?.subscriber : undefined,
|
||||
};
|
||||
|
||||
const { data: userProfileIssues, mutate: mutateProfileIssues } = useSWR(
|
||||
workspaceSlug && userId
|
||||
? USER_PROFILE_ISSUES(workspaceSlug.toString(), userId.toString(), params)
|
||||
: null,
|
||||
workspaceSlug && userId
|
||||
? () => userService.getUserProfileIssues(workspaceSlug.toString(), userId.toString(), params)
|
||||
: null
|
||||
);
|
||||
|
||||
const groupedIssues:
|
||||
| {
|
||||
[key: string]: IIssue[];
|
||||
}
|
||||
| undefined = useMemo(() => {
|
||||
if (!userProfileIssues) return undefined;
|
||||
|
||||
if (Array.isArray(userProfileIssues))
|
||||
return {
|
||||
allIssues: userProfileIssues,
|
||||
};
|
||||
|
||||
return userProfileIssues;
|
||||
}, [userProfileIssues]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!userId || !filters) return;
|
||||
|
||||
console.log("Triggered");
|
||||
|
||||
if (
|
||||
router.pathname.includes("assigned") &&
|
||||
(!filters.assignees || !filters.assignees.includes(userId))
|
||||
) {
|
||||
setFilters({ assignees: [...(filters.assignees ?? []), userId] });
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
router.pathname.includes("created") &&
|
||||
(!filters.created_by || !filters.created_by.includes(userId))
|
||||
) {
|
||||
setFilters({ created_by: [...(filters.created_by ?? []), userId] });
|
||||
return;
|
||||
}
|
||||
|
||||
if (router.pathname.includes("subscribed") && filters.subscriber === null) {
|
||||
setFilters({ subscriber: userId });
|
||||
return;
|
||||
}
|
||||
}, [filters, router, setFilters, userId]);
|
||||
|
||||
const isEmpty =
|
||||
Object.values(groupedIssues ?? {}).every((group) => group.length === 0) ||
|
||||
Object.keys(groupedIssues ?? {}).length === 0;
|
||||
|
||||
return {
|
||||
groupedIssues,
|
||||
issueView,
|
||||
setIssueView,
|
||||
groupByProperty,
|
||||
setGroupByProperty,
|
||||
orderBy,
|
||||
setOrderBy,
|
||||
showEmptyGroups,
|
||||
setShowEmptyGroups,
|
||||
showSubIssues,
|
||||
setShowSubIssues,
|
||||
filters,
|
||||
setFilters,
|
||||
properties,
|
||||
setProperties,
|
||||
isEmpty,
|
||||
mutateProfileIssues,
|
||||
params,
|
||||
};
|
||||
};
|
||||
|
||||
export default useProfileIssues;
|
@ -6,12 +6,18 @@ import { PROJECT_MEMBERS } from "constants/fetch-keys";
|
||||
// hooks
|
||||
import useUser from "./use-user";
|
||||
|
||||
const useProjectMembers = (workspaceSlug: string | undefined, projectId: string | undefined) => {
|
||||
const useProjectMembers = (
|
||||
workspaceSlug: string | undefined,
|
||||
projectId: string | undefined,
|
||||
fetchCondition?: boolean
|
||||
) => {
|
||||
fetchCondition = fetchCondition ?? true;
|
||||
|
||||
const { user } = useUser();
|
||||
// fetching project members
|
||||
const { data: members } = useSWR(
|
||||
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId) : null,
|
||||
workspaceSlug && projectId
|
||||
workspaceSlug && projectId && fetchCondition ? PROJECT_MEMBERS(projectId) : null,
|
||||
workspaceSlug && projectId && fetchCondition
|
||||
? () => projectService.projectMembers(workspaceSlug, projectId)
|
||||
: null
|
||||
);
|
||||
|
@ -87,7 +87,7 @@ const MyIssuesPage: NextPage = () => {
|
||||
}
|
||||
>
|
||||
<div className="h-full w-full flex flex-col overflow-hidden">
|
||||
<div className="border-b border-custom-border-300">
|
||||
<div className="px-4 sm:px-5 border-b border-custom-border-300">
|
||||
<div className="flex items-center overflow-x-scroll">
|
||||
{tabsList.map((tab) => (
|
||||
<button
|
||||
|
48
apps/app/pages/[workspaceSlug]/profile/[userId]/assigned.tsx
Normal file
48
apps/app/pages/[workspaceSlug]/profile/[userId]/assigned.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// contexts
|
||||
import { ProfileIssuesContextProvider } from "contexts/profile-issues-context";
|
||||
// hooks
|
||||
import useUser from "hooks/use-user";
|
||||
// layouts
|
||||
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
||||
// components
|
||||
import { ProfileIssuesView, ProfileNavbar, ProfileSidebar } from "components/profile";
|
||||
// ui
|
||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||
// types
|
||||
import type { NextPage } from "next";
|
||||
|
||||
const ProfileAssignedIssues: NextPage = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { user } = useUser();
|
||||
|
||||
return (
|
||||
<ProfileIssuesContextProvider>
|
||||
<WorkspaceAuthorizationLayout
|
||||
breadcrumbs={
|
||||
<Breadcrumbs>
|
||||
<BreadcrumbItem title="Settings" link={`/${workspaceSlug}/me/profile`} />
|
||||
<BreadcrumbItem title={`${user?.first_name} ${user?.last_name}`} />
|
||||
</Breadcrumbs>
|
||||
}
|
||||
>
|
||||
<div className="h-full w-full flex overflow-hidden">
|
||||
<div className="h-full w-full flex flex-col overflow-hidden">
|
||||
<ProfileNavbar />
|
||||
<div className="h-full w-full flex flex-col overflow-hidden">
|
||||
<ProfileIssuesView />
|
||||
</div>
|
||||
</div>
|
||||
<ProfileSidebar />
|
||||
</div>
|
||||
</WorkspaceAuthorizationLayout>
|
||||
</ProfileIssuesContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileAssignedIssues;
|
48
apps/app/pages/[workspaceSlug]/profile/[userId]/created.tsx
Normal file
48
apps/app/pages/[workspaceSlug]/profile/[userId]/created.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// contexts
|
||||
import { ProfileIssuesContextProvider } from "contexts/profile-issues-context";
|
||||
// hooks
|
||||
import useUser from "hooks/use-user";
|
||||
// layouts
|
||||
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
||||
// components
|
||||
import { ProfileIssuesView, ProfileNavbar, ProfileSidebar } from "components/profile";
|
||||
// ui
|
||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||
// types
|
||||
import type { NextPage } from "next";
|
||||
|
||||
const ProfileCreatedIssues: NextPage = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { user } = useUser();
|
||||
|
||||
return (
|
||||
<ProfileIssuesContextProvider>
|
||||
<WorkspaceAuthorizationLayout
|
||||
breadcrumbs={
|
||||
<Breadcrumbs>
|
||||
<BreadcrumbItem title="Settings" link={`/${workspaceSlug}/me/profile`} />
|
||||
<BreadcrumbItem title={`${user?.first_name} ${user?.last_name}`} />
|
||||
</Breadcrumbs>
|
||||
}
|
||||
>
|
||||
<div className="h-full w-full flex overflow-hidden">
|
||||
<div className="h-full w-full flex flex-col overflow-hidden">
|
||||
<ProfileNavbar />
|
||||
<div className="h-full w-full flex flex-col overflow-hidden">
|
||||
<ProfileIssuesView />
|
||||
</div>
|
||||
</div>
|
||||
<ProfileSidebar />
|
||||
</div>
|
||||
</WorkspaceAuthorizationLayout>
|
||||
</ProfileIssuesContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileCreatedIssues;
|
152
apps/app/pages/[workspaceSlug]/profile/[userId]/index.tsx
Normal file
152
apps/app/pages/[workspaceSlug]/profile/[userId]/index.tsx
Normal file
@ -0,0 +1,152 @@
|
||||
import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
import Link from "next/link";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// layouts
|
||||
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
||||
// services
|
||||
import userService from "services/user.service";
|
||||
// components
|
||||
import {
|
||||
ProfileNavbar,
|
||||
ProfilePriorityDistribution,
|
||||
ProfileSidebar,
|
||||
ProfileStateDistribution,
|
||||
ProfileStats,
|
||||
ProfileWorkload,
|
||||
} from "components/profile";
|
||||
// ui
|
||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||
import { Icon, Loader } from "components/ui";
|
||||
// helpers
|
||||
import { activityDetails } from "helpers/activity.helper";
|
||||
import { timeAgo } from "helpers/date-time.helper";
|
||||
// types
|
||||
import type { NextPage } from "next";
|
||||
import { IUserStateDistribution, TStateGroups } from "types";
|
||||
// constants
|
||||
import { USER_PROFILE_DATA, USER_PROFILE_ACTIVITY } from "constants/fetch-keys";
|
||||
import { GROUP_CHOICES } from "constants/project";
|
||||
|
||||
const ProfileOverview: NextPage = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, userId } = router.query;
|
||||
|
||||
const { data: userProfile } = useSWR(
|
||||
workspaceSlug && userId ? USER_PROFILE_DATA(workspaceSlug.toString(), userId.toString()) : null,
|
||||
workspaceSlug && userId
|
||||
? () => userService.getUserProfileData(workspaceSlug.toString(), userId.toString())
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: userProfileActivity } = useSWR(
|
||||
workspaceSlug && userId
|
||||
? USER_PROFILE_ACTIVITY(workspaceSlug.toString(), userId.toString())
|
||||
: null,
|
||||
workspaceSlug && userId
|
||||
? () => userService.getUserProfileActivity(workspaceSlug.toString(), userId.toString())
|
||||
: null
|
||||
);
|
||||
|
||||
const stateDistribution: IUserStateDistribution[] = Object.keys(GROUP_CHOICES).map((key) => {
|
||||
const group = userProfile?.state_distribution.find((g) => g.state_group === key);
|
||||
|
||||
if (group) return group;
|
||||
else return { state_group: key as TStateGroups, state_count: 0 };
|
||||
});
|
||||
|
||||
return (
|
||||
<WorkspaceAuthorizationLayout
|
||||
breadcrumbs={
|
||||
<Breadcrumbs>
|
||||
<BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} />
|
||||
<BreadcrumbItem title={`User Name`} />
|
||||
</Breadcrumbs>
|
||||
}
|
||||
>
|
||||
<div className="h-full w-full flex overflow-hidden">
|
||||
<div className="h-full w-full flex flex-col overflow-hidden">
|
||||
<ProfileNavbar />
|
||||
<div className="h-full w-full overflow-y-auto px-9 py-5 space-y-7">
|
||||
<ProfileStats userProfile={userProfile} />
|
||||
<ProfileWorkload stateDistribution={stateDistribution} />
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 items-stretch gap-5">
|
||||
<ProfilePriorityDistribution userProfile={userProfile} />
|
||||
<ProfileStateDistribution
|
||||
stateDistribution={stateDistribution}
|
||||
userProfile={userProfile}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-lg font-medium">Recent Activity</h3>
|
||||
<div className="border border-custom-border-100 rounded p-6">
|
||||
{userProfileActivity ? (
|
||||
<div className="space-y-5">
|
||||
{userProfileActivity.results.map((activity) => (
|
||||
<div key={activity.id} className="flex gap-3">
|
||||
<div className="flex-shrink-0">
|
||||
{activity.actor_detail.avatar && activity.actor_detail.avatar !== "" ? (
|
||||
<img
|
||||
src={activity.actor_detail.avatar}
|
||||
alt={activity.actor_detail.first_name}
|
||||
height={24}
|
||||
width={24}
|
||||
className="rounded"
|
||||
/>
|
||||
) : (
|
||||
<div className="grid h-6 w-6 place-items-center rounded border-2 bg-gray-700 text-xs text-white">
|
||||
{activity.actor_detail.first_name.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="-mt-1">
|
||||
<p className="text-sm text-custom-text-200">
|
||||
<span className="font-medium text-custom-text-100">
|
||||
{activity.actor_detail.first_name} {activity.actor_detail.last_name}{" "}
|
||||
</span>
|
||||
{activity.field ? (
|
||||
activityDetails[activity.field]?.message(activity as any)
|
||||
) : (
|
||||
<span>
|
||||
created this{" "}
|
||||
<Link
|
||||
href={`/${activity.workspace_detail.slug}/projects/${activity.project}/issues/${activity.issue}`}
|
||||
>
|
||||
<a className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline">
|
||||
Issue
|
||||
<Icon iconName="launch" className="!text-xs" />
|
||||
</a>
|
||||
</Link>
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
<p className="text-xs text-custom-text-200">
|
||||
{timeAgo(activity.created_at)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Loader className="space-y-5">
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ProfileSidebar />
|
||||
</div>
|
||||
</WorkspaceAuthorizationLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileOverview;
|
@ -0,0 +1,48 @@
|
||||
import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// contexts
|
||||
import { ProfileIssuesContextProvider } from "contexts/profile-issues-context";
|
||||
// hooks
|
||||
import useUser from "hooks/use-user";
|
||||
// layouts
|
||||
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
||||
// components
|
||||
import { ProfileIssuesView, ProfileNavbar, ProfileSidebar } from "components/profile";
|
||||
// ui
|
||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||
// types
|
||||
import type { NextPage } from "next";
|
||||
|
||||
const ProfileSubscribedIssues: NextPage = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { user } = useUser();
|
||||
|
||||
return (
|
||||
<ProfileIssuesContextProvider>
|
||||
<WorkspaceAuthorizationLayout
|
||||
breadcrumbs={
|
||||
<Breadcrumbs>
|
||||
<BreadcrumbItem title="Settings" link={`/${workspaceSlug}/me/profile`} />
|
||||
<BreadcrumbItem title={`${user?.first_name} ${user?.last_name}`} />
|
||||
</Breadcrumbs>
|
||||
}
|
||||
>
|
||||
<div className="h-full w-full flex overflow-hidden">
|
||||
<div className="h-full w-full flex flex-col overflow-hidden">
|
||||
<ProfileNavbar />
|
||||
<div className="h-full w-full flex flex-col overflow-hidden">
|
||||
<ProfileIssuesView />
|
||||
</div>
|
||||
</div>
|
||||
<ProfileSidebar />
|
||||
</div>
|
||||
</WorkspaceAuthorizationLayout>
|
||||
</ProfileIssuesContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileSubscribedIssues;
|
@ -7,6 +7,8 @@ import type {
|
||||
IIssue,
|
||||
IUser,
|
||||
IUserActivityResponse,
|
||||
IUserProfileData,
|
||||
IUserProfileProjectSegregation,
|
||||
IUserWorkspaceDashboard,
|
||||
} from "types";
|
||||
|
||||
@ -144,6 +146,55 @@ class UserService extends APIService {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getUserProfileData(workspaceSlug: string, userId: string): Promise<IUserProfileData> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/user-stats/${userId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getUserProfileProjectsSegregation(
|
||||
workspaceSlug: string,
|
||||
userId: string
|
||||
): Promise<IUserProfileProjectSegregation> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/user-profile/${userId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getUserProfileActivity(
|
||||
workspaceSlug: string,
|
||||
userId: string
|
||||
): Promise<IUserActivityResponse> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/user-activity/${userId}/?per_page=15`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getUserProfileIssues(
|
||||
workspaceSlug: string,
|
||||
userId: string,
|
||||
params: any
|
||||
): Promise<
|
||||
| {
|
||||
[key: string]: IIssue[];
|
||||
}
|
||||
| IIssue[]
|
||||
> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/user-issues/${userId}/`, {
|
||||
params,
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new UserService();
|
||||
|
7
apps/app/types/issues.d.ts
vendored
7
apps/app/types/issues.d.ts
vendored
@ -1,3 +1,4 @@
|
||||
import { KeyedMutator } from "swr";
|
||||
import type {
|
||||
IState,
|
||||
IUser,
|
||||
@ -292,6 +293,12 @@ export interface IIssueViewProps {
|
||||
groupByProperty: TIssueGroupByOptions;
|
||||
isEmpty: boolean;
|
||||
issueView: TIssueViewOptions;
|
||||
mutateIssues: KeyedMutator<
|
||||
| IIssue[]
|
||||
| {
|
||||
[key: string]: IIssue[];
|
||||
}
|
||||
>;
|
||||
orderBy: TIssueOrderByOptions;
|
||||
params: any;
|
||||
properties: Properties;
|
||||
|
52
apps/app/types/users.d.ts
vendored
52
apps/app/types/users.d.ts
vendored
@ -1,4 +1,12 @@
|
||||
import { IIssue, IIssueLite, IWorkspace, NestedKeyOf, Properties } from "./";
|
||||
import {
|
||||
IIssue,
|
||||
IIssueLite,
|
||||
IWorkspace,
|
||||
IWorkspaceLite,
|
||||
NestedKeyOf,
|
||||
Properties,
|
||||
TStateGroups,
|
||||
} from "./";
|
||||
|
||||
export interface IUser {
|
||||
avatar: string;
|
||||
@ -51,7 +59,6 @@ export interface ICurrentUserResponse extends IUser {
|
||||
last_workspace_slug: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IUserLite {
|
||||
avatar: string;
|
||||
created_at: Date;
|
||||
@ -67,8 +74,13 @@ export interface IUserActivity {
|
||||
activity_count: number;
|
||||
}
|
||||
|
||||
export interface IUserPriorityDistribution {
|
||||
priority: string;
|
||||
priority_count: number;
|
||||
}
|
||||
|
||||
export interface IUserStateDistribution {
|
||||
state_group: string;
|
||||
state_group: TStateGroups;
|
||||
state_count: number;
|
||||
}
|
||||
|
||||
@ -107,6 +119,7 @@ export interface IUserDetailedActivity {
|
||||
updated_by: string | null;
|
||||
verb: string;
|
||||
workspace: string;
|
||||
workspace_detail: IWorkspaceLite;
|
||||
}
|
||||
|
||||
export interface IUserActivityResponse {
|
||||
@ -133,3 +146,36 @@ export type OnboardingSteps = {
|
||||
workspace_invite: boolean;
|
||||
workspace_join: boolean;
|
||||
};
|
||||
|
||||
export interface IUserProfileData {
|
||||
assigned_issues: number;
|
||||
completed_issues: number;
|
||||
created_issues: number;
|
||||
pending_issues: number;
|
||||
priority_distribution: IUserPriorityDistribution[];
|
||||
state_distribution: IUserStateDistribution[];
|
||||
subscribed_issues: number;
|
||||
}
|
||||
|
||||
export interface IUserProfileProjectSegregation {
|
||||
project_data: {
|
||||
assigned_issues: number;
|
||||
completed_issues: number;
|
||||
created_issues: number;
|
||||
emoji: string | null;
|
||||
icon_prop: null;
|
||||
id: string;
|
||||
identifier: string;
|
||||
name: string;
|
||||
pending_issues: number;
|
||||
}[];
|
||||
user_data: {
|
||||
avatar: string;
|
||||
cover_image: string | null;
|
||||
date_joined: Date;
|
||||
email: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
user_timezone: string;
|
||||
};
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user