diff --git a/apiserver/plane/app/serializers/project.py b/apiserver/plane/app/serializers/project.py index 7918482042..fab412ca90 100644 --- a/apiserver/plane/app/serializers/project.py +++ b/apiserver/plane/app/serializers/project.py @@ -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") diff --git a/apiserver/plane/app/views/project/base.py b/apiserver/plane/app/views/project/base.py index 4a55fa9974..207614e20f 100644 --- a/apiserver/plane/app/views/project/base.py +++ b/apiserver/plane/app/views/project/base.py @@ -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", diff --git a/apiserver/plane/ee/urls/app/project.py b/apiserver/plane/ee/urls/app/project.py index aa91c97f69..2c9d043bc6 100644 --- a/apiserver/plane/ee/urls/app/project.py +++ b/apiserver/plane/ee/urls/app/project.py @@ -90,7 +90,7 @@ urlpatterns = [ name="project-reactions", ), path( - "workspaces//projects//features/", + "workspaces//projects//features/", ProjectFeatureEndpoint.as_view(), name="project-features", ), diff --git a/apiserver/plane/ee/views/app/project/base.py b/apiserver/plane/ee/views/app/project/base.py index 1fa975a9b5..629dcba021 100644 --- a/apiserver/plane/ee/views/app/project/base.py +++ b/apiserver/plane/ee/views/app/project/base.py @@ -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 ) diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/project-updates/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/project-updates/page.tsx new file mode 100644 index 0000000000..d4183f6ea4 --- /dev/null +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/project-updates/page.tsx @@ -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 ; + } + if (!canPerformProjectAdminActions) + return ( + <> +
+

You are not authorized to access this page.

+
+ + ); + + 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 ( + <> +
+

Projects Updates

+ Toggle this on or off this project. +
+ } + workspaceSlug={workspaceSlug?.toString()} + > +
+
+
+ +
+
+
Turn on Project Updates
+ + See all updates on demand from anyone in this project. Easily track updates across four preset + categories. + +
+
+ {currentProjectDetails && ( + + )} +
+
+ + ); +}); + +export default UpdatesSettingsPage; diff --git a/web/core/layouts/auth-layout/project-wrapper.tsx b/web/core/layouts/auth-layout/project-wrapper.tsx index dcba2a8fef..e437a337f0 100644 --- a/web/core/layouts/auth-layout/project-wrapper.tsx +++ b/web/core/layouts/auth-layout/project-wrapper.tsx @@ -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 = 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 = 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, diff --git a/web/ee/components/project-overview/sidebar/project-activity/activity/actions/helpers/activity-block.tsx b/web/ee/components/common/activity/actions/helpers/activity-block.tsx similarity index 72% rename from web/ee/components/project-overview/sidebar/project-activity/activity/actions/helpers/activity-block.tsx rename to web/ee/components/common/activity/actions/helpers/activity-block.tsx index 0f164db58b..84f2dc9a47 100644 --- a/web/ee/components/project-overview/sidebar/project-activity/activity/actions/helpers/activity-block.tsx +++ b/web/ee/components/common/activity/actions/helpers/activity-block.tsx @@ -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 = (props) => { +export const ActivityBlockComponent: FC = (props) => { const { icon, activity, ends, children, customUserName } = props; // hooks const { isMobile } = usePlatformOS(); @@ -36,15 +34,17 @@ export const ProjectActivityBlockComponent: FC = {icon ? icon : }
- {children} - + {children} +
- {calculateTimeAgo(activity.created_at)} + + {calculateTimeAgo(activity.created_at)} + - +
); diff --git a/web/ee/components/common/activity/actions/helpers/activity-item.tsx b/web/ee/components/common/activity/actions/helpers/activity-item.tsx new file mode 100644 index 0000000000..c6a4a3b612 --- /dev/null +++ b/web/ee/components/common/activity/actions/helpers/activity-item.tsx @@ -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; + messages: (activity: TProjectActivity) => { message: ReactNode; customUserName?: string }; +}; + +export const ActivityItem: FC = 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 ( + + <> + {message} + {showProject && " for project "} + {activity.project_detail?.name} + + + ); +}); diff --git a/web/ee/components/project-overview/sidebar/project-activity/activity/actions/helpers/issue-user.tsx b/web/ee/components/common/activity/actions/helpers/user.tsx similarity index 81% rename from web/ee/components/project-overview/sidebar/project-activity/activity/actions/helpers/issue-user.tsx rename to web/ee/components/common/activity/actions/helpers/user.tsx index b6645682f7..0229d80e85 100644 --- a/web/ee/components/project-overview/sidebar/project-activity/activity/actions/helpers/issue-user.tsx +++ b/web/ee/components/common/activity/actions/helpers/user.tsx @@ -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 = (props) => { +export const User: FC = (props) => { const { activity, customUserName } = props; return ( diff --git a/web/ee/components/issues/advanced-header.tsx b/web/ee/components/issues/advanced-header.tsx index 69243be74c..af9dc044b3 100644 --- a/web/ee/components/issues/advanced-header.tsx +++ b/web/ee/components/issues/advanced-header.tsx @@ -89,7 +89,7 @@ export const AdvancedIssuesHeader = observer(() => { ) : null} Overview diff --git a/web/ee/components/project-overview/details/attachment/attachment-detail.tsx b/web/ee/components/project-overview/details/attachment/attachment-detail.tsx index b992608455..0eee886575 100644 --- a/web/ee/components/project-overview/details/attachment/attachment-detail.tsx +++ b/web/ee/components/project-overview/details/attachment/attachment-detail.tsx @@ -58,7 +58,7 @@ export const ProjectAttachmentsDetail: FC = observer( attachmentId={attachmentId} /> -
+
{fileIcon}
diff --git a/web/ee/components/project-overview/details/attachment/root.tsx b/web/ee/components/project-overview/details/attachment/root.tsx index 7dbdccff0d..5477da8694 100644 --- a/web/ee/components/project-overview/details/attachment/root.tsx +++ b/web/ee/components/project-overview/details/attachment/root.tsx @@ -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 = 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 (
diff --git a/web/ee/components/project-overview/details/header/actions.tsx b/web/ee/components/project-overview/details/header/actions.tsx index ce85306cea..7b4441c26c 100644 --- a/web/ee/components/project-overview/details/header/actions.tsx +++ b/web/ee/components/project-overview/details/header/actions.tsx @@ -9,7 +9,7 @@ type TProps = { export const Actions = (props: TProps) => { const { toggleLinkModalOpen, workspaceSlug, projectId } = props; return ( -
+
+ +
+
+ ); +}; diff --git a/web/ee/components/project-overview/sidebar/root.tsx b/web/ee/components/project-overview/sidebar/root.tsx index fd3b9356be..1bcfb7c428 100644 --- a/web/ee/components/project-overview/sidebar/root.tsx +++ b/web/ee/components/project-overview/sidebar/root.tsx @@ -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 = 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 = observer((pro icon: InfoFillIcon, content: ( - + {!isProjectGroupingEnabled ? ( + + ) : ( + + )} ), }, { key: "updates", icon: UpdatesIcon, - content: ( - - - + content: isProjectUpdatesEnabled ? ( + + ) : ( + ), }, - { key: "activity", icon: Activity, @@ -76,8 +96,11 @@ export const ProjectDetailsSidebar: FC = observer((pro return (
= (props) => { const { title, children } = props; return (
-
+
{title &&
{title}
} {children}
diff --git a/web/ee/components/project-overview/sidebar/updates/block.tsx b/web/ee/components/project-overview/sidebar/updates/block.tsx index 5fa912d080..3bc1fb5af7 100644 --- a/web/ee/components/project-overview/sidebar/updates/block.tsx +++ b/web/ee/components/project-overview/sidebar/updates/block.tsx @@ -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 && (
@@ -102,6 +102,24 @@ export const UpdateBlock = observer((props: TProps) => { {/* Update */}
{updateData.description}
+ {/* Progress */} +
Since last update
+
+
+ + + Progress{" "} + {updateData.total_issues > 0 + ? Math.round((updateData.completed_issues / updateData.total_issues) * 100) + : 0} + % + +
+
+ {updateData.completed_issues} / {updateData.total_issues} done +
+
+ {/* Actions */}
@@ -112,10 +130,15 @@ export const UpdateBlock = observer((props: TProps) => { currentUser={currentUser} />
{/* + +
+
+ ); +}; diff --git a/web/ee/components/project-overview/upgrade.tsx b/web/ee/components/project-overview/upgrade.tsx new file mode 100644 index 0000000000..bea474f126 --- /dev/null +++ b/web/ee/components/project-overview/upgrade.tsx @@ -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 ( +
+
+
+
Track all your projects from one screen.
+
+ Group Projects like you group issues—by state, priority, or any other—and track their progress in one click. +
+
+ {isSelfManagedUpgradeDisabled ? ( + + + Get Pro + + ) : ( + + )} + {}} + > + Get custom quote + +
+
+
+
+ ); +}); + +export const ProjectUpdatesUpgrade: FC = () => ( +
+ +
+); diff --git a/web/ee/constants/project/settings/tabs.ts b/web/ee/constants/project/settings/tabs.ts index f23633f3f8..cd55a8c2e1 100644 --- a/web/ee/constants/project/settings/tabs.ts +++ b/web/ee/constants/project/settings/tabs.ts @@ -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"], ]; diff --git a/web/ee/hooks/store/projects/use-projects.ts b/web/ee/hooks/store/projects/use-projects.ts new file mode 100644 index 0000000000..99b975a7f2 --- /dev/null +++ b/web/ee/hooks/store/projects/use-projects.ts @@ -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; +}; diff --git a/web/ee/hooks/store/use-flag.ts b/web/ee/hooks/store/use-flag.ts index cf3ec4320b..1863b48977 100644 --- a/web/ee/hooks/store/use-flag.ts +++ b/web/ee/hooks/store/use-flag.ts @@ -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", diff --git a/web/ee/services/project/project.service.ts b/web/ee/services/project/project.service.ts index 390d6f1e19..e7b7edd9c7 100644 --- a/web/ee/services/project/project.service.ts +++ b/web/ee/services/project/project.service.ts @@ -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 { + 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): Promise { + return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/features/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } } const projectService = new ProjectService(); diff --git a/web/ee/store/projects/projects.ts b/web/ee/store/projects/projects.ts index 3510dbf486..e1625b8ce6 100644 --- a/web/ee/store/projects/projects.ts +++ b/web/ee/store/projects/projects.ts @@ -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; //computed publicProjectIds: string[]; privateProjectIds: string[]; myProjectIds: string[]; // actions filteredProjectCount: (filter: string) => number | undefined; + toggleFeatures: (workspaceSlug: string, projectId: string, data: Partial) => Promise; + fetchFeatures: (workspaceSlug: string, projectId: string) => Promise; // store linkStore: IProjectLinkStore; updatesStore: IProjectUpdateStore; } export class ProjectStore extends CeProjectStore implements IProjectStore { + features: Record = {}; //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 => { + const response = await this.projectService.getFeatures(workspaceSlug, projectId); + runInAction(() => { + this.features[projectId] = response; + }); + }; + + toggleFeatures = async (workspaceSlug: string, projectId: string, data: Partial): Promise => { + 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; + } + }; } diff --git a/web/ee/types/projects/project-activity.ts b/web/ee/types/projects/project-activity.ts index b64e465bac..ed75a6b9c8 100644 --- a/web/ee/types/projects/project-activity.ts +++ b/web/ee/types/projects/project-activity.ts @@ -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; } diff --git a/web/ee/types/projects/project_updates.ts b/web/ee/types/projects/project_updates.ts index 59e6581606..d5caf83356 100644 --- a/web/ee/types/projects/project_updates.ts +++ b/web/ee/types/projects/project_updates.ts @@ -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; }; diff --git a/web/ee/types/projects/projects.ts b/web/ee/types/projects/projects.ts index 68b60f9021..8f9cebd10e 100644 --- a/web/ee/types/projects/projects.ts +++ b/web/ee/types/projects/projects.ts @@ -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; +};