fix: project overview improvements (#1930)

* fix: activity new fields added

* fix: borders

* fix: minor bug fixes

* chore: added the is_project_updates_enabled key

* chore: passed the proejct id

* fix: bugs

* fix: padding issues

* fix: typo

* fix: project service code for toggle apis

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
This commit is contained in:
Akshita Goyal
2024-12-16 19:37:59 +05:30
committed by GitHub
parent 02c8f60eea
commit beaef62781
63 changed files with 987 additions and 898 deletions

View File

@@ -112,6 +112,7 @@ class ProjectListSerializer(DynamicBaseSerializer):
start_date = serializers.DateTimeField(read_only=True)
target_date = serializers.DateTimeField(read_only=True)
is_epic_enabled = serializers.BooleanField(read_only=True)
is_project_updates_enabled = serializers.BooleanField(read_only=True)
# EE: project_grouping ends
inbox_view = serializers.BooleanField(read_only=True, source="intake_view")

View File

@@ -173,7 +173,15 @@ class ProjectViewSet(BaseViewSet):
).values("is_epic_enabled")
)
)
# EE: project_grouping ends
.annotate(
is_project_updates_enabled=Exists(
ProjectFeature.objects.filter(
workspace__slug=self.kwargs.get("slug"),
project_id=OuterRef("pk"),
).values("is_project_updates_enabled")
)
)
# EE: project_grouping ends
.prefetch_related(
Prefetch(
"project_projectmember",

View File

@@ -90,7 +90,7 @@ urlpatterns = [
name="project-reactions",
),
path(
"workspaces/<str:slug>/projects/<uuid:pk>/features/",
"workspaces/<str:slug>/projects/<uuid:project_id>/features/",
ProjectFeatureEndpoint.as_view(),
name="project-features",
),

View File

@@ -54,22 +54,25 @@ class ProjectAnalyticsEndpoint(BaseAPIView):
class ProjectFeatureEndpoint(BaseAPIView):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
def get(self, request, slug, pk):
@allow_permission(
[
ROLE.ADMIN,
ROLE.MEMBER,
ROLE.GUEST,
],
)
def get(self, request, slug, project_id):
project_feature, _ = ProjectFeature.objects.get_or_create(
project_id=pk,
workspace__slug=slug,
)
project_id=project_id)
serializer = ProjectFeatureSerializer(project_feature)
return Response(serializer.data, status=status.HTTP_200_OK)
@allow_permission([ROLE.ADMIN], level="WORKSPACE")
def patch(self, request, slug, pk):
@allow_permission([ROLE.ADMIN])
def patch(self, request, slug, project_id):
project_feature = ProjectFeature.objects.get(
project_id=pk, workspace__slug=slug
project_id=project_id, workspace__slug=slug
)
current_instance = json.dumps(
ProjectFeatureSerializer(project_feature).data, cls=DjangoJSONEncoder
)

View File

@@ -0,0 +1,98 @@
"use client";
import React from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// component
import { setPromiseToast, UpdatesIcon, ToggleSwitch } from "@plane/ui";
import { NotAuthorizedView } from "@/components/auth-screens";
// store hooks
import { useUserPermissions } from "@/hooks/store";
// plane web components
import { WithFeatureFlagHOC } from "@/plane-web/components/feature-flags";
// plane web constants
import { ProjectUpdatesUpgrade } from "@/plane-web/components/project-overview/upgrade";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
import { useProjectAdvanced } from "@/plane-web/hooks/store/projects/use-projects";
const UpdatesSettingsPage = observer(() => {
// router
const { workspaceSlug, projectId } = useParams();
// store hooks
const { allowPermissions } = useUserPermissions();
const { toggleFeatures, features } = useProjectAdvanced();
// derived values
const currentProjectDetails = features[projectId?.toString()];
const canPerformProjectAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT);
if (!canPerformProjectAdminActions) {
return <NotAuthorizedView section="settings" isProjectView />;
}
if (!canPerformProjectAdminActions)
return (
<>
<div className="mt-10 flex h-full w-full justify-center p-4">
<p className="text-sm text-custom-text-300">You are not authorized to access this page.</p>
</div>
</>
);
const toggleUpdatesFeature = async () => {
if (!currentProjectDetails) return;
// making the request to update the project feature
const settingsPayload = {
is_project_updates_enabled: !currentProjectDetails?.["is_project_updates_enabled"],
};
const updateProjectPromise = toggleFeatures(workspaceSlug.toString(), projectId.toString(), settingsPayload);
setPromiseToast(updateProjectPromise, {
loading: "Updating project feature...",
success: {
title: "Success!",
message: () => "Project feature updated successfully.",
},
error: {
title: "Error!",
message: () => "Something went wrong while updating project feature. Please try again.",
},
});
};
return (
<>
<div className="border-b border-custom-border-200 pb-3 tracking-tight">
<h3 className="text-xl font-medium">Projects Updates</h3>
<span className="text-custom-sidebar-text-400 text-sm font-medium">Toggle this on or off this project. </span>
</div>
<WithFeatureFlagHOC
flag="PROJECT_UPDATES"
fallback={<ProjectUpdatesUpgrade />}
workspaceSlug={workspaceSlug?.toString()}
>
<div className="px-4 py-6 flex items-center justify-between gap-2 border-b border-custom-border-100">
<div className="flex items-center gap-4">
<div className="size-10 bg-custom-background-90 rounded-md flex items-center justify-center">
<UpdatesIcon className="size-5 text-custom-text-300" />
</div>
<div className="leading-tight">
<h5 className="font-medium">Turn on Project Updates</h5>
<span className="text-custom-sidebar-text-400 text-sm">
See all updates on demand from anyone in this project. Easily track updates across four preset
categories.
</span>
</div>
</div>
{currentProjectDetails && (
<ToggleSwitch
value={currentProjectDetails?.["is_project_updates_enabled"]}
onChange={toggleUpdatesFeature}
size="sm"
/>
)}
</div>
</WithFeatureFlagHOC>
</>
);
});
export default UpdatesSettingsPage;

View File

@@ -31,6 +31,8 @@ import { useTimeLineChart } from "@/hooks/use-timeline-chart";
import { persistence } from "@/local-db/storage.sqlite";
// plane web constants
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
// plane web hooks
import { useProjectAdvanced } from "@/plane-web/hooks/store/projects/use-projects";
interface IProjectAuthWrapper {
children: ReactNode;
@@ -46,6 +48,7 @@ export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
const { setTrackElement } = useEventTracker();
const { fetchUserProjectInfo, allowPermissions, projectUserInfo } = useUserPermissions();
const { loader, getProjectById, fetchProjectDetails } = useProject();
const { fetchFeatures } = useProjectAdvanced();
const { fetchAllCycles } = useCycle();
const { fetchModulesSlim, fetchModules } = useModule();
const { initGantt } = useTimeLineChart(ETimeLineTypeType.MODULE);
@@ -87,6 +90,16 @@ export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
}
);
// features
useSWR(
workspaceSlug && projectId ? `PROJECT_ADVANCED_FEATURES_${workspaceSlug}_${projectId}` : null,
workspaceSlug && projectId
? () => {
fetchFeatures(workspaceSlug.toString(), projectId.toString());
}
: null
);
// fetching project details
useSWR(
workspaceSlug && projectId ? `PROJECT_DETAILS_${workspaceSlug.toString()}_${projectId.toString()}` : null,

View File

@@ -6,20 +6,18 @@ import { Network } from "lucide-react";
import { Tooltip } from "@plane/ui";
import { renderFormattedTime, renderFormattedDate, calculateTimeAgo } from "@/helpers/date-time.helper";
import { usePlatformOS } from "@/hooks/use-platform-os";
// ui
// components
import { ProjectUser } from "..";
// helpers
import { TProjectActivity } from "@/plane-web/types";
import { User } from "./user";
type TProjectActivityBlockComponent = {
type TActivityBlockComponent = {
icon?: ReactNode;
activity: any;
activity: TProjectActivity;
ends: "top" | "bottom" | undefined;
children: ReactNode;
customUserName?: string;
};
export const ProjectActivityBlockComponent: FC<TProjectActivityBlockComponent> = (props) => {
export const ActivityBlockComponent: FC<TActivityBlockComponent> = (props) => {
const { icon, activity, ends, children, customUserName } = props;
// hooks
const { isMobile } = usePlatformOS();
@@ -36,15 +34,17 @@ export const ProjectActivityBlockComponent: FC<TProjectActivityBlockComponent> =
{icon ? icon : <Network className="w-3.5 h-3.5" />}
</div>
<div className="w-full truncate text-custom-text-200">
<ProjectUser activity={activity} customUserName={customUserName} /> {children}
<span>
<User activity={activity} customUserName={customUserName} /> {children}
<div className="mt-1">
<Tooltip
isMobile={isMobile}
tooltipContent={`${renderFormattedDate(activity.created_at)}, ${renderFormattedTime(activity.created_at)}`}
>
<span className="whitespace-nowrap"> {calculateTimeAgo(activity.created_at)}</span>
<span className="whitespace-nowrap text-custom-text-350 font-medium">
{calculateTimeAgo(activity.created_at)}
</span>
</Tooltip>
</span>
</div>
</div>
</div>
);

View File

@@ -0,0 +1,35 @@
"use client";
import { FC, ReactNode } from "react";
import { observer } from "mobx-react";
import { TProjectActivity } from "@/plane-web/types";
import { ActivityBlockComponent } from "./activity-block";
type TActivityItem = {
activity: TProjectActivity;
showProject?: boolean;
ends?: "top" | "bottom" | undefined;
iconsMap: Record<string, ReactNode>;
messages: (activity: TProjectActivity) => { message: ReactNode; customUserName?: string };
};
export const ActivityItem: FC<TActivityItem> = observer((props) => {
const { activity, showProject = true, ends, messages, iconsMap } = props;
if (!activity) return null;
const activityType = activity.field;
const { message, customUserName } = messages(activity);
const icon = iconsMap[activityType] || iconsMap.default;
return (
<ActivityBlockComponent icon={icon} activity={activity} ends={ends} customUserName={customUserName}>
<>
{message}
{showProject && " for project "}
<span className="font-normal">{activity.project_detail?.name}</span>
</>
</ActivityBlockComponent>
);
});

View File

@@ -1,12 +1,13 @@
import { FC } from "react";
import Link from "next/link";
import { TProjectActivity } from "@/plane-web/types";
type TProjectUser = {
activity: any;
type TUser = {
activity: TProjectActivity;
customUserName?: string;
};
export const ProjectUser: FC<TProjectUser> = (props) => {
export const User: FC<TUser> = (props) => {
const { activity, customUserName } = props;
return (

View File

@@ -89,7 +89,7 @@ export const AdvancedIssuesHeader = observer(() => {
) : null}
<Link
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/overview`}
className="border border-custom-border-80 rounded px-2.5 py-1.5 flex gap-1 text-custom-text-300 hover:text-custom-text-300"
className="border border-custom-border-100 rounded px-2.5 py-1.5 flex gap-1 text-custom-text-300 hover:text-custom-text-300"
>
<OverviewIcon className="flex h-[14px] w-[14px] my-auto" />
<span className="hidden text-xs md:block font-medium">Overview</span>

View File

@@ -58,7 +58,7 @@ export const ProjectAttachmentsDetail: FC<TProjectAttachmentsDetail> = observer(
attachmentId={attachmentId}
/>
<div className="w-full flex items-center justify-between gap-1 rounded-md border border-custom-border-80 bg-custom-background-100 px-4 py-2 text-sm">
<div className="w-full flex items-center justify-between gap-1 rounded-md border border-custom-border-100 bg-custom-background-100 px-4 py-2 text-sm">
<Link href={fileURL ?? ""} target="_blank" rel="noopener noreferrer">
<div className="flex items-center gap-3">
<div className="h-7 w-7">{fileIcon}</div>

View File

@@ -2,10 +2,7 @@
import { FC } from "react";
import { observer } from "mobx-react";
// hooks
// components
import useSWR from "swr";
import { useProjectAttachments } from "@/plane-web/hooks/store/projects/use-project-attachments";
import { ProjectAttachmentsList } from "./attachments-list";
import { useAttachmentOperations } from "./use-attachments";
@@ -20,17 +17,7 @@ export const ProjectAttachmentRoot: FC<TProjectAttachmentRoot> = observer((props
const { workspaceSlug, projectId, disabled = false } = props;
// hooks
const attachmentHelpers = useAttachmentOperations(workspaceSlug, projectId);
const { fetchAttachments } = useProjectAttachments();
// api calls
useSWR(
projectId && workspaceSlug ? `PROJECT_ATTACHMENTS_${projectId}` : null,
projectId && workspaceSlug ? () => fetchAttachments(workspaceSlug, projectId) : null,
{
revalidateIfStale: false,
revalidateOnFocus: false,
revalidateOnReconnect: false,
}
);
return (
<div className="relative py-3 space-y-3">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">

View File

@@ -9,7 +9,7 @@ type TProps = {
export const Actions = (props: TProps) => {
const { toggleLinkModalOpen, workspaceSlug, projectId } = props;
return (
<div className="text-base font-medium flex gap-4 text-custom-text-350 my-auto">
<div className="text-base font-medium flex gap-4 text-custom-text-200 my-auto">
<button className="flex gap-1" onClick={() => toggleLinkModalOpen(true)}>
<Link2 className="rotate-[135deg] my-auto" size={16} />
<div>Add link</div>

View File

@@ -20,7 +20,7 @@ export const DescriptionBox = (props: TProps) => {
return (
<div>
<div className="text-2xl font-semibold text-custom-text-300 w-full rounded-md pb-2">
<div className="text-2xl w-full rounded-md pb-2">
<ProjectDescriptionInput
workspaceSlug={workspaceSlug}
project={project}

View File

@@ -1,12 +1,87 @@
import { Logo } from "@plane/ui";
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { IProject, IWorkspace } from "@plane/types";
import { EUserPermissions } from "@plane/types/src/enums";
import { CustomEmojiIconPicker, EmojiIconPickerTypes, Logo, setToast, TOAST_TYPE } from "@plane/ui";
import { EUserPermissionsLevel } from "@/ce/constants";
import { PROJECT_UPDATED } from "@/constants/event-tracker";
import { convertHexEmojiToDecimal } from "@/helpers/emoji.helper";
import { getFileURL } from "@/helpers/file.helper";
import { useEventTracker, useProject, useUserPermissions } from "@/hooks/store";
import { TProject } from "@/plane-web/types";
type THeroSection = {
project: TProject;
workspaceSlug: string;
};
export const HeroSection = (props: THeroSection) => {
const { project } = props;
const { project, workspaceSlug } = props;
const [isOpen, setIsOpen] = useState(false);
const { allowPermissions } = useUserPermissions();
const { captureProjectEvent } = useEventTracker();
const { updateProject } = useProject();
// form info
const {
handleSubmit,
control,
formState: { dirtyFields },
getValues,
} = useForm<IProject>({
defaultValues: {
...project,
workspace: (project.workspace as IWorkspace).id,
},
});
// derived values
const isAdmin = allowPermissions(
[EUserPermissions.ADMIN],
EUserPermissionsLevel.PROJECT,
workspaceSlug.toString(),
project.id.toString()
);
const handleUpdateChange = async (payload: Partial<IProject>) => {
if (!workspaceSlug || !project) return;
return updateProject(workspaceSlug.toString(), project.id, payload)
.then((res) => {
const changed_properties = Object.keys(dirtyFields);
captureProjectEvent({
eventName: PROJECT_UPDATED,
payload: {
...res,
changed_properties: changed_properties,
state: "SUCCESS",
element: "Project general settings",
},
});
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Project updated successfully",
});
})
.catch((error) => {
captureProjectEvent({
eventName: PROJECT_UPDATED,
payload: { ...payload, state: "FAILED", element: "Project general settings" },
});
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: error?.error ?? "Project could not be updated. Please try again.",
});
});
};
const onSubmit = async () => {
if (!workspaceSlug) return;
const payload: Partial<IProject> = {
logo_props: getValues<"logo_props">("logo_props"),
};
handleUpdateChange(payload);
};
return (
<div>
@@ -21,9 +96,47 @@ export const HeroSection = (props: THeroSection) => {
/>
</div>
<div className="relative px-page-x pt-page-y mt-2">
<div className="absolute -top-[27px] h-10 w-10 flex-shrink-0 grid place-items-center rounded bg-custom-background-80">
<Logo logo={project.logo_props} size={18} />
</div>
<form
onSubmit={handleSubmit(onSubmit)}
className="absolute -top-[27px] h-10 w-10 flex-shrink-0 grid place-items-center rounded bg-custom-background-80"
>
<Controller
control={control}
name="logo_props"
render={({ field: { value, onChange } }) => (
<CustomEmojiIconPicker
closeOnSelect={false}
isOpen={isOpen}
handleToggle={(val: boolean) => setIsOpen(val)}
className="flex items-center justify-center"
buttonClassName="flex flex-shrink-0 items-center justify-center rounded-lg bg-white/10"
label={<Logo logo={value} size={28} />}
onChange={(val) => {
let logoValue = {};
if (val?.type === "emoji")
logoValue = {
value: convertHexEmojiToDecimal(val.value.unified),
url: val.value.imageUrl,
};
else if (val?.type === "icon") logoValue = val.value;
onChange({
in_use: val?.type,
[val?.type]: logoValue,
});
onSubmit();
setIsOpen(false);
}}
defaultIconColor={value?.in_use && value.in_use === "icon" ? value?.icon?.color : undefined}
defaultOpen={
value.in_use && value.in_use === "emoji" ? EmojiIconPickerTypes.EMOJI : EmojiIconPickerTypes.ICON
}
disabled={!isAdmin}
/>
)}
/>
</form>
<div className="font-bold text-xl">{project.name}</div>
</div>
</div>

View File

@@ -2,7 +2,6 @@
import { FC } from "react";
import { observer } from "mobx-react";
import useSWR from "swr";
// components
import { ProjectLinkList } from "./links";
import { useLinks } from "./use-links";
@@ -20,16 +19,7 @@ export const ProjectLinkRoot: FC<TProjectLinkRoot> = observer((props) => {
// props
const { workspaceSlug, projectId, disabled = false } = props;
// hooks
const { handleLinkOperations, fetchLinks } = useLinks(workspaceSlug, projectId);
// api calls
useSWR(
projectId && workspaceSlug ? `PROJECT_LINKS_${projectId}` : null,
projectId && workspaceSlug ? () => fetchLinks(workspaceSlug, projectId) : null,
{
revalidateIfStale: false,
revalidateOnFocus: false,
revalidateOnReconnect: false,
}
);
const { handleLinkOperations } = useLinks(workspaceSlug, projectId);
return <ProjectLinkList projectId={projectId} linkOperations={handleLinkOperations} disabled={disabled} />;
});

View File

@@ -78,10 +78,11 @@ export const StatsToday = (props: TStatsTodayProps) => {
<Loader.Item height="125px" width="100%" />
</Loader>
) : (
<>
<div className="mt-4 mx-2 border-y border-custom-border-200 py-4">
<div className="text-base text-custom-text-100 font-medium">Metrics as of today</div>
{(!analytics || project.total_issues === 0) && <NoStats workspaceSlug={workspaceSlug} projectId={project?.id} />}
{analytics && project.total_issues > 0 && (
<div className="text-custom-text-300 mt-4 mx-2">
<div className="text-custom-text-300 mt-4">
{/* Progress bar */}
<div className="flex w-full h-[14px] rounded gap-[2px]">
{stats.map((stat) => (
@@ -89,9 +90,9 @@ export const StatsToday = (props: TStatsTodayProps) => {
key={stat.title}
className={cn(`h-full rounded`)}
style={{
width: `${stat.stat ? Math.round(stat.stat / project.total_issues) * 100 : 0}%`,
width: `${stat.stat ? Math.round((stat.stat * 100) / project.total_issues) : 0}%`,
backgroundColor: `${stat.color}`,
opacity: 0.4,
opacity: 0.5,
}}
/>
))}
@@ -138,6 +139,6 @@ export const StatsToday = (props: TStatsTodayProps) => {
</div>
</div>
)}
</>
</div>
);
};

View File

@@ -1,7 +1,10 @@
"use client";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import useSWR from "swr";
import { useProject } from "@/hooks/store";
import { useProjectLinks } from "@/plane-web/hooks/store";
import { useProjectAttachments } from "@/plane-web/hooks/store/projects/use-project-attachments";
import { TProject } from "@/plane-web/types";
import { WithFeatureFlagHOC } from "../feature-flags";
import { ProjectAttachmentRoot } from "./details/attachment";
@@ -19,12 +22,33 @@ import { ProjectDetailsSidebar } from "./sidebar";
export const ProjectOverviewRoot = observer(() => {
const { projectId, workspaceSlug } = useParams();
const { getProjectById, updateProject } = useProject();
const { handleOnClose, handleLinkOperations, isLinkModalOpen, toggleLinkModal, linkData, setLinkData } = useLinks(
workspaceSlug.toString(),
projectId.toString()
);
const { handleOnClose, handleLinkOperations, isLinkModalOpen, toggleLinkModal, linkData, setLinkData, fetchLinks } =
useLinks(workspaceSlug.toString(), projectId.toString());
const { getLinksByProjectId } = useProjectLinks();
const { fetchAttachments, getAttachmentsByProjectId } = useProjectAttachments();
// api calls
useSWR(
projectId && workspaceSlug ? `PROJECT_LINKS_${projectId}` : null,
projectId && workspaceSlug ? () => fetchLinks(workspaceSlug.toString(), projectId.toString()) : null,
{
revalidateIfStale: false,
revalidateOnFocus: false,
revalidateOnReconnect: false,
}
);
useSWR(
projectId && workspaceSlug ? `PROJECT_ATTACHMENTS_${projectId}` : null,
projectId && workspaceSlug ? () => fetchAttachments(workspaceSlug.toString(), projectId.toString()) : null,
{
revalidateIfStale: false,
revalidateOnFocus: false,
revalidateOnReconnect: false,
}
);
// derived state
const projectLinks = getLinksByProjectId(projectId.toString());
const projectAttachments = getAttachmentsByProjectId(projectId.toString());
const project = getProjectById(projectId.toString());
if (!project) return null;
@@ -41,38 +65,40 @@ export const ProjectOverviewRoot = observer(() => {
preloadedData={linkData}
setLinkData={setLinkData}
/>
<div className="w-full h-full flex">
<div className="h-full flex-1 flex flex-col gap-4 overflow-y-scroll pb-6">
<HeroSection project={project} />
<div className="px-page-x ">
<div className="w-full h-auto flex overflow-hidden">
<div className="h-full flex-1 flex flex-col">
<HeroSection project={project} workspaceSlug={workspaceSlug.toString()} />
<div className="flex-1 overflow-y-auto px-page-x space-y-4">
<DescriptionBox
workspaceSlug={workspaceSlug.toString()}
project={project}
handleProjectUpdate={handleUpdateProject}
toggleLinkModalOpen={toggleLinkModal}
/>
<SectionHeader title="Links" actionButton={<ProjectActionButton setIsModalOpen={toggleLinkModal} />}>
<ProjectLinkRoot
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
isModalOpen={isLinkModalOpen}
setIsModalOpen={toggleLinkModal}
/>
</SectionHeader>
<SectionHeader
title="Attachments"
actionButton={
<ProjectAttachmentActionButton
<StatsToday workspaceSlug={workspaceSlug.toString()} project={project} />
{projectLinks && projectLinks?.length > 0 && (
<SectionHeader title="Links" actionButton={<ProjectActionButton setIsModalOpen={toggleLinkModal} />}>
<ProjectLinkRoot
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
isModalOpen={isLinkModalOpen}
setIsModalOpen={toggleLinkModal}
/>
}
>
<ProjectAttachmentRoot workspaceSlug={workspaceSlug.toString()} projectId={projectId.toString()} />
</SectionHeader>
<SectionHeader title="Metrics as of today">
<StatsToday workspaceSlug={workspaceSlug.toString()} project={project} />
</SectionHeader>
</SectionHeader>
)}
{projectAttachments && projectAttachments?.length > 0 && (
<SectionHeader
title="Attachments"
actionButton={
<ProjectAttachmentActionButton
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
/>
}
>
<ProjectAttachmentRoot workspaceSlug={workspaceSlug.toString()} projectId={projectId.toString()} />
</SectionHeader>
)}
</div>
</div>
<div className="w-[342px] shadow-md">

View File

@@ -3,8 +3,9 @@ import { observer } from "mobx-react";
// components
import useSWR from "swr";
import { Loader } from "@plane/ui";
import { ActivityItem } from "@/plane-web/components/common/activity/actions/helpers/activity-item";
import { ProjectActivityService } from "@/plane-web/services";
import { ProjectActivityItem } from "./activity/activity-list";
import { iconsMap, messages } from "./helper";
const projectActivityService = new ProjectActivityService();
@@ -37,10 +38,12 @@ export const ProjectActivityCommentRoot: FC<TProjectActivityCommentRoot> = obser
<div>
{activity &&
activity.map((activityComment, index) => (
<ProjectActivityItem
<ActivityItem
key={activityComment.id}
activity={activityComment}
ends={index === 0 ? "top" : index === activity.length - 1 ? "bottom" : undefined}
iconsMap={iconsMap}
messages={messages}
/>
))}
</div>

View File

@@ -1,36 +0,0 @@
"use client";
import { FC } from "react";
import { observer } from "mobx-react";
import { RotateCcw } from "lucide-react";
// hooks
import { ArchiveIcon } from "@plane/ui";
// components
import { ProjectActivityBlockComponent } from ".";
// ui
type TProjectArchivedAtActivity = { activity: any; ends: "top" | "bottom" | undefined };
export const ProjectArchivedAtActivity: FC<TProjectArchivedAtActivity> = observer((props) => {
const { activity, ends } = props;
if (!activity) return <></>;
return (
<ProjectActivityBlockComponent
icon={
activity.new_value === "restore" ? (
<RotateCcw className="h-3.5 w-3.5 text-custom-text-200" aria-hidden="true" />
) : (
<ArchiveIcon className="h-3.5 w-3.5 text-custom-text-200" aria-hidden="true" />
)
}
activity={activity}
ends={ends}
customUserName={activity.new_value === "archive" ? "Plane" : undefined}
>
{activity.new_value === "restore" ? "restored the project" : "archived the project"}.
</ProjectActivityBlockComponent>
);
});

View File

@@ -1,36 +0,0 @@
import { FC } from "react";
import { observer } from "mobx-react";
// icons
import { Users } from "lucide-react";
// components
import { ProjectActivityBlockComponent, ProjectLink } from ".";
type TProjectAssigneeActivity = { activity: any; showProject?: boolean; ends: "top" | "bottom" | undefined };
export const ProjectAssigneeActivity: FC<TProjectAssigneeActivity> = observer((props) => {
const { activity, ends, showProject = true } = props;
if (!activity) return <></>;
return (
<ProjectActivityBlockComponent
icon={<Users className="h-3.5 w-3.5 flex-shrink-0 text-custom-text-200" />}
activity={activity}
ends={ends}
>
<>
{activity.old_value === "" ? `added a new assignee ` : `removed the assignee `}
<a
href={`/${activity.workspace_detail?.slug}/profile/${activity.new_identifier ?? activity.old_identifier}`}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center font-medium text-custom-text-100 hover:underline capitalize"
>
{activity.new_value && activity.new_value !== "" ? activity.new_value : activity.old_value}
</a>
{showProject && (activity.old_value === "" ? ` to ` : ` from `)}
{showProject && <ProjectLink activity={activity} />}.
</>
</ProjectActivityBlockComponent>
);
});

View File

@@ -1,36 +0,0 @@
import { FC } from "react";
import { observer } from "mobx-react";
import { Paperclip } from "lucide-react";
// components
import { ProjectActivityBlockComponent, ProjectLink } from ".";
type TProjectAttachmentActivity = { activity: any; showProject?: boolean; ends: "top" | "bottom" | undefined };
export const ProjectAttachmentActivity: FC<TProjectAttachmentActivity> = observer((props) => {
const { activity, showProject = true, ends } = props;
if (!activity) return <></>;
return (
<ProjectActivityBlockComponent
icon={<Paperclip size={14} className="text-custom-text-200" aria-hidden="true" />}
activity={activity}
ends={ends}
>
<>
{activity.verb === "created" ? `uploaded a new ` : `removed an attachment`}
{activity.verb === "created" && (
<a
href={`${activity.new_value}`}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 font-medium text-custom-text-100 hover:underline"
>
attachment
</a>
)}
{showProject && (activity.verb === "created" ? ` to ` : ` from `)}
{showProject && <ProjectLink activity={activity} />}.
</>
</ProjectActivityBlockComponent>
);
});

View File

@@ -1,65 +0,0 @@
"use client";
import { FC } from "react";
import { observer } from "mobx-react";
// hooks
import { ContrastIcon } from "@plane/ui";
// components
import { ProjectActivityBlockComponent } from ".";
// icons
type TProjectCycleActivity = { activity: any; ends: "top" | "bottom" | undefined };
export const ProjectCycleActivity: FC<TProjectCycleActivity> = observer((props) => {
const { activity, ends } = props;
if (!activity) return <></>;
return (
<ProjectActivityBlockComponent
icon={<ContrastIcon className="h-4 w-4 flex-shrink-0 text-custom-text-200" />}
activity={activity}
ends={ends}
>
<>
{activity.verb === "created" ? (
<>
<span>added this project to the cycle </span>
<a
href={`/${activity.workspace_detail?.slug}/projects/${activity.project}/cycles/${activity.new_identifier}`}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 truncate font-medium text-custom-text-100 hover:underline"
>
<span className="truncate">{activity.new_value}</span>
</a>
</>
) : activity.verb === "updated" ? (
<>
<span>set the cycle to </span>
<a
href={`/${activity.workspace_detail?.slug}/projects/${activity.project}/cycles/${activity.new_identifier}`}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 truncate font-medium text-custom-text-100 hover:underline"
>
<span className="truncate"> {activity.new_value}</span>
</a>
</>
) : (
<>
<span>removed the project from the cycle </span>
<a
href={`/${activity.workspace_detail?.slug}/projects/${activity.project}/cycles/${activity.old_identifier}`}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 truncate font-medium text-custom-text-100 hover:underline"
>
<span className="truncate"> {activity.old_value}</span>
</a>
</>
)}
</>
</ProjectActivityBlockComponent>
);
});

View File

@@ -1,25 +0,0 @@
"use client";
import { FC } from "react";
import { observer } from "mobx-react";
// hooks
import { LayersIcon } from "@plane/ui";
// components
import { ProjectActivityBlockComponent } from ".";
// icons
type TProjectDefaultActivity = { activity: any; ends: "top" | "bottom" | undefined };
export const ProjectDefaultActivity: FC<TProjectDefaultActivity> = observer((props) => {
const { activity, ends } = props;
return (
<ProjectActivityBlockComponent
icon={<LayersIcon width={14} height={14} className="text-custom-text-200" aria-hidden="true" />}
activity={activity}
ends={ends}
>
<>{activity.verb === "created" ? " created the project." : " deleted a project."}</>
</ProjectActivityBlockComponent>
);
});

View File

@@ -1,25 +0,0 @@
import { FC } from "react";
import { observer } from "mobx-react";
import { MessageSquare } from "lucide-react";
// components
import { ProjectActivityBlockComponent, ProjectLink } from ".";
type TProjectDescriptionActivity = { activity: any; showProject?: boolean; ends: "top" | "bottom" | undefined };
export const ProjectDescriptionActivity: FC<TProjectDescriptionActivity> = observer((props) => {
const { activity, showProject = true, ends } = props;
if (!activity) return <></>;
return (
<ProjectActivityBlockComponent
icon={<MessageSquare size={14} className="text-custom-text-200" aria-hidden="true" />}
activity={activity}
ends={ends}
>
<>
{showProject ? `described the project as ${activity.new_value}` : `removed the description of the project`}
{showProject && <ProjectLink activity={activity} />}.
</>
</ProjectActivityBlockComponent>
);
});

View File

@@ -1,26 +0,0 @@
import { FC } from "react";
import { observer } from "mobx-react";
import { Triangle } from "lucide-react";
// components
import { ProjectActivityBlockComponent, ProjectLink } from ".";
type TProjectEstimateActivity = { activity: any; showProject?: boolean; ends: "top" | "bottom" | undefined };
export const ProjectEstimateActivity: FC<TProjectEstimateActivity> = observer((props) => {
const { activity, showProject = true, ends } = props;
return (
<ProjectActivityBlockComponent
icon={<Triangle size={14} className="text-custom-text-200" aria-hidden="true" />}
activity={activity}
ends={ends}
>
<>
{activity.new_value ? `set the estimate point to ` : `removed the estimate point `}
{activity.new_value ? activity.new_value : activity?.old_value || ""}
{showProject && (activity.new_value ? ` to ` : ` from `)}
{showProject && <ProjectLink activity={activity} />}.
</>
</ProjectActivityBlockComponent>
);
});

View File

@@ -1,42 +0,0 @@
"use client";
import { FC } from "react";
// hooks
import { Tooltip } from "@plane/ui";
import { usePlatformOS } from "@/hooks/use-platform-os";
// ui
type TProjectLink = {
activity: any;
};
export const ProjectLink: FC<TProjectLink> = (props) => {
const { activity } = props;
// hooks
const { isMobile } = usePlatformOS();
if (!activity) return <></>;
return (
<Tooltip
tooltipContent={activity.project_detail ? activity.project_detail.name : "This project has been deleted"}
isMobile={isMobile}
>
<a
aria-disabled={activity.project === null}
href={`${
activity.project_detail
? `/${activity.workspace_detail?.slug}/projects/${activity.project}/projects/${activity.project}`
: "#"
}`}
target={activity.project === null ? "_self" : "_blank"}
rel={activity.project === null ? "" : "noopener noreferrer"}
className="inline-flex items-center gap-1 font-medium text-custom-text-100 hover:underline"
>
{activity.project_detail
? `${activity.project_detail.identifier}-${activity.project_detail.sequence_id}`
: "Project"}{" "}
<span className="font-normal">{activity.project_detail?.name}</span>
</a>
</Tooltip>
);
};

View File

@@ -1,32 +0,0 @@
import { FC } from "react";
import { observer } from "mobx-react";
// hooks
import { Intake } from "@plane/ui";
// components
import { ProjectActivityBlockComponent, ProjectLink } from ".";
// icons
type TProjectInboxActivity = { activity: any; ends: "top" | "bottom" | undefined };
export const ProjectInboxActivity: FC<TProjectInboxActivity> = observer((props) => {
const { activity, ends } = props;
if (!activity) return <></>;
return (
<ProjectActivityBlockComponent
icon={<Intake className="h-4 w-4 flex-shrink-0 text-custom-text-200" />}
activity={activity}
ends={ends}
>
<>
{activity.new_value ? `declined this project from intake.` : `snoozed this project.`}
{activity.new_value
? `accepted this project from intake.`
: `declined this project from intake by marking a duplicate project.`}
{activity.new_value ? `updated intake project status.` : ``}
<ProjectLink activity={activity} />.
</>
</ProjectActivityBlockComponent>
);
});

View File

@@ -1,23 +0,0 @@
export * from "./default";
export * from "./name";
export * from "./description";
export * from "./state";
export * from "./assignee";
export * from "./priority";
export * from "./estimate";
export * from "./parent";
export * from "./start_date";
export * from "./target_date";
export * from "./cycle";
export * from "./module";
export * from "./label";
export * from "./link";
export * from "./attachment";
export * from "./archived-at";
export * from "./inbox";
export * from "./label-activity-chip";
// helpers
export * from "./helpers/activity-block";
export * from "./helpers/issue-user";
export * from "./helpers/project-link";

View File

@@ -1,12 +0,0 @@
import { FC } from "react";
import { observer } from "mobx-react";
import { ProjectLink } from ".";
type TProjectLabelActivityChip = { activity: any; showProject?: boolean };
export const ProjectLabelActivityChip: FC<TProjectLabelActivityChip> = observer((props) => {
const { activity, showProject = true } = props;
if (!activity) return <></>;
return <span>{showProject && <ProjectLink activity={activity} />}</span>;
});

View File

@@ -1,39 +0,0 @@
import { FC } from "react";
import { observer } from "mobx-react";
import { Tag } from "lucide-react";
// hooks
import { LabelActivityChip } from "@/components/issues/issue-detail/issue-activity/activity/actions";
import { useLabel } from "@/hooks/store";
// components
import { ProjectActivityBlockComponent, ProjectLink } from ".";
type TProjectLabelActivity = { activity: any; showProject?: boolean; ends: "top" | "bottom" | undefined };
export const ProjectLabelActivity: FC<TProjectLabelActivity> = observer((props) => {
const { activity, showProject = true, ends } = props;
// hooks
const { projectLabels } = useLabel();
if (!activity) return <></>;
return (
<ProjectActivityBlockComponent
icon={<Tag size={14} className="text-custom-text-200" aria-hidden="true" />}
activity={activity}
ends={ends}
>
<>
{activity.old_value === "" ? `added a new label ` : `removed the label `}
<LabelActivityChip
name={activity.old_value === "" ? activity.new_value : activity.old_value}
color={
activity.old_value === ""
? projectLabels?.find((l) => l.id === activity.new_identifier)?.color
: projectLabels?.find((l) => l.id === activity.old_identifier)?.color
}
/>
{showProject && (activity.old_value === "" ? ` to ` : ` from `)}
{showProject && <ProjectLink activity={activity} />}
</>
</ProjectActivityBlockComponent>
);
});

View File

@@ -1,61 +0,0 @@
import { FC } from "react";
import { observer } from "mobx-react";
import { Link } from "lucide-react";
// components
import { ProjectActivityBlockComponent, ProjectLink } from ".";
type TProjectLinkActivity = { activity: any; showProject?: boolean; ends: "top" | "bottom" | undefined };
export const ProjectLinkActivity: FC<TProjectLinkActivity> = observer((props) => {
const { activity, showProject = true, ends } = props;
return (
<ProjectActivityBlockComponent
icon={<Link size={14} className="text-custom-text-200" aria-hidden="true" />}
activity={activity}
ends={ends}
>
<>
{activity.verb === "created" ? (
<>
<span>added </span>
<a
href={`${activity.new_value}`}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 font-medium text-custom-text-100 hover:underline"
>
link
</a>
</>
) : activity.verb === "updated" ? (
<>
<span>updated the </span>
<a
href={`${activity.old_value}`}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 font-medium text-custom-text-100 hover:underline"
>
link
</a>
</>
) : (
<>
<span>removed this </span>
<a
href={`${activity.old_value}`}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 font-medium text-custom-text-100 hover:underline"
>
link
</a>
</>
)}
{showProject && (activity.verb === "created" ? ` to ` : ` from `)}
{showProject && <ProjectLink activity={activity} />}
</>
</ProjectActivityBlockComponent>
);
});

View File

@@ -1,37 +0,0 @@
"use client";
import { FC } from "react";
import { observer } from "mobx-react";
// hooks
// components
import { DiceIcon } from "@plane/ui";
import { ProjectActivityBlockComponent, ProjectLink } from ".";
type TProjectModuleActivity = { activity: any; ends: "top" | "bottom" | undefined };
export const ProjectModuleActivity: FC<TProjectModuleActivity> = observer((props) => {
const { activity, ends } = props;
if (!activity) return <></>;
return (
<ProjectActivityBlockComponent
icon={<DiceIcon className="h-4 w-4 flex-shrink-0 text-custom-text-200" />}
activity={activity}
ends={ends}
>
<>
{activity.new_value ? (
<>
<span>added this project to the module </span>
<ProjectLink activity={activity} />
</>
) : (
<>
<span>removed the project from the module </span>
<ProjectLink activity={activity} />
</>
)}
</>
</ProjectActivityBlockComponent>
);
});

View File

@@ -1,26 +0,0 @@
import { FC } from "react";
import { observer } from "mobx-react";
import { MessageSquare } from "lucide-react";
// components
import { ProjectActivityBlockComponent, ProjectLink } from ".";
type TProjectNameActivity = { activity: any; showProject?: boolean; ends: "top" | "bottom" | undefined };
export const ProjectNameActivity: FC<TProjectNameActivity> = observer((props) => {
const { activity, showProject = true, ends } = props;
if (!activity) return <></>;
return (
<ProjectActivityBlockComponent
icon={<MessageSquare size={14} className="text-custom-text-200" aria-hidden="true" />}
activity={activity}
ends={ends}
>
<>
{activity.new_value ? `updated the name to ${activity.new_value}` : `removed the name`}
{showProject && <ProjectLink activity={activity} />}.
</>
</ProjectActivityBlockComponent>
);
});

View File

@@ -1,31 +0,0 @@
import { FC } from "react";
import { observer } from "mobx-react";
import { LayoutPanelTop } from "lucide-react";
// components
import { ProjectActivityBlockComponent, ProjectLink } from ".";
type TProjectParentActivity = { activity: any; showProject?: boolean; ends: "top" | "bottom" | undefined };
export const ProjectParentActivity: FC<TProjectParentActivity> = observer((props) => {
const { activity, showProject = true, ends } = props;
if (!activity) return <></>;
return (
<ProjectActivityBlockComponent
icon={<LayoutPanelTop size={14} className="text-custom-text-200" aria-hidden="true" />}
activity={activity}
ends={ends}
>
<>
{activity.new_value ? `set the parent to ` : `removed the parent `}
{activity.new_value ? (
<span className="font-medium text-custom-text-100">{activity.new_value}</span>
) : (
<span className="font-medium text-custom-text-100">{activity.old_value}</span>
)}
{showProject && (activity.new_value ? ` for ` : ` from `)}
{showProject && <ProjectLink activity={activity} />}.
</>
</ProjectActivityBlockComponent>
);
});

View File

@@ -1,27 +0,0 @@
import { FC } from "react";
import { observer } from "mobx-react";
import { Signal } from "lucide-react";
// hooks
// components
import { ProjectActivityBlockComponent, ProjectLink } from ".";
type TProjectPriorityActivity = { activity: any; showProject?: boolean; ends: "top" | "bottom" | undefined };
export const ProjectPriorityActivity: FC<TProjectPriorityActivity> = observer((props) => {
const { activity, showProject = true, ends } = props;
if (!activity) return <></>;
return (
<ProjectActivityBlockComponent
icon={<Signal size={14} className="text-custom-text-200" aria-hidden="true" />}
activity={activity}
ends={ends}
>
<>
set the priority to <span className="font-medium text-custom-text-100">{activity.new_value}</span>
{showProject ? ` for project` : ``}
{showProject && <ProjectLink activity={activity} />}.
</>
</ProjectActivityBlockComponent>
);
});

View File

@@ -1,34 +0,0 @@
import { FC } from "react";
import { observer } from "mobx-react";
import { CalendarDays } from "lucide-react";
// hooks
import { renderFormattedDate } from "@/helpers/date-time.helper";
// components
import { ProjectActivityBlockComponent, ProjectLink } from ".";
// helpers
type TProjectStartDateActivity = { activity: any; showProject?: boolean; ends: "top" | "bottom" | undefined };
export const ProjectStartDateActivity: FC<TProjectStartDateActivity> = observer((props) => {
const { activity, showProject = true, ends } = props;
if (!activity) return <></>;
return (
<ProjectActivityBlockComponent
icon={<CalendarDays size={14} className="text-custom-text-200" aria-hidden="true" />}
activity={activity}
ends={ends}
>
<>
{activity.new_value ? `set the start date to ` : `removed the start date `}
{activity.new_value && (
<>
<span className="font-medium text-custom-text-100">{renderFormattedDate(activity.new_value)}</span>
</>
)}
{showProject && (activity.new_value ? ` for ` : ` from `)}
{showProject && <ProjectLink activity={activity} />}.
</>
</ProjectActivityBlockComponent>
);
});

View File

@@ -1,30 +0,0 @@
"use client";
import { FC } from "react";
import { observer } from "mobx-react";
// hooks
import { DoubleCircleIcon } from "@plane/ui";
// components
import { ProjectActivityBlockComponent, ProjectLink } from ".";
// icons
type TProjectStateActivity = { activity: any; showProject?: boolean; ends: "top" | "bottom" | undefined };
export const ProjectStateActivity: FC<TProjectStateActivity> = observer((props) => {
const { activity, showProject = true, ends } = props;
if (!activity) return <></>;
return (
<ProjectActivityBlockComponent
icon={<DoubleCircleIcon className="h-4 w-4 flex-shrink-0 text-custom-text-200" />}
activity={activity}
ends={ends}
>
<>
set the state to <span className="font-medium text-custom-text-100">{activity.new_value}</span>
{showProject ? ` for ` : ``}
{showProject && <ProjectLink activity={activity} />}.
</>
</ProjectActivityBlockComponent>
);
});

View File

@@ -1,34 +0,0 @@
import { FC } from "react";
import { observer } from "mobx-react";
import { CalendarDays } from "lucide-react";
// hooks
import { renderFormattedDate } from "@/helpers/date-time.helper";
// components
import { ProjectActivityBlockComponent, ProjectLink } from ".";
// helpers
type TProjectTargetDateActivity = { activity: any; showProject?: boolean; ends: "top" | "bottom" | undefined };
export const ProjectTargetDateActivity: FC<TProjectTargetDateActivity> = observer((props) => {
const { activity, showProject = true, ends } = props;
if (!activity) return <></>;
return (
<ProjectActivityBlockComponent
icon={<CalendarDays size={14} className="text-custom-text-200" aria-hidden="true" />}
activity={activity}
ends={ends}
>
<>
{activity.new_value ? `set the target date to ` : `removed the target date `}
{activity.new_value && (
<>
<span className="font-medium text-custom-text-100">{renderFormattedDate(activity.new_value)}</span>
</>
)}
{showProject && (activity.new_value ? ` for ` : ` from `)}
{showProject && <ProjectLink activity={activity} />}.
</>
</ProjectActivityBlockComponent>
);
});

View File

@@ -1,75 +0,0 @@
import { FC } from "react";
import { observer } from "mobx-react";
// local components
import {
ProjectDefaultActivity,
ProjectNameActivity,
ProjectDescriptionActivity,
ProjectStateActivity,
ProjectAssigneeActivity,
ProjectPriorityActivity,
ProjectEstimateActivity,
ProjectParentActivity,
ProjectStartDateActivity,
ProjectTargetDateActivity,
ProjectCycleActivity,
ProjectModuleActivity,
ProjectLabelActivity,
ProjectLinkActivity,
ProjectAttachmentActivity,
ProjectArchivedAtActivity,
ProjectInboxActivity,
} from "./actions";
type TProjectActivityItem = {
activity: {
field: string | null;
verb: string;
};
ends: "top" | "bottom" | undefined;
};
export const ProjectActivityItem: FC<TProjectActivityItem> = observer((props) => {
const { activity, ends } = props;
const componentDefaultProps = { activity, ends };
switch (activity.field) {
case null: // default project creation
return <ProjectDefaultActivity {...componentDefaultProps} />;
case "state":
return <ProjectStateActivity {...componentDefaultProps} showProject={false} />;
case "name":
return <ProjectNameActivity {...componentDefaultProps} />;
case "description":
return <ProjectDescriptionActivity {...componentDefaultProps} showProject={false} />;
case "assignees":
return <ProjectAssigneeActivity {...componentDefaultProps} showProject={false} />;
case "priority":
return <ProjectPriorityActivity {...componentDefaultProps} showProject={false} />;
case "estimate_point":
return <ProjectEstimateActivity {...componentDefaultProps} showProject={false} />;
case "parent":
return <ProjectParentActivity {...componentDefaultProps} showProject={false} />;
case "start_date":
return <ProjectStartDateActivity {...componentDefaultProps} showProject={false} />;
case "target_date":
return <ProjectTargetDateActivity {...componentDefaultProps} showProject={false} />;
case "cycles":
return <ProjectCycleActivity {...componentDefaultProps} />;
case "modules":
return <ProjectModuleActivity {...componentDefaultProps} />;
case "labels":
return <ProjectLabelActivity {...componentDefaultProps} showProject={false} />;
case "link":
return <ProjectLinkActivity {...componentDefaultProps} showProject={false} />;
case "attachment":
return <ProjectAttachmentActivity {...componentDefaultProps} showProject={false} />;
case "archived_at":
return <ProjectArchivedAtActivity {...componentDefaultProps} />;
case "inbox":
return <ProjectInboxActivity {...componentDefaultProps} />;
default:
return <></>;
}
});

View File

@@ -0,0 +1,315 @@
import { ReactNode } from "react";
import {
Signal,
RotateCcw,
Network,
Link as LinkIcon,
Calendar,
Tag,
Inbox,
AlignLeft,
Users,
Paperclip,
Type,
Triangle,
FileText,
Globe,
Hash,
Clock,
Bell,
LayoutGrid,
GitBranch,
Timer,
ListTodo,
Layers,
} from "lucide-react";
// components
import { ArchiveIcon, DoubleCircleIcon, ContrastIcon, DiceIcon, Intake } from "@plane/ui";
import { TProjectActivity } from "@/plane-web/types";
type ActivityIconMap = {
[key: string]: ReactNode;
};
export const iconsMap: ActivityIconMap = {
priority: <Signal size={14} className="text-custom-text-200" />,
archived_at: <ArchiveIcon className="h-3.5 w-3.5 text-custom-text-200" />,
restored: <RotateCcw className="h-3.5 w-3.5 text-custom-text-200" />,
link: <LinkIcon className="h-3.5 w-3.5 text-custom-text-200" />,
start_date: <Calendar className="h-3.5 w-3.5 text-custom-text-200" />,
target_date: <Calendar className="h-3.5 w-3.5 text-custom-text-200" />,
label: <Tag className="h-3.5 w-3.5 text-custom-text-200" />,
inbox: <Inbox className="h-3.5 w-3.5 text-custom-text-200" />,
description: <AlignLeft className="h-3.5 w-3.5 text-custom-text-200" />,
assignee: <Users className="h-3.5 w-3.5 text-custom-text-200" />,
attachment: <Paperclip className="h-3.5 w-3.5 text-custom-text-200" />,
name: <Type className="h-3.5 w-3.5 text-custom-text-200" />,
state: <DoubleCircleIcon className="h-4 w-4 flex-shrink-0 text-custom-text-200" />,
estimate: <Triangle size={14} className="text-custom-text-200" />,
cycle: <ContrastIcon className="h-4 w-4 flex-shrink-0 text-custom-text-200" />,
module: <DiceIcon className="h-4 w-4 flex-shrink-0 text-custom-text-200" />,
page: <FileText className="h-3.5 w-3.5 text-custom-text-200" />,
network: <Globe className="h-3.5 w-3.5 text-custom-text-200" />,
identifier: <Hash className="h-3.5 w-3.5 text-custom-text-200" />,
timezone: <Clock className="h-3.5 w-3.5 text-custom-text-200" />,
is_project_updates_enabled: <Bell className="h-3.5 w-3.5 text-custom-text-200" />,
is_epic_enabled: <LayoutGrid className="h-3.5 w-3.5 text-custom-text-200" />,
is_workflow_enabled: <GitBranch className="h-3.5 w-3.5 text-custom-text-200" />,
is_time_tracking_enabled: <Timer className="h-3.5 w-3.5 text-custom-text-200" />,
is_issue_type_enabled: <ListTodo className="h-3.5 w-3.5 text-custom-text-200" />,
default: <Network className="h-3.5 w-3.5 text-custom-text-200" />,
module_view: <DiceIcon className="h-3.5 w-3.5 text-custom-text-200" />,
cycle_view: <ContrastIcon className="h-3.5 w-3.5 text-custom-text-200" />,
issue_views_view: <Layers className="h-3.5 w-3.5 text-custom-text-200" />,
page_view: <FileText className="h-3.5 w-3.5 text-custom-text-200" />,
intake_view: <Intake className="h-3.5 w-3.5 text-custom-text-200" />,
};
export const messages = (activity: TProjectActivity): { message: string | ReactNode; customUserName?: string } => {
const activityType = activity.field;
const newValue = activity.new_value;
const oldValue = activity.old_value;
const verb = activity.verb;
const getBooleanActionText = (value: string) => {
if (value === "true") return "enabled";
if (value === "false") return "disabled";
return verb;
};
switch (activityType) {
case "priority":
return {
message: (
<>
set the priority to <span className="font-medium text-custom-text-100">{newValue || "none"}</span>
</>
),
};
case "archived_at":
return {
message: newValue === "restore" ? "restored the project" : "archived the project",
customUserName: newValue === "archive" ? "Plane" : undefined,
};
case "name":
return {
message: (
<>
renamed the project to <span className="font-medium text-custom-text-100">{newValue}</span>
</>
),
};
case "description":
return {
message: newValue ? "updated the project description" : "removed the project description",
};
case "start_date":
return {
message: (
<>
{newValue ? (
<>
set the start date to <span className="font-medium text-custom-text-100">{newValue}</span>
</>
) : (
"removed the start date"
)}
</>
),
};
case "target_date":
return {
message: (
<>
{newValue ? (
<>
set the target date to <span className="font-medium text-custom-text-100">{newValue}</span>
</>
) : (
"removed the target date"
)}
</>
),
};
case "state":
return {
message: (
<>
set the state to <span className="font-medium text-custom-text-100">{newValue || "none"}</span>
</>
),
};
case "estimate":
return {
message: (
<>
{newValue ? (
<>
set the estimate point to <span className="font-medium text-custom-text-100">{newValue}</span>
</>
) : (
<>
removed the estimate point
{oldValue && (
<>
{" "}
<span className="font-medium text-custom-text-100">{oldValue}</span>
</>
)}
</>
)}
</>
),
};
case "cycles":
return {
message: (
<>
<span>
{verb} this project {verb === "removed" ? "from" : "to"} the cycle{" "}
</span>
{verb !== "removed" ? (
<a
href={`/${activity.workspace_detail?.slug}/projects/${activity.project}/cycles/${activity.new_identifier}`}
target="_blank"
rel="noopener noreferrer"
className="inline-flex font-medium text-custom-text-100"
>
{activity.new_value}
</a>
) : (
<span className="font-medium text-custom-text-100">{activity.old_value || "Unknown cycle"}</span>
)}
</>
),
};
case "modules":
return {
message: (
<>
<span>
{verb} this project {verb === "removed" ? "from" : "to"} the module{" "}
</span>
<span className="font-medium text-custom-text-100">
{verb === "removed" ? oldValue : newValue || "Unknown module"}
</span>
</>
),
};
case "labels":
return {
message: (
<>
{verb} the label{" "}
<span className="font-medium text-custom-text-100">{newValue || oldValue || "Untitled label"}</span>
</>
),
};
case "inbox":
return {
message: (
<>
{newValue ? "enabled" : "disabled"} inbox for{" "}
<span className="font-medium text-custom-text-100">{activity.project_detail?.name || "this project"}</span>
</>
),
};
case "page":
return {
message: (
<>
{newValue ? "created" : "removed"} the project page{" "}
<span className="font-medium text-custom-text-100">{newValue || oldValue || "Untitled page"}</span>
</>
),
};
case "network":
return {
message: (
<>
{newValue ? "enabled" : "disabled"} network access for{" "}
<span className="font-medium text-custom-text-100">{activity.project_detail?.name || "this project"}</span>
</>
),
};
case "identifier":
return {
message: (
<>
updated project identifier to <span className="font-medium text-custom-text-100">{newValue || "none"}</span>
</>
),
};
case "timezone":
return {
message: (
<>
changed project timezone to{" "}
<span className="font-medium text-custom-text-100">{newValue || "default"}</span>
</>
),
};
case "module_view":
case "cycle_view":
case "issue_views_view":
case "page_view":
case "intake_view":
return {
message: (
<>
{getBooleanActionText(newValue)} {activityType.replace(/_view$/, "").replace(/_/g, " ")} view for{" "}
<span className="font-medium text-custom-text-100">{activity.project_detail?.name || "this project"}</span>
</>
),
};
case "is_project_updates_enabled":
return {
message: (
<>
{getBooleanActionText(newValue)} project updates for{" "}
<span className="font-medium text-custom-text-100">{activity.project_detail?.name || "this project"}</span>
</>
),
};
case "is_epic_enabled":
return {
message: (
<>
{getBooleanActionText(newValue)} epics for{" "}
<span className="font-medium text-custom-text-100">{activity.project_detail?.name || "this project"}</span>
</>
),
};
case "is_workflow_enabled":
return {
message: (
<>
{getBooleanActionText(newValue)} custom workflow for{" "}
<span className="font-medium text-custom-text-100">{activity.project_detail?.name || "this project"}</span>
</>
),
};
case "is_time_tracking_enabled":
return {
message: (
<>
{getBooleanActionText(newValue)} time tracking for{" "}
<span className="font-medium text-custom-text-100">{activity.project_detail?.name || "this project"}</span>
</>
),
};
case "is_issue_type_enabled":
return {
message: (
<>
{getBooleanActionText(newValue)} issue types for{" "}
<span className="font-medium text-custom-text-100">{activity.project_detail?.name || "this project"}</span>
</>
),
};
default:
return {
message: `${verb} ${activityType.replace(/_/g, " ")}`,
};
}
};

View File

@@ -1,6 +1,3 @@
export * from "./root";
export * from "./activity-comment-root";
// activity
export * from "./activity/activity-list";

View File

@@ -14,7 +14,7 @@ export const ProjectActivity: FC<TProjectActivity> = observer((props) => {
const { workspaceSlug, projectId } = props;
return (
<div className="space-y-4 pt-3">
<div className="space-y-4 pt-3 pb-20">
{/* rendering activity */}
<div className="space-y-3">
<div className="min-h-[200px]">

View File

@@ -109,7 +109,7 @@ export const ProjectPropertiesSidebar: React.FC<Props> = observer((props) => {
<div className="flex h-8 items-center gap-2">
<div className="flex w-2/5 flex-shrink-0 items-center gap-1 text-sm text-custom-text-300">
<Users className="h-4 w-4 flex-shrink-0" />
<span>Assignees</span>
<span>Members</span>
</div>
<MembersDropdown
value={projectMembersIds}

View File

@@ -0,0 +1,27 @@
import Image from "next/image";
import Link from "next/link";
import { Button } from "@plane/ui";
import ImagelLight from "@/public/empty-state/empty-updates-light.png";
type TProps = {
workspaceSlug: string;
projectId: string;
};
export const UpgradeProperties = (props: TProps) => {
const { workspaceSlug, projectId } = props;
return (
<div className="flex h-full">
<div className="m-auto">
<Image src={ImagelLight} alt="No updates" className="w-[161px] m-auto" />
<div className="w-fit m-auto text-lg font-medium items-center">Project Properties</div>
<div className="w-fit m-auto font-medium text-base text-custom-text-350 text-center my-2">
Enable project grouping to access this feature
</div>
<Link href={`/${workspaceSlug}/settings/project-states`} className="mt-4 mx-auto">
<Button className="mx-auto"> Enable project grouping</Button>
</Link>
</div>
</div>
);
};

View File

@@ -12,11 +12,16 @@ import { cn } from "@/helpers/common.helper";
// local components
import { useProject, useWorkspace } from "@/hooks/store";
import { useFlag, useWorkspaceFeatures } from "@/plane-web/hooks/store";
import { useProjectAdvanced } from "@/plane-web/hooks/store/projects/use-projects";
import { TProject } from "@/plane-web/types";
import { EWorkspaceFeatures } from "@/plane-web/types/workspace-feature";
import { ProjectActivity } from "./project-activity";
import { ProjectPropertiesSidebar } from "./properties";
import { UpgradeProperties } from "./properties/upgrade";
import { SidebarTabContent } from "./sidebar-tab-content";
import { ProjectUpdates } from "./updates/root";
import { UpgradeUpdates } from "./updates/upgrade";
type TEpicDetailsSidebarProps = {
project: TProject;
@@ -27,7 +32,19 @@ export const ProjectDetailsSidebar: FC<TEpicDetailsSidebarProps> = observer((pro
const { workspaceSlug } = useParams();
// store hooks
const { updateProject } = useProject();
const { features } = useProjectAdvanced();
const { currentWorkspace } = useWorkspace();
const { isWorkspaceFeatureEnabled } = useWorkspaceFeatures();
// derived
const isProjectUpdatesEnabled =
features &&
features[project.id] &&
features[project.id].is_project_updates_enabled &&
useFlag(workspaceSlug.toString(), "PROJECT_UPDATES");
const isProjectGroupingEnabled =
isWorkspaceFeatureEnabled(EWorkspaceFeatures.IS_PROJECT_GROUPING_ENABLED) &&
useFlag(workspaceSlug.toString(), "PROJECT_GROUPING");
if (!project || !currentWorkspace) return null;
@@ -42,26 +59,29 @@ export const ProjectDetailsSidebar: FC<TEpicDetailsSidebarProps> = observer((pro
icon: InfoFillIcon,
content: (
<SidebarTabContent title="Properties">
<ProjectPropertiesSidebar
project={project}
isArchived={project.archived_at !== null}
handleUpdateProject={handleUpdateProject}
workspaceSlug={workspaceSlug.toString()}
currentWorkspace={currentWorkspace}
/>
{!isProjectGroupingEnabled ? (
<ProjectPropertiesSidebar
project={project}
isArchived={project.archived_at !== null}
handleUpdateProject={handleUpdateProject}
workspaceSlug={workspaceSlug.toString()}
currentWorkspace={currentWorkspace}
/>
) : (
<UpgradeProperties workspaceSlug={workspaceSlug.toString()} projectId={project.id} />
)}
</SidebarTabContent>
),
},
{
key: "updates",
icon: UpdatesIcon,
content: (
<SidebarTabContent title="">
<ProjectUpdates />
</SidebarTabContent>
content: isProjectUpdatesEnabled ? (
<ProjectUpdates />
) : (
<UpgradeUpdates workspaceSlug={workspaceSlug.toString()} projectId={project.id} />
),
},
{
key: "activity",
icon: Activity,
@@ -76,8 +96,11 @@ export const ProjectDetailsSidebar: FC<TEpicDetailsSidebarProps> = observer((pro
return (
<div
className={cn(
`z-[5] flex flex-col gap-4 p-6 h-full border-l border-custom-border-200 bg-custom-sidebar-background-100 py-5 md:relative transition-[width] ease-linear overflow-hidden overflow-y-auto`
`!fixed z-[5] flex flex-col gap-4 p-6 h-full border-l border-custom-border-200 bg-custom-sidebar-background-100 py-5 md:relative transition-[width] ease-linear overflow-hidden overflow-y-auto`
)}
style={{
width: "inherit",
}}
>
<Tabs
tabs={PROJECT_OVERVIEW_DETAILS_SIDEBAR_TABS}

View File

@@ -11,7 +11,7 @@ export const SidebarTabContent: FC<SidebarTabContentProps> = (props) => {
const { title, children } = props;
return (
<div className="flex items-center h-full w-full flex-col divide-y-2 divide-custom-border-200 overflow-hidden">
<div className="flex flex-col gap-2 h-full w-full overflow-y-auto">
<div className="flex flex-col gap-2 h-full w-full overflow-y-auto pb-4 overflow-x-hidden">
{title && <h5 className="text-sm font-medium !mt-2">{title}</h5>}
{children}
</div>

View File

@@ -1,6 +1,6 @@
import React, { useState } from "react";
import { observer } from "mobx-react";
import { MessageCircle } from "lucide-react";
import { MessageCircle, Rocket } from "lucide-react";
import { cn } from "@plane/editor";
import { AtRiskIcon, OffTrackIcon, OnTrackIcon } from "@plane/ui";
import { renderFormattedDate } from "@/helpers/date-time.helper";
@@ -69,7 +69,7 @@ export const UpdateBlock = observer((props: TProps) => {
updateData && (
<div
key={updateData.id}
className="relative flex updateDatas-center gap-2 border border-custom-border-80 rounded-md p-4 pb-0"
className="relative flex updateDatas-center gap-2 border border-custom-border-100 rounded-md p-4 pb-0"
>
<div className="flex-1">
<div className="flex flex-1">
@@ -102,6 +102,24 @@ export const UpdateBlock = observer((props: TProps) => {
{/* Update */}
<div className="text-base my-3">{updateData.description}</div>
{/* Progress */}
<div className="text-xs text-custom-text-350 ">Since last update</div>
<div className="flex text-custom-text-300 text-xs gap-4 mb-3">
<div className="flex font-medium mr-2">
<Rocket size={12} className="my-auto mr-1" />
<span>
Progress{" "}
{updateData.total_issues > 0
? Math.round((updateData.completed_issues / updateData.total_issues) * 100)
: 0}
%
</span>
</div>
<div>
{updateData.completed_issues} / {updateData.total_issues} done
</div>
</div>
{/* Actions */}
<div className="flex gap-2 mb-3 justify-between mt-4 ">
<div className="flex gap-2 flex-wrap">
@@ -112,10 +130,15 @@ export const UpdateBlock = observer((props: TProps) => {
currentUser={currentUser}
/>
<button
className="text-custom-text-350 bg-custom-background-80 rounded h-7 w-7"
className="text-custom-text-350 bg-custom-background-80 rounded h-7 flex px-2 gap-2 text-xs font-medium items-center"
onClick={() => setShowComment(!showComment)}
>
<MessageCircle className="h-3.5 w-3.5 m-auto" />
{updateData.comments_count > 0 && (
<span>
{updateData.comments_count} {updateData.comments_count === 1 ? "Comment" : "Comments"}
</span>
)}
</button>
</div>
{/* <button

View File

@@ -101,22 +101,11 @@ export const CommentList = observer((props: TProps) => {
<div
className={cn(
"overflow-hidden transition-all duration-500 ease-in-out ",
!isCollapsed ? "max-h-[800px] border-t border-custom-border-80" : "max-h-0"
!isCollapsed ? "max-h-[800px] border-t border-custom-border-100" : "max-h-0"
)}
>
<div className="mt-4">
<form onSubmit={updateCommentOperations.create}>
<Input
placeholder="Write your comment"
value={newComment}
onChange={(e) => {
console.log(e.target.value);
setNewComment(e.target.value);
}}
className="w-full shadow border-custom-border-80 mb-4"
/>
</form>
<div className="max-h-[500px] overflow-scroll pb-2">
<div className="max-h-[300px] overflow-scroll pb-2">
{comments &&
comments.map((item, id) => {
const commentData = getCommentById(item);
@@ -133,6 +122,17 @@ export const CommentList = observer((props: TProps) => {
);
})}
</div>
<form onSubmit={updateCommentOperations.create}>
<Input
placeholder="Write your comment"
value={newComment}
onChange={(e) => {
console.log(e.target.value);
setNewComment(e.target.value);
}}
className="w-full shadow border-custom-border-100 mb-4"
/>
</form>
</div>
</div>
);

View File

@@ -26,7 +26,7 @@ export const EditComment = (props: TProps) => {
onChange={(e) => {
setNewComment(e.target.value);
}}
className="w-full shadow border-custom-border-80 mb-2"
className="w-full shadow border-custom-border-100 mb-2"
/>
{/* actions */}
<div className="flex text-sm gap-2 w-fit">

View File

@@ -15,7 +15,7 @@ export const NewUpdate = (props: TProps) => {
const [selectedStatus, setSelectedStatus] = useState(initialValues?.status ?? EProjectUpdateStatus.ON_TRACK);
return (
<div className="border border-custom-border-80 rounded-md p-4 flex flex-col gap-4">
<div className="border border-custom-border-100 rounded-md p-4 flex flex-col gap-4 mb-4">
{/* Type */}
<StatusDropdown selectedStatus={selectedStatus} setStatus={setSelectedStatus} />

View File

@@ -10,20 +10,10 @@ export const Properties = (props: TProps) => {
<div
className={cn(
"overflow-hidden transition-all duration-500 ease-in-out ",
!isCollapsed ? "max-h-[800px] border-t border-custom-border-80" : "max-h-0"
!isCollapsed ? "max-h-[800px] border-t border-custom-border-100" : "max-h-0"
)}
>
<div className="my-4">
{/* Progress */}
<div className="text-xs text-custom-text-350 ">Since last update</div>
<div className="flex text-custom-text-300 text-xs gap-4 mb-3">
<div className="flex font-medium mr-2">
<Rocket size={12} className="my-auto mr-1" />
<span>Progress 13%</span>{" "}
</div>
<div>20 / 150 done</div>
</div>
{/* Properties */}
<div>
<div className="flex">

View File

@@ -57,7 +57,7 @@ export const ProjectUpdates = observer(() => {
{/* Updates */}
{projectUpdates.length > 0 && (
<div className="flex flex-col gap-4 pb-4">
<div className="flex flex-col gap-4 pb-20">
{projectUpdates.map((updateId) => (
<UpdateBlock
updateId={updateId}

View File

@@ -0,0 +1,27 @@
import Image from "next/image";
import Link from "next/link";
import { Button } from "@plane/ui";
import ImagelLight from "@/public/empty-state/empty-updates-light.png";
type TProps = {
workspaceSlug: string;
projectId: string;
};
export const UpgradeUpdates = (props: TProps) => {
const { workspaceSlug, projectId } = props;
return (
<div className="flex h-full">
<div className="m-auto">
<Image src={ImagelLight} alt="No updates" className="w-[161px] m-auto" />
<div className="w-fit m-auto text-lg font-medium items-center">Updates</div>
<div className="w-fit m-auto font-medium text-base text-custom-text-350 text-center my-2">
Feature is disabled, you can enable it in settings
</div>
<Link href={`/${workspaceSlug}/projects/${projectId}/settings/project-updates`} className="mt-4 mx-auto">
<Button className="mx-auto"> Turn on Project Updates</Button>
</Link>
</div>
</div>
);
};

View File

@@ -0,0 +1,65 @@
"use client";
import { FC } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { useTheme } from "next-themes";
import { Crown } from "lucide-react";
// ui
import { Button, getButtonStyling } from "@plane/ui";
// helpers
import { cn } from "@/helpers/common.helper";
// plane web hooks
import { useWorkspaceSubscription } from "@/plane-web/hooks/store";
const Upgrade = observer(() => {
const { resolvedTheme } = useTheme();
const { currentWorkspaceSubscribedPlanDetail: subscriptionDetail, togglePaidPlanModal } = useWorkspaceSubscription();
// derived values
const isSelfManagedUpgradeDisabled = subscriptionDetail?.is_self_managed && subscriptionDetail?.product !== "FREE";
return (
<div
className={cn("flex flex-col rounded-xl mt-5 xl:flex-row", {
"bg-gradient-to-l from-[#CFCFCF] to-[#212121]": resolvedTheme?.includes("dark"),
"bg-gradient-to-l from-[#3b5ec6] to-[#f5f7fe]": !resolvedTheme?.includes("dark"),
})}
>
<div className={cn("flex w-full flex-col justify-center relative p-5 xl:pl-10 xl:min-h-[25rem]")}>
<div className="w-full xl:max-w-[300px]">
<div className="text-2xl font-semibold">Track all your projects from one screen.</div>
<div className="text-sm">
Group Projects like you group issuesby state, priority, or any otherand track their progress in one click.
</div>
<div className="flex mt-6 gap-4 flex-wrap">
{isSelfManagedUpgradeDisabled ? (
<a href="https://prime.plane.so/" target="_blank" className={getButtonStyling("primary", "md")}>
<Crown className="h-3.5 w-3.5" />
Get Pro
</a>
) : (
<Button variant="primary" onClick={() => togglePaidPlanModal(true)}>
<Crown className="h-3.5 w-3.5" />
Upgrade
</Button>
)}
<Link
target="_blank"
href="https://plane.so/contact"
className={"bg-transparent underline text-sm text-custom-primary-200 my-auto font-medium"}
onClick={() => {}}
>
Get custom quote
</Link>
</div>
</div>
</div>
</div>
);
});
export const ProjectUpdatesUpgrade: FC = () => (
<div className="pr-10">
<Upgrade />
</div>
);

View File

@@ -27,6 +27,14 @@ export const PROJECT_SETTINGS = {
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/epics/`,
Icon: EpicIcon,
},
project_updates: {
key: "project-updates",
label: "Project Updates",
href: `/settings/project-updates/`,
access: [EUserPermissions.ADMIN],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/updates/`,
Icon: SettingIcon,
},
};
export const PROJECT_SETTINGS_LINKS: {
@@ -46,4 +54,5 @@ export const PROJECT_SETTINGS_LINKS: {
PROJECT_SETTINGS["automations"],
PROJECT_SETTINGS["issue-types"],
PROJECT_SETTINGS["epics"],
PROJECT_SETTINGS["project_updates"],
];

View File

@@ -0,0 +1,11 @@
import { useContext } from "react";
// context
import { StoreContext } from "@/lib/store-context";
// plane web stores
import { IProjectStore } from "@/plane-web/store/projects/projects";
export const useProjectAdvanced = (): IProjectStore => {
const context = useContext(StoreContext);
if (context === undefined) throw new Error("useProject must be used within StoreProvider");
return context.projectDetails;
};

View File

@@ -29,6 +29,7 @@ export enum E_FEATURE_FLAGS {
TEAMS = "TEAMS",
INBOX_STACKING = "INBOX_STACKING",
PROJECT_OVERVIEW = "PROJECT_OVERVIEW",
PROJECT_UPDATES = "PROJECT_UPDATES",
// integrations
SILO_IMPORTERS = "SILO_IMPORTERS",
SILO_INTEGRATIONS = "SILO_INTEGRATIONS",

View File

@@ -2,7 +2,7 @@
// ce services
import { TProjectLink } from "@plane/types";
import { TProjectAnalytics } from "@/plane-web/types";
import { TProject, TProjectAnalytics, TProjectFeatures } from "@/plane-web/types";
import { ProjectService as CeProjectService } from "@/services/project";
export class ProjectService extends CeProjectService {
@@ -62,6 +62,22 @@ export class ProjectService extends CeProjectService {
throw error?.response?.data;
});
}
async getFeatures(workspaceSlug: string, projectId: string): Promise<TProjectFeatures> {
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/features/`)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
async toggleFeatures(workspaceSlug: string, projectId: string, data: Partial<TProject>): Promise<TProject> {
return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/features/`, data)
.then((response) => response?.data)
.catch((error) => {
throw error?.response;
});
}
}
const projectService = new ProjectService();

View File

@@ -1,8 +1,10 @@
/* eslint-disable no-useless-catch */
import { action, computed, makeObservable } from "mobx";
import { action, computed, makeObservable, observable, runInAction } from "mobx";
// store
import { ProjectService } from "@/plane-web/services";
import { TProject, TProjectFeatures } from "@/plane-web/types";
import { ProjectStore as CeProjectStore, IProjectStore as ICeProjectStore } from "@/store/project/project.store";
import { CoreRootStore } from "@/store/root.store";
import { IProjectAttachmentStore, ProjectAttachmentStore } from "./project-details/attachment.store";
@@ -13,26 +15,34 @@ import { IProjectUpdateStore, ProjectUpdateStore } from "./project-details/updat
export interface IProjectStore extends ICeProjectStore {
reactionStore: IProjectReactionStore;
attachmentStore: IProjectAttachmentStore;
features: Record<string, TProjectFeatures>;
//computed
publicProjectIds: string[];
privateProjectIds: string[];
myProjectIds: string[];
// actions
filteredProjectCount: (filter: string) => number | undefined;
toggleFeatures: (workspaceSlug: string, projectId: string, data: Partial<TProject>) => Promise<void>;
fetchFeatures: (workspaceSlug: string, projectId: string) => Promise<void>;
// store
linkStore: IProjectLinkStore;
updatesStore: IProjectUpdateStore;
}
export class ProjectStore extends CeProjectStore implements IProjectStore {
features: Record<string, TProjectFeatures> = {};
//store
linkStore: IProjectLinkStore;
attachmentStore: IProjectAttachmentStore;
updatesStore: IProjectUpdateStore;
reactionStore: IProjectReactionStore;
// services
projectService;
constructor(public store: CoreRootStore) {
super(store);
makeObservable(this, {
// observables
features: observable,
// computed
publicProjectIds: computed,
privateProjectIds: computed,
@@ -44,6 +54,9 @@ export class ProjectStore extends CeProjectStore implements IProjectStore {
this.attachmentStore = new ProjectAttachmentStore(this);
this.updatesStore = new ProjectUpdateStore();
this.reactionStore = new ProjectReactionStore();
// services
this.projectService = new ProjectService();
}
// computed
@@ -109,4 +122,27 @@ export class ProjectStore extends CeProjectStore implements IProjectStore {
return 0;
}
};
// actions
fetchFeatures = async (workspaceSlug: string, projectId: string): Promise<void> => {
const response = await this.projectService.getFeatures(workspaceSlug, projectId);
runInAction(() => {
this.features[projectId] = response;
});
};
toggleFeatures = async (workspaceSlug: string, projectId: string, data: Partial<TProject>): Promise<void> => {
const initialState = this.features[projectId];
try {
this.features[projectId] = {
...this.features[projectId],
...data,
};
await this.projectService.toggleFeatures(workspaceSlug, projectId, data);
} catch (error) {
console.error(error);
this.features[projectId] = initialState;
throw error;
}
};
}

View File

@@ -5,6 +5,21 @@ export interface TProjectActivity {
updatedAt: string;
userId: string;
projectId: string;
created_at: string;
field: string;
verb: string;
actor_detail: {
display_name: string;
id: string;
};
workspace_detail: {
slug: string;
};
project_detail: {
name: string;
};
new_value: string;
old_value: string;
project: string;
new_identifier?: string;
}

View File

@@ -13,4 +13,7 @@ export type TProjectUpdate = {
created_by: string;
updated_at: string;
update_reactions: TProjectUpdateReaction[];
comments_count: number;
completed_issues: number;
total_issues: number;
};

View File

@@ -9,7 +9,7 @@ export type TProject = IProject & {
target_date: string | undefined;
description_html: string | undefined;
is_epic_enabled: boolean;
is_project_updates_enabled: boolean;
};
export type TProjectAnalytics = {
@@ -20,3 +20,7 @@ export type TProjectAnalytics = {
cancelled_issues: number;
overdue_issues: number;
};
export type TProjectFeatures = {
is_project_updates_enabled: boolean;
};