feat: init
This commit is contained in:
177
apps/web/core/components/profile/activity/activity-list.tsx
Normal file
177
apps/web/core/components/profile/activity/activity-list.tsx
Normal file
@@ -0,0 +1,177 @@
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
import { History, MessageSquare } from "lucide-react";
|
||||
// plane imports
|
||||
import type { IUserActivityResponse } from "@plane/types";
|
||||
import { calculateTimeAgo, getFileURL } from "@plane/utils";
|
||||
// components
|
||||
import { ActivityIcon, ActivityMessage, IssueLink } from "@/components/core/activity";
|
||||
import { RichTextEditor } from "@/components/editor/rich-text";
|
||||
import { ActivitySettingsLoader } from "@/components/ui/loader/settings/activity";
|
||||
// hooks
|
||||
import { useWorkspace } from "@/hooks/store/use-workspace";
|
||||
import { useUser } from "@/hooks/store/user";
|
||||
|
||||
type Props = {
|
||||
activity: IUserActivityResponse | undefined;
|
||||
};
|
||||
|
||||
export const ActivityList: React.FC<Props> = observer((props) => {
|
||||
const { activity } = props;
|
||||
// params
|
||||
const { workspaceSlug } = useParams();
|
||||
// store hooks
|
||||
const { data: currentUser } = useUser();
|
||||
const { getWorkspaceBySlug } = useWorkspace();
|
||||
// derived values
|
||||
const workspaceId = getWorkspaceBySlug(workspaceSlug?.toString() ?? "")?.id ?? "";
|
||||
|
||||
// TODO: refactor this component
|
||||
return (
|
||||
<>
|
||||
{activity ? (
|
||||
<ul role="list">
|
||||
{activity.results.map((activityItem) => {
|
||||
if (activityItem.field === "comment")
|
||||
return (
|
||||
<div key={activityItem.id} className="mt-2">
|
||||
<div className="relative flex items-start space-x-3">
|
||||
<div className="relative px-1">
|
||||
{activityItem.field ? (
|
||||
activityItem.new_value === "restore" && <History className="h-3.5 w-3.5 text-custom-text-200" />
|
||||
) : activityItem.actor_detail.avatar_url && activityItem.actor_detail.avatar_url !== "" ? (
|
||||
<img
|
||||
src={getFileURL(activityItem.actor_detail.avatar_url)}
|
||||
alt={activityItem.actor_detail.display_name}
|
||||
height={30}
|
||||
width={30}
|
||||
className="grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 text-white"
|
||||
/>
|
||||
) : (
|
||||
<div className="grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 capitalize text-white">
|
||||
{activityItem.actor_detail.display_name?.[0]}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<span className="ring-6 flex h-6 w-6 items-center justify-center rounded-full bg-custom-background-80 text-custom-text-200 ring-white">
|
||||
<MessageSquare className="h-6 w-6 !text-2xl text-custom-text-200" aria-hidden="true" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div>
|
||||
<div className="text-xs">
|
||||
{activityItem.actor_detail.is_bot
|
||||
? activityItem.actor_detail.first_name + " Bot"
|
||||
: activityItem.actor_detail.display_name}
|
||||
</div>
|
||||
<p className="mt-0.5 text-xs text-custom-text-200">
|
||||
Commented {calculateTimeAgo(activityItem.created_at)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="issue-comments-section p-0">
|
||||
<RichTextEditor
|
||||
editable={false}
|
||||
id={activityItem.id}
|
||||
initialValue={
|
||||
activityItem?.new_value !== ""
|
||||
? (activityItem.new_value?.toString() as string)
|
||||
: (activityItem.old_value?.toString() as string)
|
||||
}
|
||||
containerClassName="text-xs bg-custom-background-100"
|
||||
workspaceId={workspaceId}
|
||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||
projectId={activityItem.project}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const message =
|
||||
activityItem.verb === "created" &&
|
||||
!["cycles", "modules", "attachment", "link", "estimate"].includes(
|
||||
activityItem.field?.toString() as string
|
||||
) &&
|
||||
!activityItem.field ? (
|
||||
<span>
|
||||
created <IssueLink activity={activityItem} />
|
||||
</span>
|
||||
) : (
|
||||
<ActivityMessage activity={activityItem} showIssue />
|
||||
);
|
||||
|
||||
if ("field" in activityItem && activityItem.field !== "updated_by")
|
||||
return (
|
||||
<li key={activityItem.id}>
|
||||
<div className="relative pb-1">
|
||||
<div className="relative flex items-start space-x-2">
|
||||
<>
|
||||
<div>
|
||||
<div className="relative px-1.5 mt-4">
|
||||
<div className="mt-1.5">
|
||||
<div className="flex h-6 w-6 items-center justify-center">
|
||||
{activityItem.field ? (
|
||||
activityItem.new_value === "restore" ? (
|
||||
<History className="h-5 w-5 text-custom-text-200" />
|
||||
) : (
|
||||
<ActivityIcon activity={activityItem} />
|
||||
)
|
||||
) : activityItem.actor_detail.avatar_url &&
|
||||
activityItem.actor_detail.avatar_url !== "" ? (
|
||||
<img
|
||||
src={getFileURL(activityItem.actor_detail.avatar_url)}
|
||||
alt={activityItem.actor_detail.display_name}
|
||||
height={24}
|
||||
width={24}
|
||||
className="h-full w-full rounded-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="grid h-6 w-6 place-items-center rounded-full border-2 border-white bg-gray-700 text-xs capitalize text-white">
|
||||
{activityItem.actor_detail.display_name?.[0]}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 border-b border-custom-border-100 py-4">
|
||||
<div className="break-words text-sm text-custom-text-200">
|
||||
{activityItem.field === "archived_at" && activityItem.new_value !== "restore" ? (
|
||||
<span className="text-gray font-medium">Plane</span>
|
||||
) : activityItem.actor_detail.is_bot ? (
|
||||
<span className="text-gray font-medium">{activityItem.actor_detail.first_name} Bot</span>
|
||||
) : (
|
||||
<Link
|
||||
href={`/${activityItem.workspace_detail?.slug}/profile/${activityItem.actor_detail.id}`}
|
||||
className="inline"
|
||||
>
|
||||
<span className="text-gray font-medium">
|
||||
{currentUser?.id === activityItem.actor_detail.id
|
||||
? "You"
|
||||
: activityItem.actor_detail.display_name}
|
||||
</span>
|
||||
</Link>
|
||||
)}{" "}
|
||||
<div className="inline gap-1">
|
||||
{message}{" "}
|
||||
<span className="flex-shrink-0 whitespace-nowrap">
|
||||
{calculateTimeAgo(activityItem.created_at)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
) : (
|
||||
<ActivitySettingsLoader />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
// services
|
||||
// ui
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Button } from "@plane/propel/button";
|
||||
// helpers
|
||||
import { renderFormattedPayloadDate } from "@plane/utils";
|
||||
import { UserService } from "@/services/user.service";
|
||||
|
||||
const userService = new UserService();
|
||||
|
||||
export const DownloadActivityButton = () => {
|
||||
// states
|
||||
const [isDownloading, setIsDownloading] = useState(false);
|
||||
// router
|
||||
const { workspaceSlug, userId } = useParams();
|
||||
//hooks
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleDownload = async () => {
|
||||
const today = renderFormattedPayloadDate(new Date());
|
||||
|
||||
if (!workspaceSlug || !userId || !today) return;
|
||||
|
||||
setIsDownloading(true);
|
||||
|
||||
const csv = await userService
|
||||
.downloadProfileActivity(workspaceSlug.toString(), userId.toString(), {
|
||||
date: today,
|
||||
})
|
||||
.finally(() => setIsDownloading(false));
|
||||
|
||||
// create a Blob object
|
||||
const blob = new Blob([csv], { type: "text/csv" });
|
||||
|
||||
// create URL for the Blob object
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
|
||||
// create a link element
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `profile-activity-${Date.now()}.csv`;
|
||||
document.body.appendChild(a);
|
||||
|
||||
// simulate click on the link element to trigger download
|
||||
a.click();
|
||||
|
||||
// cleanup
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
};
|
||||
|
||||
return (
|
||||
<Button onClick={handleDownload} loading={isDownloading}>
|
||||
{isDownloading ? t("profile.stats.recent_activity.button_loading") : t("profile.stats.recent_activity.button")}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,188 @@
|
||||
import { useEffect } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import useSWR from "swr";
|
||||
// icons
|
||||
import { History, MessageSquare } from "lucide-react";
|
||||
import { calculateTimeAgo, getFileURL } from "@plane/utils";
|
||||
// hooks
|
||||
import { ActivityIcon, ActivityMessage } from "@/components/core/activity";
|
||||
import { RichTextEditor } from "@/components/editor/rich-text";
|
||||
import { ActivitySettingsLoader } from "@/components/ui/loader/settings/activity";
|
||||
// constants
|
||||
import { USER_ACTIVITY } from "@/constants/fetch-keys";
|
||||
// hooks
|
||||
import { useUser } from "@/hooks/store/user";
|
||||
// services
|
||||
import { UserService } from "@/services/user.service";
|
||||
const userService = new UserService();
|
||||
|
||||
type Props = {
|
||||
cursor: string;
|
||||
perPage: number;
|
||||
updateResultsCount: (count: number) => void;
|
||||
updateTotalPages: (count: number) => void;
|
||||
updateEmptyState: (state: boolean) => void;
|
||||
};
|
||||
|
||||
export const ProfileActivityListPage: React.FC<Props> = observer((props) => {
|
||||
const { cursor, perPage, updateResultsCount, updateTotalPages, updateEmptyState } = props;
|
||||
// store hooks
|
||||
const { data: currentUser } = useUser();
|
||||
|
||||
const { data: userProfileActivity } = useSWR(
|
||||
USER_ACTIVITY({
|
||||
cursor,
|
||||
}),
|
||||
() =>
|
||||
userService.getUserActivity({
|
||||
cursor,
|
||||
per_page: perPage,
|
||||
})
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!userProfileActivity) return;
|
||||
|
||||
// if no results found then show empty state
|
||||
if (userProfileActivity.total_results === 0) updateEmptyState(true);
|
||||
|
||||
updateTotalPages(userProfileActivity.total_pages);
|
||||
updateResultsCount(userProfileActivity.results.length);
|
||||
}, [updateResultsCount, updateTotalPages, userProfileActivity, updateEmptyState]);
|
||||
|
||||
// TODO: refactor this component
|
||||
return (
|
||||
<>
|
||||
{userProfileActivity ? (
|
||||
<ul role="list">
|
||||
{userProfileActivity.results.map((activityItem: any) => {
|
||||
if (activityItem.field === "comment")
|
||||
return (
|
||||
<div key={activityItem.id} className="mt-2">
|
||||
<div className="relative flex items-start space-x-3">
|
||||
<div className="relative px-1">
|
||||
{activityItem.field ? (
|
||||
activityItem.new_value === "restore" && <History className="h-3.5 w-3.5 text-custom-text-200" />
|
||||
) : activityItem.actor_detail.avatar_url && activityItem.actor_detail.avatar_url !== "" ? (
|
||||
<img
|
||||
src={getFileURL(activityItem.actor_detail.avatar_url)}
|
||||
alt={activityItem.actor_detail.display_name}
|
||||
height={30}
|
||||
width={30}
|
||||
className="grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 text-white"
|
||||
/>
|
||||
) : (
|
||||
<div className="grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 capitalize text-white">
|
||||
{activityItem.actor_detail.display_name?.[0]}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<span className="ring-6 flex h-6 w-6 p-2 items-center justify-center rounded-full bg-custom-background-80 text-custom-text-200 ring-white">
|
||||
<MessageSquare className="!text-2xl text-custom-text-200" aria-hidden="true" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div>
|
||||
<div className="text-xs">
|
||||
{activityItem.actor_detail.is_bot
|
||||
? activityItem.actor_detail.first_name + " Bot"
|
||||
: activityItem.actor_detail.display_name}
|
||||
</div>
|
||||
<p className="mt-0.5 text-xs text-custom-text-200">
|
||||
Commented {calculateTimeAgo(activityItem.created_at)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="issue-comments-section p-0">
|
||||
<RichTextEditor
|
||||
editable={false}
|
||||
id={activityItem.id}
|
||||
initialValue={
|
||||
activityItem?.new_value !== "" ? activityItem.new_value : activityItem.old_value
|
||||
}
|
||||
containerClassName="text-xs bg-custom-background-100"
|
||||
workspaceId={activityItem?.workspace_detail?.id?.toString() ?? ""}
|
||||
workspaceSlug={activityItem?.workspace_detail?.slug?.toString() ?? ""}
|
||||
projectId={activityItem.project ?? ""}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const message = <ActivityMessage activity={activityItem} showIssue />;
|
||||
|
||||
if ("field" in activityItem && activityItem.field !== "updated_by")
|
||||
return (
|
||||
<li key={activityItem.id}>
|
||||
<div className="relative pb-1">
|
||||
<div className="relative flex items-start space-x-2">
|
||||
<>
|
||||
<div>
|
||||
<div className="relative px-1.5 mt-4">
|
||||
<div className="mt-1.5">
|
||||
<div className="flex h-6 w-6 items-center justify-center">
|
||||
{activityItem.field ? (
|
||||
activityItem.new_value === "restore" ? (
|
||||
<History className="h-5 w-5 text-custom-text-200" />
|
||||
) : (
|
||||
<ActivityIcon activity={activityItem} />
|
||||
)
|
||||
) : activityItem.actor_detail.avatar_url &&
|
||||
activityItem.actor_detail.avatar_url !== "" ? (
|
||||
<img
|
||||
src={getFileURL(activityItem.actor_detail.avatar_url)}
|
||||
alt={activityItem.actor_detail.display_name}
|
||||
height={24}
|
||||
width={24}
|
||||
className="h-full w-full rounded-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="grid h-6 w-6 place-items-center rounded-full border-2 border-white bg-gray-700 text-xs capitalize text-white">
|
||||
{activityItem.actor_detail.display_name?.[0]}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 border-b border-custom-border-100 py-4">
|
||||
<div className="break-words text-sm text-custom-text-200">
|
||||
{activityItem.field === "archived_at" && activityItem.new_value !== "restore" ? (
|
||||
<span className="text-gray font-medium">Plane</span>
|
||||
) : activityItem.actor_detail.is_bot ? (
|
||||
<span className="text-gray font-medium">{activityItem.actor_detail.first_name} Bot</span>
|
||||
) : (
|
||||
<Link
|
||||
href={`/${activityItem.workspace_detail.slug}/profile/${activityItem.actor_detail.id}`}
|
||||
className="inline"
|
||||
>
|
||||
<span className="text-gray font-medium">
|
||||
{currentUser?.id === activityItem.actor_detail.id
|
||||
? "You"
|
||||
: activityItem.actor_detail.display_name}
|
||||
</span>
|
||||
</Link>
|
||||
)}{" "}
|
||||
<div className="inline gap-1">
|
||||
{message}{" "}
|
||||
<span className="flex-shrink-0 whitespace-nowrap">
|
||||
{calculateTimeAgo(activityItem.created_at)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
) : (
|
||||
<ActivitySettingsLoader />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
import { useEffect } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
// services
|
||||
import { USER_PROFILE_ACTIVITY } from "@/constants/fetch-keys";
|
||||
import { UserService } from "@/services/user.service";
|
||||
// components
|
||||
import { ActivityList } from "./activity-list";
|
||||
// fetch-keys
|
||||
|
||||
// services
|
||||
const userService = new UserService();
|
||||
|
||||
type Props = {
|
||||
cursor: string;
|
||||
perPage: number;
|
||||
updateResultsCount: (count: number) => void;
|
||||
updateTotalPages: (count: number) => void;
|
||||
};
|
||||
|
||||
export const WorkspaceActivityListPage: React.FC<Props> = (props) => {
|
||||
const { cursor, perPage, updateResultsCount, updateTotalPages } = props;
|
||||
// router
|
||||
const { workspaceSlug, userId } = useParams();
|
||||
|
||||
const { data: userProfileActivity } = useSWR(
|
||||
workspaceSlug && userId
|
||||
? USER_PROFILE_ACTIVITY(workspaceSlug.toString(), userId.toString(), {
|
||||
cursor,
|
||||
})
|
||||
: null,
|
||||
workspaceSlug && userId
|
||||
? () =>
|
||||
userService.getUserProfileActivity(workspaceSlug.toString(), userId.toString(), {
|
||||
cursor,
|
||||
per_page: perPage,
|
||||
})
|
||||
: null
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!userProfileActivity) return;
|
||||
|
||||
updateTotalPages(userProfileActivity.total_pages);
|
||||
updateResultsCount(userProfileActivity.results.length);
|
||||
}, [updateResultsCount, updateTotalPages, userProfileActivity]);
|
||||
|
||||
return <ActivityList activity={userProfileActivity} />;
|
||||
};
|
||||
Reference in New Issue
Block a user