mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
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
|
// types
|
||||||
import { ICurrentUserResponse, IIssue, IIssueViewProps, ISubIssueResponse, UserAuth } from "types";
|
import { ICurrentUserResponse, IIssue, IIssueViewProps, ISubIssueResponse, UserAuth } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import {
|
import { CYCLE_DETAILS, MODULE_DETAILS, SUB_ISSUES } from "constants/fetch-keys";
|
||||||
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";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
type?: string;
|
type?: string;
|
||||||
@ -99,10 +90,10 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
|
|
||||||
const actionSectionRef = useRef<HTMLDivElement | null>(null);
|
const actionSectionRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
const { groupByProperty: selectedGroup, orderBy, params, properties } = viewProps;
|
const { groupByProperty: selectedGroup, orderBy, properties, mutateIssues } = viewProps;
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
@ -110,16 +101,6 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
(formData: Partial<IIssue>, issue: IIssue) => {
|
(formData: Partial<IIssue>, issue: IIssue) => {
|
||||||
if (!workspaceSlug || !issue) return;
|
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) {
|
if (issue.parent) {
|
||||||
mutate<ISubIssueResponse>(
|
mutate<ISubIssueResponse>(
|
||||||
SUB_ISSUES(issue.parent.toString()),
|
SUB_ISSUES(issue.parent.toString()),
|
||||||
@ -142,13 +123,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
false
|
false
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
mutate<
|
mutateIssues(
|
||||||
| {
|
|
||||||
[key: string]: IIssue[];
|
|
||||||
}
|
|
||||||
| IIssue[]
|
|
||||||
>(
|
|
||||||
fetchKey,
|
|
||||||
(prevData) =>
|
(prevData) =>
|
||||||
handleIssuesMutation(
|
handleIssuesMutation(
|
||||||
formData,
|
formData,
|
||||||
@ -165,7 +140,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
issuesService
|
issuesService
|
||||||
.patchIssue(workspaceSlug as string, issue.project, issue.id, formData, user)
|
.patchIssue(workspaceSlug as string, issue.project, issue.id, formData, user)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
mutate(fetchKey);
|
mutateIssues();
|
||||||
|
|
||||||
if (cycleId) mutate(CYCLE_DETAILS(cycleId as string));
|
if (cycleId) mutate(CYCLE_DETAILS(cycleId as string));
|
||||||
if (moduleId) mutate(MODULE_DETAILS(moduleId as string));
|
if (moduleId) mutate(MODULE_DETAILS(moduleId as string));
|
||||||
@ -175,13 +150,11 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
workspaceSlug,
|
workspaceSlug,
|
||||||
cycleId,
|
cycleId,
|
||||||
moduleId,
|
moduleId,
|
||||||
viewId,
|
|
||||||
groupTitle,
|
groupTitle,
|
||||||
index,
|
index,
|
||||||
selectedGroup,
|
selectedGroup,
|
||||||
|
mutateIssues,
|
||||||
orderBy,
|
orderBy,
|
||||||
params,
|
|
||||||
router,
|
|
||||||
user,
|
user,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
@ -79,6 +79,7 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
groupedByIssues,
|
groupedByIssues,
|
||||||
|
mutateIssues,
|
||||||
issueView,
|
issueView,
|
||||||
groupByProperty: selectedGroup,
|
groupByProperty: selectedGroup,
|
||||||
orderBy,
|
orderBy,
|
||||||
@ -525,6 +526,7 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
groupedIssues: groupedByIssues,
|
groupedIssues: groupedByIssues,
|
||||||
isEmpty,
|
isEmpty,
|
||||||
issueView,
|
issueView,
|
||||||
|
mutateIssues,
|
||||||
orderBy,
|
orderBy,
|
||||||
params,
|
params,
|
||||||
properties,
|
properties,
|
||||||
|
@ -35,25 +35,9 @@ import { LayerDiagonalIcon } from "components/icons";
|
|||||||
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
|
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
|
||||||
import { handleIssuesMutation } from "constants/issue";
|
import { handleIssuesMutation } from "constants/issue";
|
||||||
// types
|
// types
|
||||||
import {
|
import { ICurrentUserResponse, IIssue, IIssueViewProps, ISubIssueResponse, UserAuth } from "types";
|
||||||
ICurrentUserResponse,
|
|
||||||
IIssue,
|
|
||||||
IIssueViewProps,
|
|
||||||
ISubIssueResponse,
|
|
||||||
Properties,
|
|
||||||
UserAuth,
|
|
||||||
} from "types";
|
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import {
|
import { CYCLE_DETAILS, MODULE_DETAILS, SUB_ISSUES } from "constants/fetch-keys";
|
||||||
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";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
type?: string;
|
type?: string;
|
||||||
@ -89,27 +73,17 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 });
|
const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 });
|
||||||
|
|
||||||
const router = useRouter();
|
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 isArchivedIssues = router.pathname.includes("archived-issues");
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const { groupByProperty: selectedGroup, orderBy, params, properties } = viewProps;
|
const { groupByProperty: selectedGroup, orderBy, properties, mutateIssues } = viewProps;
|
||||||
|
|
||||||
const partialUpdateIssue = useCallback(
|
const partialUpdateIssue = useCallback(
|
||||||
(formData: Partial<IIssue>, issue: IIssue) => {
|
(formData: Partial<IIssue>, issue: IIssue) => {
|
||||||
if (!workspaceSlug || !issue) return;
|
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) {
|
if (issue.parent) {
|
||||||
mutate<ISubIssueResponse>(
|
mutate<ISubIssueResponse>(
|
||||||
SUB_ISSUES(issue.parent.toString()),
|
SUB_ISSUES(issue.parent.toString()),
|
||||||
@ -132,13 +106,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
false
|
false
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
mutate<
|
mutateIssues(
|
||||||
| {
|
|
||||||
[key: string]: IIssue[];
|
|
||||||
}
|
|
||||||
| IIssue[]
|
|
||||||
>(
|
|
||||||
fetchKey,
|
|
||||||
(prevData) =>
|
(prevData) =>
|
||||||
handleIssuesMutation(
|
handleIssuesMutation(
|
||||||
formData,
|
formData,
|
||||||
@ -155,7 +123,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
issuesService
|
issuesService
|
||||||
.patchIssue(workspaceSlug as string, issue.project, issue.id, formData, user)
|
.patchIssue(workspaceSlug as string, issue.project, issue.id, formData, user)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
mutate(fetchKey);
|
mutateIssues();
|
||||||
|
|
||||||
if (cycleId) mutate(CYCLE_DETAILS(cycleId as string));
|
if (cycleId) mutate(CYCLE_DETAILS(cycleId as string));
|
||||||
if (moduleId) mutate(MODULE_DETAILS(moduleId as string));
|
if (moduleId) mutate(MODULE_DETAILS(moduleId as string));
|
||||||
@ -165,13 +133,11 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
workspaceSlug,
|
workspaceSlug,
|
||||||
cycleId,
|
cycleId,
|
||||||
moduleId,
|
moduleId,
|
||||||
viewId,
|
|
||||||
groupTitle,
|
groupTitle,
|
||||||
index,
|
index,
|
||||||
selectedGroup,
|
selectedGroup,
|
||||||
|
mutateIssues,
|
||||||
orderBy,
|
orderBy,
|
||||||
params,
|
|
||||||
router,
|
|
||||||
user,
|
user,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
@ -10,313 +10,14 @@ import issuesService from "services/issues.service";
|
|||||||
import { CommentCard } from "components/issues/comment";
|
import { CommentCard } from "components/issues/comment";
|
||||||
// ui
|
// ui
|
||||||
import { Icon, Loader } from "components/ui";
|
import { Icon, Loader } from "components/ui";
|
||||||
// icons
|
|
||||||
import { Squares2X2Icon } from "@heroicons/react/24/outline";
|
|
||||||
import { BlockedIcon, BlockerIcon } from "components/icons";
|
|
||||||
// helpers
|
// helpers
|
||||||
import { renderShortDateWithYearFormat, timeAgo } from "helpers/date-time.helper";
|
import { timeAgo } from "helpers/date-time.helper";
|
||||||
import { capitalizeFirstLetter } from "helpers/string.helper";
|
import { activityDetails } from "helpers/activity.helper";
|
||||||
// types
|
// types
|
||||||
import { ICurrentUserResponse, IIssueActivity, IIssueComment } from "types";
|
import { ICurrentUserResponse, IIssueComment } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { PROJECT_ISSUES_ACTIVITY } from "constants/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 = {
|
type Props = {
|
||||||
issueId: string;
|
issueId: string;
|
||||||
user: ICurrentUserResponse | undefined;
|
user: ICurrentUserResponse | undefined;
|
||||||
|
@ -25,7 +25,6 @@ import type { IIssue } from "types";
|
|||||||
// fetch-keys
|
// fetch-keys
|
||||||
import {
|
import {
|
||||||
PROJECT_ISSUES_DETAILS,
|
PROJECT_ISSUES_DETAILS,
|
||||||
PROJECT_ISSUES_LIST,
|
|
||||||
USER_ISSUE,
|
USER_ISSUE,
|
||||||
SUB_ISSUES,
|
SUB_ISSUES,
|
||||||
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||||
@ -40,11 +39,11 @@ import {
|
|||||||
import { INBOX_ISSUE_SOURCE } from "constants/inbox";
|
import { INBOX_ISSUE_SOURCE } from "constants/inbox";
|
||||||
|
|
||||||
export interface IssuesModalProps {
|
export interface IssuesModalProps {
|
||||||
isOpen: boolean;
|
|
||||||
handleClose: () => void;
|
|
||||||
data?: IIssue | null;
|
data?: IIssue | null;
|
||||||
prePopulateData?: Partial<IIssue>;
|
handleClose: () => void;
|
||||||
|
isOpen: boolean;
|
||||||
isUpdatingSingleIssue?: boolean;
|
isUpdatingSingleIssue?: boolean;
|
||||||
|
prePopulateData?: Partial<IIssue>;
|
||||||
fieldsToShow?: (
|
fieldsToShow?: (
|
||||||
| "project"
|
| "project"
|
||||||
| "name"
|
| "name"
|
||||||
@ -58,15 +57,17 @@ export interface IssuesModalProps {
|
|||||||
| "parent"
|
| "parent"
|
||||||
| "all"
|
| "all"
|
||||||
)[];
|
)[];
|
||||||
|
onSubmit?: (data: Partial<IIssue>) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
||||||
isOpen,
|
|
||||||
handleClose,
|
|
||||||
data,
|
data,
|
||||||
prePopulateData,
|
handleClose,
|
||||||
|
isOpen,
|
||||||
isUpdatingSingleIssue = false,
|
isUpdatingSingleIssue = false,
|
||||||
|
prePopulateData,
|
||||||
fieldsToShow = ["all"],
|
fieldsToShow = ["all"],
|
||||||
|
onSubmit,
|
||||||
}) => {
|
}) => {
|
||||||
// states
|
// states
|
||||||
const [createMore, setCreateMore] = useState(false);
|
const [createMore, setCreateMore] = useState(false);
|
||||||
@ -95,9 +96,14 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (data && data.project) {
|
||||||
|
setActiveProject(data.project);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (projects && projects.length > 0 && !activeProject)
|
if (projects && projects.length > 0 && !activeProject)
|
||||||
setActiveProject(projects?.find((p) => p.id === projectId)?.id ?? projects?.[0].id ?? null);
|
setActiveProject(projects?.find((p) => p.id === projectId)?.id ?? projects?.[0].id ?? null);
|
||||||
}, [activeProject, projectId, projects]);
|
}, [activeProject, data, projectId, projects]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
@ -306,6 +312,8 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
|||||||
|
|
||||||
if (!data) await createIssue(payload);
|
if (!data) await createIssue(payload);
|
||||||
else await updateIssue(payload);
|
else await updateIssue(payload);
|
||||||
|
|
||||||
|
if (onSubmit) await onSubmit(payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!projects || projects.length === 0) return null;
|
if (!projects || projects.length === 0) return null;
|
||||||
|
@ -15,12 +15,12 @@ import useUserAuth from "hooks/use-user-auth";
|
|||||||
// components
|
// components
|
||||||
import { AllViews, FiltersList } from "components/core";
|
import { AllViews, FiltersList } from "components/core";
|
||||||
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||||
import { CreateUpdateViewModal } from "components/views";
|
// helpers
|
||||||
|
import { orderArrayBy } from "helpers/array.helper";
|
||||||
// types
|
// types
|
||||||
import { IIssue, IIssueFilterOptions } from "types";
|
import { IIssue, IIssueFilterOptions } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { USER_ISSUES, WORKSPACE_LABELS } from "constants/fetch-keys";
|
import { USER_ISSUES, WORKSPACE_LABELS } from "constants/fetch-keys";
|
||||||
import { orderArrayBy } from "helpers/array.helper";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
openIssuesListModal?: () => void;
|
openIssuesListModal?: () => void;
|
||||||
@ -33,7 +33,6 @@ export const MyIssuesView: React.FC<Props> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
// create issue modal
|
// create issue modal
|
||||||
const [createIssueModal, setCreateIssueModal] = useState(false);
|
const [createIssueModal, setCreateIssueModal] = useState(false);
|
||||||
const [createViewModal, setCreateViewModal] = useState<any>(null);
|
|
||||||
const [preloadedData, setPreloadedData] = useState<
|
const [preloadedData, setPreloadedData] = useState<
|
||||||
(Partial<IIssue> & { actionType: "createIssue" | "edit" | "delete" }) | undefined
|
(Partial<IIssue> & { actionType: "createIssue" | "edit" | "delete" }) | undefined
|
||||||
>(undefined);
|
>(undefined);
|
||||||
@ -56,15 +55,15 @@ export const MyIssuesView: React.FC<Props> = ({
|
|||||||
|
|
||||||
const { user } = useUserAuth();
|
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 } =
|
const { filters, setFilters, issueView, groupBy, orderBy, properties, showEmptyGroups } =
|
||||||
useMyIssuesFilters(workspaceSlug?.toString());
|
useMyIssuesFilters(workspaceSlug?.toString());
|
||||||
|
|
||||||
const { data: labels } = useSWR(
|
const { data: labels } = useSWR(
|
||||||
workspaceSlug && (filters.labels ?? []).length > 0
|
workspaceSlug && (filters?.labels ?? []).length > 0
|
||||||
? WORKSPACE_LABELS(workspaceSlug.toString())
|
? WORKSPACE_LABELS(workspaceSlug.toString())
|
||||||
: null,
|
: null,
|
||||||
workspaceSlug && (filters.labels ?? []).length > 0
|
workspaceSlug && (filters?.labels ?? []).length > 0
|
||||||
? () => issuesService.getWorkspaceLabels(workspaceSlug.toString())
|
? () => issuesService.getWorkspaceLabels(workspaceSlug.toString())
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
@ -80,7 +79,6 @@ export const MyIssuesView: React.FC<Props> = ({
|
|||||||
const handleOnDragEnd = useCallback(
|
const handleOnDragEnd = useCallback(
|
||||||
async (result: DropResult) => {
|
async (result: DropResult) => {
|
||||||
setTrashBox(false);
|
setTrashBox(false);
|
||||||
console.log(result);
|
|
||||||
|
|
||||||
if (!result.destination || !workspaceSlug || !groupedIssues || groupBy !== "priority") return;
|
if (!result.destination || !workspaceSlug || !groupedIssues || groupBy !== "priority") return;
|
||||||
|
|
||||||
@ -212,23 +210,23 @@ export const MyIssuesView: React.FC<Props> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CreateUpdateViewModal
|
|
||||||
isOpen={createViewModal !== null}
|
|
||||||
handleClose={() => setCreateViewModal(null)}
|
|
||||||
preLoadedData={createViewModal}
|
|
||||||
user={user}
|
|
||||||
/>
|
|
||||||
<CreateUpdateIssueModal
|
<CreateUpdateIssueModal
|
||||||
isOpen={createIssueModal && preloadedData?.actionType === "createIssue"}
|
isOpen={createIssueModal && preloadedData?.actionType === "createIssue"}
|
||||||
handleClose={() => setCreateIssueModal(false)}
|
handleClose={() => setCreateIssueModal(false)}
|
||||||
prePopulateData={{
|
prePopulateData={{
|
||||||
...preloadedData,
|
...preloadedData,
|
||||||
}}
|
}}
|
||||||
|
onSubmit={async () => {
|
||||||
|
mutateMyIssues();
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<CreateUpdateIssueModal
|
<CreateUpdateIssueModal
|
||||||
isOpen={editIssueModal && issueToEdit?.actionType !== "delete"}
|
isOpen={editIssueModal && issueToEdit?.actionType !== "delete"}
|
||||||
handleClose={() => setEditIssueModal(false)}
|
handleClose={() => setEditIssueModal(false)}
|
||||||
data={issueToEdit}
|
data={issueToEdit}
|
||||||
|
onSubmit={async () => {
|
||||||
|
mutateMyIssues();
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<DeleteIssueModal
|
<DeleteIssueModal
|
||||||
handleClose={() => setDeleteIssueModal(false)}
|
handleClose={() => setDeleteIssueModal(false)}
|
||||||
@ -275,6 +273,7 @@ export const MyIssuesView: React.FC<Props> = ({
|
|||||||
groupedIssues,
|
groupedIssues,
|
||||||
isEmpty,
|
isEmpty,
|
||||||
issueView,
|
issueView,
|
||||||
|
mutateIssues: mutateMyIssues,
|
||||||
orderBy,
|
orderBy,
|
||||||
params,
|
params,
|
||||||
properties,
|
properties,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
@ -15,6 +15,7 @@ import { UserGroupIcon } from "@heroicons/react/24/outline";
|
|||||||
import { ICurrentUserResponse, IIssue } from "types";
|
import { ICurrentUserResponse, IIssue } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { PROJECT_MEMBERS } from "constants/fetch-keys";
|
import { PROJECT_MEMBERS } from "constants/fetch-keys";
|
||||||
|
import useProjectMembers from "hooks/use-project-members";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issue: IIssue;
|
issue: IIssue;
|
||||||
@ -37,15 +38,12 @@ export const ViewAssigneeSelect: React.FC<Props> = ({
|
|||||||
isNotAllowed,
|
isNotAllowed,
|
||||||
customButton = false,
|
customButton = false,
|
||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
const [fetchAssignees, setFetchAssignees] = useState(false);
|
||||||
const { workspaceSlug, projectId } = router.query;
|
|
||||||
|
|
||||||
const { data: members } = useSWR(
|
const router = useRouter();
|
||||||
projectId ? PROJECT_MEMBERS(projectId as string) : null,
|
const { workspaceSlug } = router.query;
|
||||||
workspaceSlug && projectId
|
|
||||||
? () => projectService.projectMembers(workspaceSlug as string, projectId as string)
|
const { members } = useProjectMembers(workspaceSlug?.toString(), issue.project, fetchAssignees);
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
const options = members?.map((member) => ({
|
const options = members?.map((member) => ({
|
||||||
value: member.member.id,
|
value: member.member.id,
|
||||||
@ -129,6 +127,7 @@ export const ViewAssigneeSelect: React.FC<Props> = ({
|
|||||||
noChevron
|
noChevron
|
||||||
position={position}
|
position={position}
|
||||||
disabled={isNotAllowed}
|
disabled={isNotAllowed}
|
||||||
|
onOpen={() => setFetchAssignees(true)}
|
||||||
selfPositioned={selfPositioned}
|
selfPositioned={selfPositioned}
|
||||||
width="w-full min-w-[12rem]"
|
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",
|
"invitations",
|
||||||
"magic-sign-in",
|
"magic-sign-in",
|
||||||
"onboarding",
|
"onboarding",
|
||||||
|
"profile",
|
||||||
"reset-password",
|
"reset-password",
|
||||||
"sign-up",
|
"sign-up",
|
||||||
"workspace-member-invitation",
|
"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_ISSUE = (workspaceSlug: string) => `USER_ISSUE_${workspaceSlug.toUpperCase()}`;
|
||||||
export const USER_ISSUES = (workspaceSlug: string, params: any) => {
|
export const USER_ISSUES = (workspaceSlug: string, params: any) => {
|
||||||
if (!params) return `USER_ISSUES_${workspaceSlug.toUpperCase()}`;
|
|
||||||
|
|
||||||
const paramsKey = myIssuesParamsToKey(params);
|
const paramsKey = myIssuesParamsToKey(params);
|
||||||
|
|
||||||
return `USER_ISSUES_${paramsKey}`;
|
return `USER_ISSUES_${workspaceSlug.toUpperCase()}_${paramsKey}`;
|
||||||
};
|
};
|
||||||
export const USER_ACTIVITY = "USER_ACTIVITY";
|
export const USER_ACTIVITY = "USER_ACTIVITY";
|
||||||
export const USER_WORKSPACE_DASHBOARD = (workspaceSlug: string) =>
|
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) =>
|
export const ISSUE_REACTION_LIST = (workspaceSlug: string, projectId: string, issueId: string) =>
|
||||||
`ISSUE_REACTION_LIST_${workspaceSlug.toUpperCase()}_${projectId.toUpperCase()}_${issueId.toUpperCase()}`;
|
`ISSUE_REACTION_LIST_${workspaceSlug.toUpperCase()}_${projectId.toUpperCase()}_${issueId.toUpperCase()}`;
|
||||||
|
|
||||||
export const COMMENT_REACTION_LIST = (
|
export const COMMENT_REACTION_LIST = (
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
export const STATE_GROUP_COLORS: {
|
export const STATE_GROUP_COLORS: {
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
} = {
|
} = {
|
||||||
backlog: "#ced4da",
|
backlog: "#d9d9d9",
|
||||||
unstarted: "#26b5ce",
|
unstarted: "#3f76ff",
|
||||||
started: "#f7ae59",
|
started: "#f59e0b",
|
||||||
cancelled: "#d687ff",
|
completed: "#16a34a",
|
||||||
completed: "#09a953",
|
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,
|
sub_issue: showSubIssues,
|
||||||
};
|
};
|
||||||
|
|
||||||
const { data: projectIssues } = useSWR(
|
const { data: projectIssues, mutate: mutateProjectIssues } = useSWR(
|
||||||
workspaceSlug && projectId && params
|
workspaceSlug && projectId && params
|
||||||
? PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string, params)
|
? PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string, params)
|
||||||
: null,
|
: null,
|
||||||
@ -72,7 +72,7 @@ const useIssuesView = () => {
|
|||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: projectArchivedIssues } = useSWR(
|
const { data: projectArchivedIssues, mutate: mutateProjectArchivedIssues } = useSWR(
|
||||||
workspaceSlug && projectId && params && isArchivedIssues && !archivedIssueId
|
workspaceSlug && projectId && params && isArchivedIssues && !archivedIssueId
|
||||||
? PROJECT_ARCHIVED_ISSUES_LIST_WITH_PARAMS(projectId as string, params)
|
? PROJECT_ARCHIVED_ISSUES_LIST_WITH_PARAMS(projectId as string, params)
|
||||||
: null,
|
: null,
|
||||||
@ -81,7 +81,7 @@ const useIssuesView = () => {
|
|||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: cycleIssues } = useSWR(
|
const { data: cycleIssues, mutate: mutateCycleIssues } = useSWR(
|
||||||
workspaceSlug && projectId && cycleId && params
|
workspaceSlug && projectId && cycleId && params
|
||||||
? CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params)
|
? CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params)
|
||||||
: null,
|
: null,
|
||||||
@ -96,7 +96,7 @@ const useIssuesView = () => {
|
|||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: moduleIssues } = useSWR(
|
const { data: moduleIssues, mutate: mutateModuleIssues } = useSWR(
|
||||||
workspaceSlug && projectId && moduleId && params
|
workspaceSlug && projectId && moduleId && params
|
||||||
? MODULE_ISSUES_WITH_PARAMS(moduleId as string, params)
|
? MODULE_ISSUES_WITH_PARAMS(moduleId as string, params)
|
||||||
: null,
|
: null,
|
||||||
@ -111,7 +111,7 @@ const useIssuesView = () => {
|
|||||||
: null
|
: 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 ? VIEW_ISSUES(viewId.toString(), params) : null,
|
||||||
workspaceSlug && projectId && viewId && params
|
workspaceSlug && projectId && viewId && params
|
||||||
? () =>
|
? () =>
|
||||||
@ -187,6 +187,15 @@ const useIssuesView = () => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
groupedByIssues,
|
groupedByIssues,
|
||||||
|
mutateIssues: cycleId
|
||||||
|
? mutateCycleIssues
|
||||||
|
: moduleId
|
||||||
|
? mutateModuleIssues
|
||||||
|
: viewId
|
||||||
|
? mutateViewIssues
|
||||||
|
: isArchivedIssues
|
||||||
|
? mutateProjectArchivedIssues
|
||||||
|
: mutateProjectIssues,
|
||||||
issueView: isArchivedIssues ? "list" : issueView,
|
issueView: isArchivedIssues ? "list" : issueView,
|
||||||
groupByProperty,
|
groupByProperty,
|
||||||
setGroupByProperty,
|
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
|
// hooks
|
||||||
import useUser from "./use-user";
|
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();
|
const { user } = useUser();
|
||||||
// fetching project members
|
// fetching project members
|
||||||
const { data: members } = useSWR(
|
const { data: members } = useSWR(
|
||||||
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId) : null,
|
workspaceSlug && projectId && fetchCondition ? PROJECT_MEMBERS(projectId) : null,
|
||||||
workspaceSlug && projectId
|
workspaceSlug && projectId && fetchCondition
|
||||||
? () => projectService.projectMembers(workspaceSlug, projectId)
|
? () => projectService.projectMembers(workspaceSlug, projectId)
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
@ -87,7 +87,7 @@ const MyIssuesPage: NextPage = () => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="h-full w-full flex flex-col overflow-hidden">
|
<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">
|
<div className="flex items-center overflow-x-scroll">
|
||||||
{tabsList.map((tab) => (
|
{tabsList.map((tab) => (
|
||||||
<button
|
<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,
|
IIssue,
|
||||||
IUser,
|
IUser,
|
||||||
IUserActivityResponse,
|
IUserActivityResponse,
|
||||||
|
IUserProfileData,
|
||||||
|
IUserProfileProjectSegregation,
|
||||||
IUserWorkspaceDashboard,
|
IUserWorkspaceDashboard,
|
||||||
} from "types";
|
} from "types";
|
||||||
|
|
||||||
@ -144,6 +146,55 @@ class UserService extends APIService {
|
|||||||
throw error?.response?.data;
|
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();
|
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 {
|
import type {
|
||||||
IState,
|
IState,
|
||||||
IUser,
|
IUser,
|
||||||
@ -292,6 +293,12 @@ export interface IIssueViewProps {
|
|||||||
groupByProperty: TIssueGroupByOptions;
|
groupByProperty: TIssueGroupByOptions;
|
||||||
isEmpty: boolean;
|
isEmpty: boolean;
|
||||||
issueView: TIssueViewOptions;
|
issueView: TIssueViewOptions;
|
||||||
|
mutateIssues: KeyedMutator<
|
||||||
|
| IIssue[]
|
||||||
|
| {
|
||||||
|
[key: string]: IIssue[];
|
||||||
|
}
|
||||||
|
>;
|
||||||
orderBy: TIssueOrderByOptions;
|
orderBy: TIssueOrderByOptions;
|
||||||
params: any;
|
params: any;
|
||||||
properties: Properties;
|
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 {
|
export interface IUser {
|
||||||
avatar: string;
|
avatar: string;
|
||||||
@ -51,7 +59,6 @@ export interface ICurrentUserResponse extends IUser {
|
|||||||
last_workspace_slug: string | null;
|
last_workspace_slug: string | null;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IUserLite {
|
export interface IUserLite {
|
||||||
avatar: string;
|
avatar: string;
|
||||||
created_at: Date;
|
created_at: Date;
|
||||||
@ -67,8 +74,13 @@ export interface IUserActivity {
|
|||||||
activity_count: number;
|
activity_count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IUserPriorityDistribution {
|
||||||
|
priority: string;
|
||||||
|
priority_count: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IUserStateDistribution {
|
export interface IUserStateDistribution {
|
||||||
state_group: string;
|
state_group: TStateGroups;
|
||||||
state_count: number;
|
state_count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,6 +119,7 @@ export interface IUserDetailedActivity {
|
|||||||
updated_by: string | null;
|
updated_by: string | null;
|
||||||
verb: string;
|
verb: string;
|
||||||
workspace: string;
|
workspace: string;
|
||||||
|
workspace_detail: IWorkspaceLite;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IUserActivityResponse {
|
export interface IUserActivityResponse {
|
||||||
@ -133,3 +146,36 @@ export type OnboardingSteps = {
|
|||||||
workspace_invite: boolean;
|
workspace_invite: boolean;
|
||||||
workspace_join: 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