mirror of
https://github.com/makeplane/plane.git
synced 2026-07-12 21:40:18 +02:00
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:
@@ -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")
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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 (
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />;
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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";
|
||||
@@ -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>;
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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 <></>;
|
||||
}
|
||||
});
|
||||
@@ -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, " ")}`,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -1,6 +1,3 @@
|
||||
export * from "./root";
|
||||
|
||||
export * from "./activity-comment-root";
|
||||
|
||||
// activity
|
||||
export * from "./activity/activity-list";
|
||||
|
||||
@@ -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]">
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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} />
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
65
web/ee/components/project-overview/upgrade.tsx
Normal file
65
web/ee/components/project-overview/upgrade.tsx
Normal 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 issues—by state, priority, or any other—and 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>
|
||||
);
|
||||
@@ -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"],
|
||||
];
|
||||
|
||||
11
web/ee/hooks/store/projects/use-projects.ts
Normal file
11
web/ee/hooks/store/projects/use-projects.ts
Normal 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;
|
||||
};
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user