From 1ed74e76b8f4f4869c11dccad0ce073e844cf1cf Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Fri, 25 Apr 2025 01:53:56 +0530 Subject: [PATCH 01/69] chore: analytics endpoint --- apiserver/plane/app/urls/analytic.py | 18 + apiserver/plane/app/views/__init__.py | 6 + apiserver/plane/app/views/analytic/advance.py | 371 ++++++++++++++++++ 3 files changed, 395 insertions(+) create mode 100644 apiserver/plane/app/views/analytic/advance.py diff --git a/apiserver/plane/app/urls/analytic.py b/apiserver/plane/app/urls/analytic.py index abe18f2adf6..0eebd3108bc 100644 --- a/apiserver/plane/app/urls/analytic.py +++ b/apiserver/plane/app/urls/analytic.py @@ -6,6 +6,9 @@ AnalyticViewViewset, SavedAnalyticEndpoint, ExportAnalyticsEndpoint, + AdvanceAnalyticsEndpoint, + AdvanceAnalyticsStatsEndpoint, + AdvanceAnalyticsChartEndpoint, DefaultAnalyticsEndpoint, ProjectStatsEndpoint, ) @@ -49,4 +52,19 @@ ProjectStatsEndpoint.as_view(), name="project-analytics", ), + path( + "workspaces//advance-analytics/", + AdvanceAnalyticsEndpoint.as_view(), + name="advance-analytics", + ), + path( + "workspaces//advance-analytics-stats/", + AdvanceAnalyticsStatsEndpoint.as_view(), + name="advance-analytics-stats", + ), + path( + "workspaces//advance-analytics-charts/", + AdvanceAnalyticsChartEndpoint.as_view(), + name="advance-analytics-chart", + ), ] diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py index 7baba9bb075..2034c55487b 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -199,6 +199,12 @@ ProjectStatsEndpoint, ) +from .analytic.advance import ( + AdvanceAnalyticsEndpoint, + AdvanceAnalyticsStatsEndpoint, + AdvanceAnalyticsChartEndpoint, +) + from .notification.base import ( NotificationViewSet, UnreadNotificationEndpoint, diff --git a/apiserver/plane/app/views/analytic/advance.py b/apiserver/plane/app/views/analytic/advance.py new file mode 100644 index 00000000000..e8d09df5adf --- /dev/null +++ b/apiserver/plane/app/views/analytic/advance.py @@ -0,0 +1,371 @@ +from rest_framework.response import Response +from rest_framework import status + +from plane.app.views.base import BaseAPIView +from plane.app.permissions import ROLE, allow_permission +from plane.db.models import ( + WorkspaceMember, + Project, + Issue, + Cycle, + Module, + IssueView, + ProjectPage, +) + +from django.utils import timezone +from django.db.models import ( + Q, + Count, +) +from datetime import timedelta + + +class AdvanceAnalyticsEndpoint(BaseAPIView): + def initialize_workspace(self, slug): + self._workspace_slug = slug + project_ids = self.request.GET.get("project_ids", None) + self.base_filters = { + "workspace__slug": slug, + "project__project_projectmember__member": self.request.user, + "project__project_projectmember__is_active": True, + } + + self.project_filters = { + "workspace__slug": slug, + "project_projectmember__member": self.request.user, + "project_projectmember__is_active": True, + } + + if project_ids: + if isinstance(project_ids, str): + project_ids = [str(project_id) for project_id in project_ids.split(",")] + self.base_filters["project_id__in"] = project_ids + self.project_filters["id__in"] = project_ids + + def get_date_filters(self, date_filter): + now = timezone.now() + if date_filter == "today": + return { + "current": { + "gte": now.replace(hour=0, minute=0, second=0, microsecond=0), + "lte": now, + }, + "previous": { + "gte": (now - timedelta(days=1)).replace( + hour=0, minute=0, second=0, microsecond=0 + ), + "lte": now - timedelta(days=1), + }, + } + elif date_filter == "yesterday": + return { + "current": { + "gte": (now - timedelta(days=1)).replace( + hour=0, minute=0, second=0, microsecond=0 + ), + "lte": now - timedelta(days=1), + }, + "previous": { + "gte": (now - timedelta(days=2)).replace( + hour=0, minute=0, second=0, microsecond=0 + ), + "lte": now - timedelta(days=2), + }, + } + elif date_filter == "last_7_days": + return { + "current": {"gte": now - timedelta(days=7), "lte": now}, + "previous": { + "gte": now - timedelta(days=14), + "lte": now - timedelta(days=7), + }, + } + elif date_filter == "last_30_days": + return { + "current": {"gte": now - timedelta(days=30), "lte": now}, + "previous": { + "gte": now - timedelta(days=60), + "lte": now - timedelta(days=30), + }, + } + return None + + def get_filtered_counts(self, queryset, date_filters): + def get_filtered_count(): + if date_filters: + return queryset.filter( + created_at__gte=date_filters["current"]["gte"], + created_at__lte=date_filters["current"]["lte"], + ).count() + return queryset.count() + + def get_previous_count(): + if date_filters: + return queryset.filter( + created_at__gte=date_filters["previous"]["gte"], + created_at__lte=date_filters["previous"]["lte"], + ).count() + return 0 + + return { + "count": get_filtered_count(), + "filter_count": get_previous_count(), + } + + def get_overview_data(self, date_filter, project_ids): + date_filters = self.get_date_filters(date_filter) + + return { + "total_users": self.get_filtered_counts( + WorkspaceMember.objects.filter( + workspace__slug=self.workspace_slug, is_active=True + ), + date_filters, + ), + "total_admins": self.get_filtered_counts( + WorkspaceMember.objects.filter( + workspace__slug=self.workspace_slug, + role=ROLE.ADMIN.value, + is_active=True, + ), + date_filters, + ), + "total_members": self.get_filtered_counts( + WorkspaceMember.objects.filter( + workspace__slug=self.workspace_slug, + role=ROLE.MEMBER.value, + is_active=True, + ), + date_filters, + ), + "total_guests": self.get_filtered_counts( + WorkspaceMember.objects.filter( + workspace__slug=self.workspace_slug, + role=ROLE.GUEST.value, + is_active=True, + ), + date_filters, + ), + "total_projects": self.get_filtered_counts( + Project.objects.filter(**self.project_filters), + date_filters, + ), + "total_work_items": self.get_filtered_counts( + Issue.issue_objects.filter(**self.base_filters), date_filters + ), + "total_cycles": self.get_filtered_counts( + Cycle.objects.filter(**self.base_filters), date_filters + ), + "total_intake": self.get_filtered_counts( + Issue.objects.filter(**self.base_filters).filter( + issue_intake__isnull=False + ), + date_filters, + ), + } + + def get_work_items_stats(self, date_filter, project_ids): + date_filters = self.get_date_filters(date_filter) + base_queryset = Issue.objects.filter(**self.base_filters) + + return { + "total_work_items": self.get_filtered_counts(base_queryset, date_filters), + "started_work_items": self.get_filtered_counts( + base_queryset.filter(state__group=True), date_filters + ), + "backlog_work_items": self.get_filtered_counts( + base_queryset.filter(state__group=True), date_filters + ), + "un_started_work_items": self.get_filtered_counts( + base_queryset.filter(state__group=True), date_filters + ), + "completed_work_items": self.get_filtered_counts( + base_queryset.filter(state__group="completed"), date_filters + ), + } + + @allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE") + def get(self, request, slug): + self.initialize_workspace(slug) + tab = request.GET.get("tab", "overview") + project_ids = request.GET.get("project_ids", None) + date_filter = request.GET.get("date_filter", "today") + + if project_ids: + project_ids = [str(project_id) for project_id in project_ids.split(",")] + + if tab == "overview": + return Response( + self.get_overview_data(date_filter, project_ids), + status=status.HTTP_200_OK, + ) + + elif tab == "work-items": + return Response( + self.get_work_items_stats(date_filter, project_ids), + status=status.HTTP_200_OK, + ) + + return Response({"message": "Invalid tab"}, status=status.HTTP_400_BAD_REQUEST) + + +class AdvanceAnalyticsStatsEndpoint(BaseAPIView): + + def initialize_workspace(self, slug): + self._workspace_slug = slug + self.base_filters = { + "workspace__slug": slug, + "project__project_projectmember__member": self.request.user, + "project__project_projectmember__is_active": True, + } + + def project_stats(self, filters): + return ( + Project.objects.filter( + workspace__slug=self._workspace_slug, + project_projectmember__member=self.request.user, + project_projectmember__is_active=True, + ) + .annotate( + total_work_items=Count("project_issue", distinct=True), + total_cycles=Count("project_cycle", distinct=True), + total_modules=Count("project_module", distinct=True), + total_intake=Count( + "project_issue", + filter=Q(project_issue__issue_intake__isnull=False), + distinct=True, + ), + total_members=Count( + "project_projectmember", + filter=Q( + project_projectmember__is_active=True, + ), + distinct=True, + ), + total_epics=Count( + "project_issue", + filter=Q(project_issue__type__is_epic=True), + distinct=True, + ), + total_pages=Count("project_pages", distinct=True), + total_views=Count("project_issueview", distinct=True), + ) + .values( + "id", + "name", + "logo_props", + "total_work_items", + "total_cycles", + "total_modules", + "total_intake", + "total_members", + "total_epics", + "total_pages", + "total_views", + ) + ) + + def get_project_issues_stats(self, filters): + qs = Issue.objects.filter( + project__workspace__slug=self._workspace_slug, + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + **filters, + ) + + return ( + qs.values("project_id", "project__name") + .annotate( + cancelled_work_items=Count("id", filter=Q(state__group="cancelled")), + completed_work_items=Count("id", filter=Q(state__group="completed")), + backlog_work_items=Count("id", filter=Q(state__group="backlog")), + un_started_work_items=Count("id", filter=Q(state__group="un-started")), + started_work_items=Count("id", filter=Q(state__group="started")), + ) + .order_by("project_id") + ) + + @allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE") + def get(self, request, slug): + self.initialize_workspace(slug) + type = request.GET.get("type", "overview") + filters = request.GET.get("filters", {}) + + if type == "work-items": + return Response( + self.get_project_issues_stats(filters), status=status.HTTP_200_OK + ) + + return Response({"message": "Invalid type"}, status=status.HTTP_400_BAD_REQUEST) + + +class AdvanceAnalyticsChartEndpoint(BaseAPIView): + + def initialize_workspace(self, slug): + self._workspace_slug = slug + self.base_filters = { + "workspace__slug": slug, + "project_projectmember__member": self.request.user, + "project_projectmember__is_active": True, + } + + def project_chart(self, filters): + project_ids = ( + Project.objects.filter(**self.base_filters) + .values_list("id", flat=True) + .distinct() + ) + + total_work_items = Issue.issue_objects.filter( + project_id__in=project_ids + ).count() + total_cycles = Cycle.objects.filter(project_id__in=project_ids).count() + total_modules = Module.objects.filter(project_id__in=project_ids).count() + total_intake = Issue.objects.filter( + project_id__in=project_ids, issue_intake__isnull=False + ).count() + total_members = WorkspaceMember.objects.filter( + workspace__slug=self._workspace_slug, is_active=True + ).count() + + total_epics = Issue.objects.filter( + project_id__in=project_ids, type__is_epic=True + ).count() + total_pages = ProjectPage.objects.filter(project_id__in=project_ids).count() + total_views = IssueView.objects.filter(project_id__in=project_ids).count() + + data = { + "total_work_items": total_work_items, + "total_cycles": total_cycles, + "total_modules": total_modules, + "total_intake": total_intake, + "total_members": total_members, + "total_epics": total_epics, + "total_pages": total_pages, + "total_views": total_views, + } + + return [ + { + "key": key, + "name": key.replace("_", " ").title(), + "count": value or 0, + } + for key, value in data.items() + ] + + @allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE") + def get(self, request, slug): + + self.initialize_workspace(slug) + type = request.GET.get("type", "overview") + filters = request.GET.get("filters", {}) + + if type == "projects": + return Response(self.project_chart(filters), status=status.HTTP_200_OK) + + elif type == "work-items": + return Response(self.work_item_chart(filters), status=status.HTTP_200_OK) + + return Response({"message": "Invalid type"}, status=status.HTTP_400_BAD_REQUEST) From ef45ded60320c15b1bfdcc8497286c0fddf3491e Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Fri, 25 Apr 2025 15:24:14 +0530 Subject: [PATCH 02/69] added anlytics v2 --- packages/constants/src/analytics-v2/common.ts | 7 + packages/constants/src/analytics-v2/index.ts | 1 + packages/constants/src/index.ts | 1 + packages/constants/src/state.ts | 22 ++ packages/propel/package.json | 4 +- .../propel/src/charts/radar-chart/index.ts | 1 + .../propel/src/charts/radar-chart/root.tsx | 78 +++++ .../propel/src/charts/scatter-chart/index.ts | 1 + .../propel/src/charts/scatter-chart/root.tsx | 149 ++++++++ packages/propel/src/table/core.tsx | 120 +++++++ packages/propel/src/table/data-table.tsx | 122 +++++++ packages/propel/src/table/index.ts | 2 + packages/types/src/analytics-v2.d.ts | 32 ++ packages/types/src/charts.d.ts | 69 +++- packages/types/src/enums.ts | 6 + packages/types/src/index.d.ts | 1 + .../(projects)/analytics-v2/header.tsx | 72 ++++ .../(projects)/analytics-v2/layout.tsx | 14 + .../(projects)/analytics-v2/page.tsx | 91 +++++ web/ce/components/analytics-v2/tabs.ts | 6 + .../analytics-v2/analytics-filter-actions.tsx | 35 ++ .../analytics-section-wrapper.tsx | 24 ++ .../analytics-v2/analytics-wrapper.tsx | 21 ++ .../analytics-v2/duration-dropdown.tsx | 201 +++++++++++ web/core/components/analytics-v2/index.ts | 1 + .../components/analytics-v2/insight-card.tsx | 46 +++ .../analytics-v2/insight-table/index.ts | 1 + .../analytics-v2/insight-table/loader.tsx | 47 +++ .../analytics-v2/insight-table/root.tsx | 31 ++ .../overview/active-project-item.tsx | 40 +++ .../analytics-v2/overview/active-projects.tsx | 24 ++ .../components/analytics-v2/overview/index.ts | 1 + .../overview/project-insights.tsx | 65 ++++ .../components/analytics-v2/overview/root.tsx | 23 ++ .../analytics-v2/temp-dummy-data.ts | 317 ++++++++++++++++++ .../analytics-v2/total-insights.tsx | 40 +++ .../components/analytics-v2/trend-piece.tsx | 54 +++ .../work-items/customized-insights.tsx | 32 ++ .../analytics-v2/work-items/index.ts | 1 + .../analytics-v2/work-items/root.tsx | 22 ++ .../work-items/workitems-insight-table.tsx | 92 +++++ web/core/hooks/store/index.ts | 1 + web/core/hooks/store/use-analytics-v2.ts | 11 + web/core/services/analytics-v2.service.ts | 37 ++ web/core/store/analytics-v2.store.ts | 76 +++++ web/core/store/root.store.ts | 3 + web/package.json | 1 + yarn.lock | 176 +++++++++- 48 files changed, 2214 insertions(+), 8 deletions(-) create mode 100644 packages/constants/src/analytics-v2/common.ts create mode 100644 packages/constants/src/analytics-v2/index.ts create mode 100644 packages/propel/src/charts/radar-chart/index.ts create mode 100644 packages/propel/src/charts/radar-chart/root.tsx create mode 100644 packages/propel/src/charts/scatter-chart/index.ts create mode 100644 packages/propel/src/charts/scatter-chart/root.tsx create mode 100644 packages/propel/src/table/core.tsx create mode 100644 packages/propel/src/table/data-table.tsx create mode 100644 packages/propel/src/table/index.ts create mode 100644 packages/types/src/analytics-v2.d.ts create mode 100644 web/app/[workspaceSlug]/(projects)/analytics-v2/header.tsx create mode 100644 web/app/[workspaceSlug]/(projects)/analytics-v2/layout.tsx create mode 100644 web/app/[workspaceSlug]/(projects)/analytics-v2/page.tsx create mode 100644 web/ce/components/analytics-v2/tabs.ts create mode 100644 web/core/components/analytics-v2/analytics-filter-actions.tsx create mode 100644 web/core/components/analytics-v2/analytics-section-wrapper.tsx create mode 100644 web/core/components/analytics-v2/analytics-wrapper.tsx create mode 100644 web/core/components/analytics-v2/duration-dropdown.tsx create mode 100644 web/core/components/analytics-v2/index.ts create mode 100644 web/core/components/analytics-v2/insight-card.tsx create mode 100644 web/core/components/analytics-v2/insight-table/index.ts create mode 100644 web/core/components/analytics-v2/insight-table/loader.tsx create mode 100644 web/core/components/analytics-v2/insight-table/root.tsx create mode 100644 web/core/components/analytics-v2/overview/active-project-item.tsx create mode 100644 web/core/components/analytics-v2/overview/active-projects.tsx create mode 100644 web/core/components/analytics-v2/overview/index.ts create mode 100644 web/core/components/analytics-v2/overview/project-insights.tsx create mode 100644 web/core/components/analytics-v2/overview/root.tsx create mode 100644 web/core/components/analytics-v2/temp-dummy-data.ts create mode 100644 web/core/components/analytics-v2/total-insights.tsx create mode 100644 web/core/components/analytics-v2/trend-piece.tsx create mode 100644 web/core/components/analytics-v2/work-items/customized-insights.tsx create mode 100644 web/core/components/analytics-v2/work-items/index.ts create mode 100644 web/core/components/analytics-v2/work-items/root.tsx create mode 100644 web/core/components/analytics-v2/work-items/workitems-insight-table.tsx create mode 100644 web/core/hooks/store/use-analytics-v2.ts create mode 100644 web/core/services/analytics-v2.service.ts create mode 100644 web/core/store/analytics-v2.store.ts diff --git a/packages/constants/src/analytics-v2/common.ts b/packages/constants/src/analytics-v2/common.ts new file mode 100644 index 00000000000..c779b0bfed0 --- /dev/null +++ b/packages/constants/src/analytics-v2/common.ts @@ -0,0 +1,7 @@ +import { TAnalyticsTabsV2Base } from "@plane/types"; + +export const insightsFields: Record = { + "overview": ["total_users", "total_admins", "total_members", "total_guests", "total_projects", "total_work_items", "total_cycles", "total_intake"], + "work-items": ["total_work_items", "started_work_items", "backlog_work_items", "un_started_work_items", "completed_work_items"], +} + diff --git a/packages/constants/src/analytics-v2/index.ts b/packages/constants/src/analytics-v2/index.ts new file mode 100644 index 00000000000..3e2372da34e --- /dev/null +++ b/packages/constants/src/analytics-v2/index.ts @@ -0,0 +1 @@ +export * from "./common" \ No newline at end of file diff --git a/packages/constants/src/index.ts b/packages/constants/src/index.ts index f974dd64b97..0d12e55bc5e 100644 --- a/packages/constants/src/index.ts +++ b/packages/constants/src/index.ts @@ -32,3 +32,4 @@ export * from "./dashboard"; export * from "./page"; export * from "./emoji"; export * from "./subscription"; +export * from "./analytics-v2"; diff --git a/packages/constants/src/state.ts b/packages/constants/src/state.ts index fa0f5d27700..eb3a2450079 100644 --- a/packages/constants/src/state.ts +++ b/packages/constants/src/state.ts @@ -1,3 +1,6 @@ +"use client" +import { EUpdateStatus } from "@plane/types/src/enums"; +import { AtRiskIcon, OffTrackIcon, OnTrackIcon } from "@plane/ui"; export type TStateGroups = "backlog" | "unstarted" | "started" | "completed" | "cancelled"; export type TDraggableData = { @@ -77,4 +80,23 @@ export const PROGRESS_STATE_GROUPS_DETAILS = [ }, ]; +export const StatusOptions = { + [EUpdateStatus.ON_TRACK]: { + icon: OnTrackIcon, + color: "#1FAD40", + backgroundColor: "#1fad401f", + }, + + [EUpdateStatus.AT_RISK]: { + icon: AtRiskIcon, + color: "#CC7700", + backgroundColor: "#cc77002e", + }, + [EUpdateStatus.OFF_TRACK]: { + icon: OffTrackIcon, + color: "#CC0000", + backgroundColor: "#cc000026", + }, +}; + export const DISPLAY_WORKFLOW_PRO_CTA = false; diff --git a/packages/propel/package.json b/packages/propel/package.json index 382739d0368..10a51965ec2 100644 --- a/packages/propel/package.json +++ b/packages/propel/package.json @@ -9,10 +9,12 @@ }, "exports": { "./ui/*": "./src/ui/*.tsx", - "./charts/*": "./src/charts/*/index.ts" + "./charts/*": "./src/charts/*/index.ts", + "./table": "./src/table/index.ts" }, "dependencies": { "@radix-ui/react-slot": "^1.1.1", + "@tanstack/react-table": "^8.21.3", "class-variance-authority": "^0.7.1", "lucide-react": "^0.469.0", "react": "^18.3.1", diff --git a/packages/propel/src/charts/radar-chart/index.ts b/packages/propel/src/charts/radar-chart/index.ts new file mode 100644 index 00000000000..50a9c47c01f --- /dev/null +++ b/packages/propel/src/charts/radar-chart/index.ts @@ -0,0 +1 @@ +export * from "./root"; \ No newline at end of file diff --git a/packages/propel/src/charts/radar-chart/root.tsx b/packages/propel/src/charts/radar-chart/root.tsx new file mode 100644 index 00000000000..d23a45feba4 --- /dev/null +++ b/packages/propel/src/charts/radar-chart/root.tsx @@ -0,0 +1,78 @@ +import { TRadarChartProps } from '@plane/types'; +import React, { useMemo, useState } from 'react' +import { PolarGrid, Radar, RadarChart as CoreRadarChart, ResponsiveContainer, PolarAngleAxis, RadarProps, Tooltip, Legend } from 'recharts'; +import { CustomTooltip } from '../components/tooltip'; +import { getLegendProps } from '../components/legend'; + +const RadarChart = (props: TRadarChartProps) => { + const { data, radars, dataKey, margin, showTooltip, legend, className } = props; + + // states + const [activeIndex, setActiveIndex] = useState(null); + const [activeLegend, setActiveLegend] = useState(null); + + const itemKeys = useMemo(() => radars.map((radar) => radar.key), [radars]); + const itemLabels = useMemo(() => radars.reduce((acc, radar) => ({ ...acc, [radar.key]: radar.name }), {}), [radars]); + const itemDotColors = useMemo(() => radars.reduce((acc, radar) => ({ ...acc, [radar.key]: radar.stroke }), {}), [radars]); + + return ( +
+ + + + + {showTooltip && ( + ( + + )} + /> + )} + {legend && ( + // @ts-expect-error recharts types are not up to date + { + // @ts-expect-error recharts types are not up to date + const key: string | undefined = payload.payload?.key; + if (!key) return; + setActiveLegend(key); + setActiveIndex(null); + }} + onMouseLeave={() => setActiveLegend(null)} + {...getLegendProps(legend)} + /> + )} + {radars.map((radar) => ( + + ))} + + + +
+ ) +} + +export { RadarChart }; \ No newline at end of file diff --git a/packages/propel/src/charts/scatter-chart/index.ts b/packages/propel/src/charts/scatter-chart/index.ts new file mode 100644 index 00000000000..50a9c47c01f --- /dev/null +++ b/packages/propel/src/charts/scatter-chart/index.ts @@ -0,0 +1 @@ +export * from "./root"; \ No newline at end of file diff --git a/packages/propel/src/charts/scatter-chart/root.tsx b/packages/propel/src/charts/scatter-chart/root.tsx new file mode 100644 index 00000000000..ea625db69ec --- /dev/null +++ b/packages/propel/src/charts/scatter-chart/root.tsx @@ -0,0 +1,149 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +"use client"; + +import React, { useMemo, useState } from "react"; +import { + CartesianGrid, + ScatterChart as CoreScatterChart, + Legend, + Scatter, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, + ZAxis, +} from "recharts"; +// plane imports +import { AXIS_LABEL_CLASSNAME } from "@plane/constants"; +import { TScatterChartProps } from "@plane/types"; +// local components +import { getLegendProps } from "../components/legend"; +import { CustomXAxisTick, CustomYAxisTick } from "../components/tick"; +import { CustomTooltip } from "../components/tooltip"; + +export const ScatterChart = React.memo((props: TScatterChartProps) => { + const { + data, + scatterPoints, + margin, + xAxis, + yAxis, + + className, + tickCount = { + x: undefined, + y: 10, + }, + legend, + showTooltip = true, + } = props; + // states + const [activePoint, setActivePoint] = useState(null); + const [activeLegend, setActiveLegend] = useState(null); + // derived values + const itemKeys = useMemo(() => scatterPoints.map((point) => point.key), [scatterPoints]); + const itemLabels: Record = useMemo( + () => scatterPoints.reduce((acc, point) => ({ ...acc, [point.key]: point.label }), {}), + [scatterPoints] + ); + const itemDotColors = useMemo(() => scatterPoints.reduce((acc, point) => ({ ...acc, [point.key]: point.fill }), {}), [scatterPoints]); + + const renderPoints = useMemo( + () => + scatterPoints.map((point) => ( + setActivePoint(point.key)} + onMouseLeave={() => setActivePoint(null)} + /> + )), + [activeLegend, scatterPoints] + ); + + return ( +
+ + + + } + tickLine={false} + axisLine={false} + label={ + xAxis.label && { + value: xAxis.label, + dy: 28, + className: AXIS_LABEL_CLASSNAME, + } + } + tickCount={tickCount.x} + /> + } + tickCount={tickCount.y} + allowDecimals={!!yAxis.allowDecimals} + /> + {legend && ( + // @ts-expect-error recharts types are not up to date + setActiveLegend(payload.value)} + onMouseLeave={() => setActiveLegend(null)} + formatter={(value) => itemLabels[value]} + {...getLegendProps(legend)} + /> + )} + {showTooltip && ( + ( + + )} + /> + )} + {renderPoints} + + +
+ ); +}); +ScatterChart.displayName = "ScatterChart"; \ No newline at end of file diff --git a/packages/propel/src/table/core.tsx b/packages/propel/src/table/core.tsx new file mode 100644 index 00000000000..e6e7ad59c43 --- /dev/null +++ b/packages/propel/src/table/core.tsx @@ -0,0 +1,120 @@ +import * as React from "react" + +import { cn } from "@plane/utils" + +const Table = React.forwardRef< + HTMLTableElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+ + +)) +Table.displayName = "Table" + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableHeader.displayName = "TableHeader" + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableBody.displayName = "TableBody" + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableFooter.displayName = "TableFooter" + +const TableRow = React.forwardRef< + HTMLTableRowElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableRow.displayName = "TableRow" + +const TableHead = React.forwardRef< + HTMLTableHeaderCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
[role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> +)) +TableHead.displayName = "TableHead" + +const TableCell = React.forwardRef< + HTMLTableDataCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + [role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> +)) +TableCell.displayName = "TableCell" + +const TableCaption = React.forwardRef< + HTMLTableDataCellElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableCaption.displayName = "TableCaption" + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +} \ No newline at end of file diff --git a/packages/propel/src/table/data-table.tsx b/packages/propel/src/table/data-table.tsx new file mode 100644 index 00000000000..82600cbd1fa --- /dev/null +++ b/packages/propel/src/table/data-table.tsx @@ -0,0 +1,122 @@ +"use client" + +import * as React from "react" +import { + ColumnDef, + ColumnFiltersState, + SortingState, + VisibilityState, + flexRender, + getCoreRowModel, + getFacetedRowModel, + getFacetedUniqueValues, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table" + +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "./core" + + +interface DataTableProps { + columns: ColumnDef[] + data: TData[] +} + +export function DataTable({ + columns, + data, +}: DataTableProps) { + const [rowSelection, setRowSelection] = React.useState({}) + const [columnVisibility, setColumnVisibility] = + React.useState({}) + const [columnFilters, setColumnFilters] = React.useState( + [] + ) + const [sorting, setSorting] = React.useState([]) + + const table = useReactTable({ + data, + columns, + state: { + sorting, + columnVisibility, + rowSelection, + columnFilters, + }, + enableRowSelection: true, + onRowSelectionChange: setRowSelection, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + onColumnVisibilityChange: setColumnVisibility, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFacetedRowModel: getFacetedRowModel(), + getFacetedUniqueValues: getFacetedUniqueValues(), + }) + + return ( +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
+
+
+ ) +} \ No newline at end of file diff --git a/packages/propel/src/table/index.ts b/packages/propel/src/table/index.ts new file mode 100644 index 00000000000..b3170e296c6 --- /dev/null +++ b/packages/propel/src/table/index.ts @@ -0,0 +1,2 @@ +export * from "./core"; +export * from "./data-table"; \ No newline at end of file diff --git a/packages/types/src/analytics-v2.d.ts b/packages/types/src/analytics-v2.d.ts new file mode 100644 index 00000000000..e6218c79a36 --- /dev/null +++ b/packages/types/src/analytics-v2.d.ts @@ -0,0 +1,32 @@ + +export type TAnalyticsTabsV2Base = "overview" | "work-items" + + +// service types + +export interface IAnalyticsResponseV2 { + [key: string]: any; +} + +export interface IAnalyticsResponseFieldsV2 { + count: number; + filter_count: number; +} + +// table types + +export interface WorkItemInsightColumns { + project_id: string; + project__name: string; + cancelled_work_items: number; + completed_work_items: number; + backlog_work_items: number; + un_started_work_items: number; + started_work_items: number; +} + +type AnalyticsTableDataMap = { + "work-items": WorkItemInsightColumns, +} + + diff --git a/packages/types/src/charts.d.ts b/packages/types/src/charts.d.ts index b1fc2997db3..ca7326826de 100644 --- a/packages/types/src/charts.d.ts +++ b/packages/types/src/charts.d.ts @@ -1,3 +1,7 @@ +// ============================================================ +// Chart Base +// ============================================================ + export type TChartLegend = { align: "left" | "center" | "right"; verticalAlign: "top" | "middle" | "bottom"; @@ -40,6 +44,10 @@ type TChartProps = { showTooltip?: boolean; }; +// ============================================================ +// Bar Chart +// ============================================================ + export type TBarItem = { key: T; label: string; @@ -56,6 +64,10 @@ export type TBarChartProps = TChartProps = { key: T; label: string; @@ -71,6 +83,25 @@ export type TLineChartProps = TChartProps[]; }; +// ============================================================ +// Scatter Chart +// ============================================================ + +export type TScatterPointItem = { + key: T; + label: string; + fill: string; + stroke: string; +}; + +export type TScatterChartProps = TChartProps & { + scatterPoints: TScatterPointItem[]; +}; + +// ============================================================ +// Area Chart +// ============================================================ + export type TAreaItem = { key: T; label: string; @@ -92,6 +123,10 @@ export type TAreaChartProps = TChartProps = { key: T; fill: string; @@ -119,6 +154,10 @@ export type TPieChartProps = Pick< customLegend?: (props: any) => React.ReactNode; }; +// ============================================================ +// Tree Map +// ============================================================ + export type TreeMapItem = { name: string; value: number; @@ -126,13 +165,13 @@ export type TreeMapItem = { textClassName?: string; icon?: React.ReactElement; } & ( - | { + | { fillColor: string; } - | { + | { fillClassName: string; } -); + ); export type TreeMapChartProps = { data: TreeMapItem[]; @@ -158,3 +197,27 @@ export type TContentVisibility = { top: TTopSectionConfig; bottom: TBottomSectionConfig; }; + +// ============================================================ +// Radar Chart +// ============================================================ + +export type TRadarItem = { + key: T; + name: string; + fill?: string; + stroke?: string; + fillOpacity?: number; + dot?: { + r: number; + fillOpacity: number; + } +} + +export type TRadarChartProps = Pick< + TChartProps, + "className" | "showTooltip" | "margin" | "data" | "legend" +> & { + dataKey: T; + radars: TRadarItem[]; +} diff --git a/packages/types/src/enums.ts b/packages/types/src/enums.ts index 53138a1d798..a49bec7ab22 100644 --- a/packages/types/src/enums.ts +++ b/packages/types/src/enums.ts @@ -67,3 +67,9 @@ export enum EFileAssetType { PROJECT_DESCRIPTION = "PROJECT_DESCRIPTION", TEAM_SPACE_COMMENT_DESCRIPTION = "TEAM_SPACE_COMMENT_DESCRIPTION", } + +export enum EUpdateStatus { + OFF_TRACK = "OFF-TRACK", + ON_TRACK = "ON-TRACK", + AT_RISK = "AT-RISK", +} \ No newline at end of file diff --git a/packages/types/src/index.d.ts b/packages/types/src/index.d.ts index b6af3b562b7..0ac656fc69f 100644 --- a/packages/types/src/index.d.ts +++ b/packages/types/src/index.d.ts @@ -43,3 +43,4 @@ export * from "./home"; export * from "./stickies"; export * from "./utils"; export * from "./payment"; +export * from "./analytics-v2"; \ No newline at end of file diff --git a/web/app/[workspaceSlug]/(projects)/analytics-v2/header.tsx b/web/app/[workspaceSlug]/(projects)/analytics-v2/header.tsx new file mode 100644 index 00000000000..944ff71bc4d --- /dev/null +++ b/web/app/[workspaceSlug]/(projects)/analytics-v2/header.tsx @@ -0,0 +1,72 @@ +"use client"; + +import { useEffect } from "react"; +import { observer } from "mobx-react"; +import { useSearchParams } from "next/navigation"; +import { BarChart2, PanelRight } from "lucide-react"; +import { useTranslation } from "@plane/i18n"; +// ui +import { Breadcrumbs, CustomSearchSelect, Header } from "@plane/ui"; +// components +import { BreadcrumbLink } from "@/components/common"; +// helpers +import { cn } from "@/helpers/common.helper"; +// hooks +import { useAppTheme } from "@/hooks/store"; +export const WorkspaceAnalyticsHeader = observer(() => { + const { t } = useTranslation(); + const searchParams = useSearchParams(); + const analytics_tab = searchParams.get("analytics_tab"); + // store hooks + const { workspaceAnalyticsSidebarCollapsed, toggleWorkspaceAnalyticsSidebar } = useAppTheme(); + + useEffect(() => { + const handleToggleWorkspaceAnalyticsSidebar = () => { + if (window && window.innerWidth < 768) { + toggleWorkspaceAnalyticsSidebar(true); + } + if (window && workspaceAnalyticsSidebarCollapsed && window.innerWidth >= 768) { + toggleWorkspaceAnalyticsSidebar(false); + } + }; + + window.addEventListener("resize", handleToggleWorkspaceAnalyticsSidebar); + handleToggleWorkspaceAnalyticsSidebar(); + return () => window.removeEventListener("resize", handleToggleWorkspaceAnalyticsSidebar); + }, [toggleWorkspaceAnalyticsSidebar, workspaceAnalyticsSidebarCollapsed]); + + return ( +
+ + + } + /> + } + /> + + {analytics_tab === "custom" ? ( + + ) : ( + <> + )} + +
+ ); +}); diff --git a/web/app/[workspaceSlug]/(projects)/analytics-v2/layout.tsx b/web/app/[workspaceSlug]/(projects)/analytics-v2/layout.tsx new file mode 100644 index 00000000000..6f087aa5683 --- /dev/null +++ b/web/app/[workspaceSlug]/(projects)/analytics-v2/layout.tsx @@ -0,0 +1,14 @@ +"use client"; +// components +import { AppHeader, ContentWrapper } from "@/components/core"; +// plane web components +import { WorkspaceAnalyticsHeader } from "./header"; + +export default function WorkspaceAnalyticsLayout({ children }: { children: React.ReactNode }) { + return ( + <> + } /> + {children} + + ); +} diff --git a/web/app/[workspaceSlug]/(projects)/analytics-v2/page.tsx b/web/app/[workspaceSlug]/(projects)/analytics-v2/page.tsx new file mode 100644 index 00000000000..9f14742d2a6 --- /dev/null +++ b/web/app/[workspaceSlug]/(projects)/analytics-v2/page.tsx @@ -0,0 +1,91 @@ +"use client"; + +import { observer } from "mobx-react"; +import { useSearchParams } from "next/navigation"; +// plane package imports +import { ANALYTICS_TABS } from "@/plane-web/components/analytics-v2/tabs"; +import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import { Tabs } from "@plane/ui"; +// components +import { PageHead } from "@/components/core"; +import { ComicBoxButton, DetailedEmptyState } from "@/components/empty-state"; +// hooks +import AnalyticsFilterActions from "@/components/analytics-v2/analytics-filter-actions"; +import { useCommandPalette, useEventTracker, useProject, useUserPermissions, useWorkspace } from "@/hooks/store"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; +import { useMemo } from "react"; + +const AnalyticsPage = observer(() => { + // plane imports + const { t } = useTranslation(); + // store hooks + const { toggleCreateProjectModal } = useCommandPalette(); + const { setTrackElement } = useEventTracker(); + const { workspaceProjectIds, loader } = useProject(); + const { currentWorkspace } = useWorkspace(); + const { allowPermissions } = useUserPermissions(); + // helper hooks + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/onboarding/analytics" }); + // derived values + const pageTitle = currentWorkspace?.name + ? t(`workspace_analytics.page_label`, { workspace: currentWorkspace?.name }) + : undefined; + + // permissions + const canPerformEmptyStateActions = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.WORKSPACE + ); + + const tabs = useMemo(() => ANALYTICS_TABS.map((tab) => ({ + key: tab.key, + label: t(tab.i18nKey), + content: , + })), [t]); + + return ( + <> + + {workspaceProjectIds && ( + <> + {workspaceProjectIds.length > 0 || loader === "init-loader" ? ( +
+ } + /> +
+ ) : ( + { + setTrackElement("Analytics empty state"); + toggleCreateProjectModal(true); + }} + disabled={!canPerformEmptyStateActions} + /> + } + /> + )} + + )} + + ); +}); + +export default AnalyticsPage; diff --git a/web/ce/components/analytics-v2/tabs.ts b/web/ce/components/analytics-v2/tabs.ts new file mode 100644 index 00000000000..c63ed9d84b7 --- /dev/null +++ b/web/ce/components/analytics-v2/tabs.ts @@ -0,0 +1,6 @@ +import { Overview } from "@/components/analytics-v2/overview"; +import { WorkItems } from "@/components/analytics-v2/work-items"; +export const ANALYTICS_TABS = [ + { key: "overview", i18nKey: "common.overview", content: Overview }, + { key: "work_items", i18nKey: "sidebar.work_items", content: WorkItems }, +]; diff --git a/web/core/components/analytics-v2/analytics-filter-actions.tsx b/web/core/components/analytics-v2/analytics-filter-actions.tsx new file mode 100644 index 00000000000..6b4059f0b56 --- /dev/null +++ b/web/core/components/analytics-v2/analytics-filter-actions.tsx @@ -0,0 +1,35 @@ +// hooks +import { useAnalyticsV2 } from "@/hooks/store/use-analytics-v2"; +// plane web components +import { observer } from "mobx-react-lite"; +// components +import { ProjectDropdown } from "@/components/dropdowns"; +import DurationDropdown from "./duration-dropdown"; + +const AnalyticsFilterActions = observer(() => { + const { selectedProject, selectedDuration, updateSelectedProject, updateSelectedDuration } = useAnalyticsV2() + + return ( +
+ { + updateSelectedProject(val) + }} + buttonVariant="border-with-text" + multiple={false} + dropdownArrow + /> + { + updateSelectedDuration(val) + }} + dropdownArrow + /> +
+ ) +}) + +export default AnalyticsFilterActions \ No newline at end of file diff --git a/web/core/components/analytics-v2/analytics-section-wrapper.tsx b/web/core/components/analytics-v2/analytics-section-wrapper.tsx new file mode 100644 index 00000000000..eef197e552d --- /dev/null +++ b/web/core/components/analytics-v2/analytics-section-wrapper.tsx @@ -0,0 +1,24 @@ +import { cn } from '@plane/utils' +import React from 'react' + +type Props = { + title?: string, + children: React.ReactNode + className?: string + subtitle?: string | null +} + +const AnalyticsSectionWrapper: React.FC = (props) => { + const { title, children, className, subtitle } = props + return ( +
+ {title &&
+

{title}

+ {subtitle &&

• {subtitle}

} +
} + {children} +
+ ) +} + +export default AnalyticsSectionWrapper \ No newline at end of file diff --git a/web/core/components/analytics-v2/analytics-wrapper.tsx b/web/core/components/analytics-v2/analytics-wrapper.tsx new file mode 100644 index 00000000000..122815ebb37 --- /dev/null +++ b/web/core/components/analytics-v2/analytics-wrapper.tsx @@ -0,0 +1,21 @@ +import { cn } from '@plane/utils'; +import React from 'react' + +type Props = { + title: string; + children: React.ReactNode; + className? : string +} + +const AnalyticsWrapper: React.FC = (props) => { + const { title, children, className } = props; + + return ( +
+

{title}

+ {children} +
+ ) +} + +export default AnalyticsWrapper; \ No newline at end of file diff --git a/web/core/components/analytics-v2/duration-dropdown.tsx b/web/core/components/analytics-v2/duration-dropdown.tsx new file mode 100644 index 00000000000..96860f9a04b --- /dev/null +++ b/web/core/components/analytics-v2/duration-dropdown.tsx @@ -0,0 +1,201 @@ +// plane package imports +import { PROJECT_CREATED_AT_FILTER_OPTIONS } from '@plane/constants' +import { useTranslation } from '@plane/i18n' +import { ComboDropDown } from '@plane/ui' +import { cn } from '@plane/utils' +// plane web components +import { Combobox } from '@headlessui/react' +import { Check, ChevronDown, Search } from 'lucide-react' +import React, { ReactNode, useRef, useState } from 'react' +import { usePopper } from 'react-popper' +// components +import { DropdownButton } from '@/components/dropdowns/buttons' +import { BUTTON_VARIANTS_WITH_TEXT } from '@/components/dropdowns/constants' +import { TDropdownProps } from '@/components/dropdowns/types' +// hooks +import { useDropdown } from '@/hooks/use-dropdown' + +type Props = TDropdownProps & { + value: string | null + onChange: (val: typeof PROJECT_CREATED_AT_FILTER_OPTIONS[number]['value']) => void + //optional + button?: ReactNode + dropdownArrow?: boolean + dropdownArrowClassName?: string + onClose?: () => void + renderByDefault?: boolean + tabIndex?: number +} + +function DurationDropdown({ + buttonClassName, + buttonContainerClassName, + buttonVariant, + className, + disabled, + hideIcon, + placeholder = "Duration", + onClose, + placement, + onChange, + showTooltip = false, + dropdownArrow = false, + dropdownArrowClassName = "", + button, + renderByDefault = true, + tabIndex, + value +}: Props) { + //states + const [isOpen, setIsOpen] = useState(false) + const [query, setQuery] = useState("") + //refs + const dropdownRef = useRef(null) + const inputRef = useRef(null) + //popper-js refs + const [referenceElement, setReferenceElement] = useState(null) + const [popperElement, setPopperElement] = useState(null) + // popper-js init + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: placement ?? "bottom-start", + modifiers: [ + { + name: "preventOverflow", + options: { + padding: 12, + }, + }, + ], + }); + //store hooks + const { t } = useTranslation() + const { handleOnClick, handleClose, searchInputKeyDown } = useDropdown({ + dropdownRef, + inputRef, + isOpen, + onClose, + query, + setIsOpen, + setQuery + }) + const filteredOptions = + query === "" ? PROJECT_CREATED_AT_FILTER_OPTIONS : PROJECT_CREATED_AT_FILTER_OPTIONS?.filter((o) => o?.name.toLowerCase().includes(query.toLowerCase())); + + const dropdownOnChange = (val: typeof PROJECT_CREATED_AT_FILTER_OPTIONS[number]["value"]) => { + onChange(val) + handleClose() + } + + const getDisplayName = (value: string | string[] | null, placeholder: string = "") => { + const option = filteredOptions?.find((o) => o?.value === value) + return option ? option?.name : placeholder; + + }; + + + const comboButton = ( + <> + {button ? ( + + ) : ( + + )} + + ); + + return ( + + {isOpen && + +
+
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + filteredOptions.map((option) => { + if (!option) return; + return ( + + `w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${active ? "bg-custom-background-80" : "" + } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` + } + > + {({ selected }) => ( + <> + {option.name} + {selected && } + + )} + + ); + }) + ) : ( +

{t("no_matching_results")}

+ ) + ) : ( +

{t("loading")}

+ )} +
+
+
+ } +
+ ) +} + +export default DurationDropdown \ No newline at end of file diff --git a/web/core/components/analytics-v2/index.ts b/web/core/components/analytics-v2/index.ts new file mode 100644 index 00000000000..314c01ccf02 --- /dev/null +++ b/web/core/components/analytics-v2/index.ts @@ -0,0 +1 @@ +export * from "./overview/root"; \ No newline at end of file diff --git a/web/core/components/analytics-v2/insight-card.tsx b/web/core/components/analytics-v2/insight-card.tsx new file mode 100644 index 00000000000..e3512b2badf --- /dev/null +++ b/web/core/components/analytics-v2/insight-card.tsx @@ -0,0 +1,46 @@ +// plane package imports +import { Loader } from '@plane/ui' +import { IAnalyticsResponseFieldsV2 } from '@plane/types' +// components +import React, { useMemo } from 'react' +import TrendPiece from './trend-piece' + +export type InsightCardProps = { + data?: IAnalyticsResponseFieldsV2; + label: string; + isLoading?: boolean; +} + +const InsightCard = (props: InsightCardProps) => { + const { data, label, isLoading } = props; + const { count, filter_count } = data || {}; + const percentage = useMemo(() => { + if (count != null && filter_count != null) { + const result = ((count - filter_count) / count) * 100; + return isFinite(result) ? result : null; + } + return null; + }, [count, filter_count]); + const versus = "last month"; + + return ( +
+
{label}
+ {!isLoading ? ( +
+
{count}
+ {percentage && ( +
+ +
vs {versus}
+
+ )} +
+ ) : ( + + )} +
+ ); +} + +export default InsightCard; \ No newline at end of file diff --git a/web/core/components/analytics-v2/insight-table/index.ts b/web/core/components/analytics-v2/insight-table/index.ts new file mode 100644 index 00000000000..b0120ecc977 --- /dev/null +++ b/web/core/components/analytics-v2/insight-table/index.ts @@ -0,0 +1 @@ +export * from "./root" \ No newline at end of file diff --git a/web/core/components/analytics-v2/insight-table/loader.tsx b/web/core/components/analytics-v2/insight-table/loader.tsx new file mode 100644 index 00000000000..9e774436a0c --- /dev/null +++ b/web/core/components/analytics-v2/insight-table/loader.tsx @@ -0,0 +1,47 @@ +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@plane/propel/table"; +import { Loader } from "@plane/ui"; +import * as React from "react"; + +interface TableSkeletonProps { + columns: any[]; + rows: number; +} + +export const TableLoader: React.FC = ({ columns, rows }) => { + return ( + + + + { + columns.map((column, index) => ( + + {typeof column.header === 'string' ? column.header : ''} + + )) + } + + + + { + Array.from({ length: rows }).map((_, rowIndex) => ( + + { + columns.map((_, colIndex) => ( + + + + )) + } + + ))} + +
+ ); +}; \ No newline at end of file diff --git a/web/core/components/analytics-v2/insight-table/root.tsx b/web/core/components/analytics-v2/insight-table/root.tsx new file mode 100644 index 00000000000..0a89eec966c --- /dev/null +++ b/web/core/components/analytics-v2/insight-table/root.tsx @@ -0,0 +1,31 @@ +import { DataTable } from "@plane/propel/table"; +import { AnalyticsTableDataMap, TAnalyticsTabsV2Base } from "@plane/types"; +import { TableLoader } from "./loader"; +import { ColumnDef } from "@tanstack/react-table"; + + +interface InsightTableProps> { + analyticsType: T; + data?: AnalyticsTableDataMap[T][]; + isLoading?: boolean; + columns: ColumnDef[]; +} + +export const InsightTable = >( + props: InsightTableProps +): React.ReactElement => { + const { analyticsType, data, isLoading, columns } = props + + if (isLoading) { + return + } + return ( +
+ {data ? :
No data
} +
+ ); +}; + diff --git a/web/core/components/analytics-v2/overview/active-project-item.tsx b/web/core/components/analytics-v2/overview/active-project-item.tsx new file mode 100644 index 00000000000..ca3d090833c --- /dev/null +++ b/web/core/components/analytics-v2/overview/active-project-item.tsx @@ -0,0 +1,40 @@ +import React from 'react' +import { LucideProps } from 'lucide-react'; +import { EUpdateStatus } from '@plane/types/src/enums'; +import { StatusOptions } from '@plane/constants'; + + +type Props = { + icon: React.ComponentType, + label: string, + status: EUpdateStatus, +} +const StatusPill = ({ status }: { status: EUpdateStatus }) => { + const StatusIcon = StatusOptions[status].icon; + const statusColor = StatusOptions[status].color; + const statusBackgroundColor = StatusOptions[status].backgroundColor; + return ( +
+ + {status} +
+ ) +} +const ActiveProjectItem = (props: Props) => { + const { icon: IconComponent, label, status } = props; + return ( +
+
+
+ +
+

{label}

+
+ +
+ ) +} + +export default ActiveProjectItem \ No newline at end of file diff --git a/web/core/components/analytics-v2/overview/active-projects.tsx b/web/core/components/analytics-v2/overview/active-projects.tsx new file mode 100644 index 00000000000..09764005a45 --- /dev/null +++ b/web/core/components/analytics-v2/overview/active-projects.tsx @@ -0,0 +1,24 @@ +import React from 'react' +import AnalyticsSectionWrapper from '../analytics-section-wrapper' +import { overviewDummyData } from '../temp-dummy-data' +import ActiveProjectItem from './active-project-item' +import { EUpdateStatus } from '@plane/types/src/enums' + +type Props = {} + +const ActiveProjects = (props: Props) => { + + return ( + +
+ {overviewDummyData.active_projects.map((project) => ( + + ))} +
+
+ ) +} + + + +export default ActiveProjects diff --git a/web/core/components/analytics-v2/overview/index.ts b/web/core/components/analytics-v2/overview/index.ts new file mode 100644 index 00000000000..50a9c47c01f --- /dev/null +++ b/web/core/components/analytics-v2/overview/index.ts @@ -0,0 +1 @@ +export * from "./root"; \ No newline at end of file diff --git a/web/core/components/analytics-v2/overview/project-insights.tsx b/web/core/components/analytics-v2/overview/project-insights.tsx new file mode 100644 index 00000000000..9692406ba48 --- /dev/null +++ b/web/core/components/analytics-v2/overview/project-insights.tsx @@ -0,0 +1,65 @@ +import React, { useMemo } from 'react' +import AnalyticsSectionWrapper from '../analytics-section-wrapper' +import dynamic from 'next/dynamic' +import { overviewDummyData } from '../temp-dummy-data' +import TrendPiece from '../trend-piece' +import { useAnalyticsV2 } from '@/hooks/store/use-analytics-v2' +import { PROJECT_CREATED_AT_FILTER_OPTIONS } from '@plane/constants' +import { observer } from 'mobx-react' +const RadarChart = dynamic(() => + import("@plane/propel/charts/radar-chart").then((mod) => ({ + default: mod.RadarChart, + })) +) + +type Props = {} + +const ProjectInsights = observer((props: Props) => { + const { selectedProject, selectedDuration } = useAnalyticsV2() + const selectedDurationLabel = useMemo(() => PROJECT_CREATED_AT_FILTER_OPTIONS.find(item => item.value === selectedDuration)?.name, [selectedDuration]) + return ( +
+ + + + +
Summary of projects
+
All Projects
+
+
+
Trend on charts
+
Work items
+
+ {overviewDummyData.graph_data.map((item) => ( +
+
{item.entity}
+
+ +
{item.count}
+
+
+ ))} +
+
+
+ ) +}) + +export default ProjectInsights \ No newline at end of file diff --git a/web/core/components/analytics-v2/overview/root.tsx b/web/core/components/analytics-v2/overview/root.tsx new file mode 100644 index 00000000000..5931c6dfed8 --- /dev/null +++ b/web/core/components/analytics-v2/overview/root.tsx @@ -0,0 +1,23 @@ +import React from 'react' +import AnalyticsWrapper from '../analytics-wrapper' +import TotalInsights from '../total-insights' +import ProjectInsights from './project-insights' +import ActiveProjects from "./active-projects" + +type Props = {} + +const Overview: React.FC = (props) => { + return ( + +
+ +
+ + +
+
+
+ ) +} + +export { Overview } \ No newline at end of file diff --git a/web/core/components/analytics-v2/temp-dummy-data.ts b/web/core/components/analytics-v2/temp-dummy-data.ts new file mode 100644 index 00000000000..b1eef6b810a --- /dev/null +++ b/web/core/components/analytics-v2/temp-dummy-data.ts @@ -0,0 +1,317 @@ +import { Rocket } from "lucide-react"; +import { EUpdateStatus } from "@plane/types/src/enums"; +import { TChartData } from "@plane/types"; + +export const overviewDummyData = { + total_counts: [ + { + label: "Total Users", + count: 53, + hike_percentage: -10, + versus: "vs last month", + }, + { + label: "Total Admins", + count: 23, + hike_percentage: -3, + versus: "vs last month", + }, + { + label: "Total Members", + count: 62, + hike_percentage: -8, + versus: "vs last month", + }, + { + label: "Total Guests", + count: 55, + hike_percentage: 10, + versus: "vs last month", + }, + { + label: "Total Projects", + count: 112, + hike_percentage: 10, + versus: "vs last month", + }, + { + label: "Total Work Items", + count: 512, + hike_percentage: 10, + versus: "vs last month", + }, + { + label: "Total Work Items", + count: 53663, + hike_percentage: 10, + versus: "vs last month", + }, + { + label: "Total Intake", + count: 2, + hike_percentage: -9, + versus: "vs last month", + } + ], + active_projects: [ + { + icon: Rocket, + label: "Pulse", + status: EUpdateStatus.ON_TRACK, + }, + { + icon: Rocket, + label: "Plane Rocker", + status: EUpdateStatus.OFF_TRACK, + }, + { + icon: Rocket, + label: "Plane Rocker", + status: EUpdateStatus.AT_RISK, + }, + + ], + graph_data: [ + { + entity: 'Work Item', + count: 120, + hike_percentage: 10, + }, + { + entity: 'Epics', + count: 98, + hike_percentage: -32, + + }, + { + entity: 'Cycles', + count: 86, + hike_percentage: 43, + }, + { + entity: 'Modules', + count: 99, + hike_percentage: 9, + }, + { + entity: 'Views', + count: 85, + hike_percentage: 12, + }, + { + entity: 'Pages', + count: 65, + hike_percentage: -2, + }, + { + entity: 'Intake', + count: 1, + hike_percentage: -2, + }, + ], + lines: [ + { + key: "active", + label: "Active", + dashedLine: false, + fill: "#1192E8", + stroke: "#1192E8", + showDot: true, + smoothCurves: false, + }, + { + key: "inactive", + label: "Inactive", + dashedLine: false, + fill: "#FA4D56", + stroke: "#FA4D56", + showDot: true, + smoothCurves: false, + }, + ], + line_data: [ + { name: "Jan", active: 240, inactive: 2400 }, + { name: "Feb", active: 300, inactive: 139 }, + { name: "Mar", active: 200, inactive: 980 }, + { name: "Apr", active: 278, inactive: 390 }, + { name: "May", active: 189, inactive: 480 }, + { name: "Jun", active: 239, inactive: 380 }, + { name: "Jul", active: 349, inactive: 430 }, + ], + intake_lines: [ + { + key: "accepted", + label: "Accepted", + dashedLine: false, + fill: "#1192E8", + stroke: "#1192E8", + showDot: true, + smoothCurves: false, + }, + { + key: "rejected", + label: "Rejected", + dashedLine: false, + fill: "#FA4D56", + stroke: "#FA4D56", + showDot: true, + smoothCurves: false, + }, + ], + intake_line_data: [ + { name: "Jan", accepted: 240, rejected: 2400 }, + { name: "Feb", accepted: 300, rejected: 139 }, + { name: "Mar", accepted: 200, rejected: 980 }, + { name: "Apr", accepted: 278, rejected: 390 }, + { name: "May", accepted: 189, rejected: 480 }, + { name: "Jun", accepted: 239, rejected: 380 }, + { name: "Jul", accepted: 349, rejected: 430 }, + ], + bars: [ + { + key: "draft", + label: "Draft", + fill: "#808080", + stackId: "bar-one", + textClassName: "", + }, + { + key: "planning", + label: "Planning", + fill: "#8A2BE2", + stackId: "bar-one", + textClassName: "", + }, + { + key: "executing", + label: "Executing", + fill: "#0066CC", + stackId: "bar-one", + textClassName: "", + }, + { + key: "monitoring", + label: "Monitoring", + fill: "#008080", + stackId: "bar-one", + textClassName: "", + }, + { + key: "completed", + label: "Completed", + fill: "#006400", + stackId: "bar-one", + textClassName: "", + }, + { + key: "cancelled", + label: "Cancelled", + fill: "#2F4F4F", + stackId: "bar-one", + textClassName: "", + }, + + ], + bar_data: [ + { states: "Draft", draft: 40 }, + { states: "Planning", planning: 70 }, + { states: "Executing", executing: 40 }, + { states: "Monitoring", monitoring: 45 }, + { states: "Completed", completed: 25 }, + { states: "Cancelled", cancelled: 5 } + ] as TChartData[], + work_bars: [ + { + key: "urgent", + label: "Urgent", + fill: "#DC3545", // Red color for urgent + stackId: "bar-one", + textClassName: "text-custom-text-200", + }, + { + key: "high", + label: "High", + fill: "#FF6B00", // Orange color for high + stackId: "bar-one", + textClassName: "text-custom-text-200", + }, + { + key: "medium", + label: "Medium", + fill: "#F5C000", // Yellow/Gold color for medium + stackId: "bar-one", + textClassName: "text-custom-text-200", + }, + { + key: "low", + label: "Low", + fill: "#00B8D9", // Blue color for low + stackId: "bar-one", + textClassName: "text-custom-text-200", + }, + { + key: "none", + label: "None", + fill: "#808080", // Gray color for none + stackId: "bar-one", + textClassName: "text-custom-text-200", + }, + ], + work_bar_data: [ + { states: "Urgent", urgent: 15 }, + { states: "High", high: 40 }, + { states: "Medium", medium: 60 }, + { states: "Low", low: 35 }, + { states: "None", none: 70 } + ] as TChartData[], + sampleScatterData: { + data: [ + { x: 10, y: 20, z: 5, "epics": 15, "dashboard": 10 }, + { x: 15, y: 25, z: 8, "epics": 20, "dashboard": 12 }, + { x: 20, y: 30, z: 12, "epics": 25, "dashboard": 15 }, + { x: 25, y: 35, z: 15, "epics": 30, "dashboard": 18 }, + { x: 30, y: 40, z: 18, "epics": 35, "dashboard": 20 }, + { x: 35, y: 45, z: 22, "epics": 40, "dashboard": 22 }, + { x: 40, y: 50, z: 25, "epics": 45, "dashboard": 25 }, + { x: 45, y: 55, z: 28, "epics": 50, "dashboard": 28 }, + { x: 50, y: 60, z: 32, "epics": 55, "dashboard": 30 }, + { x: 55, y: 65, z: 35, "epics": 60, "dashboard": 32 }, + ], + scatterPoints: [ + { + key: "epics", + label: "Epics", + fill: "rgb(var(--color-primary-100))", + stroke: "rgb(var(--color-primary-200))", + }, + { + key: "dashboard", + label: "Dashboard", + fill: "rgb(var(--color-secondary-100))", + stroke: "rgb(var(--color-secondary-200))", + }, + ], + xAxis: { + key: "x", + label: "Timeline", + }, + yAxis: { + key: "y", + label: "Completion", + allowDecimals: false, + }, + margin: { + top: 0, + right: -10, + bottom: 70, + left: -10, + }, + legend: { + align: "right", + verticalAlign: "top", + layout: "horizontal", + }, + showTooltip: true, + } + +} \ No newline at end of file diff --git a/web/core/components/analytics-v2/total-insights.tsx b/web/core/components/analytics-v2/total-insights.tsx new file mode 100644 index 00000000000..c9ae5fe064e --- /dev/null +++ b/web/core/components/analytics-v2/total-insights.tsx @@ -0,0 +1,40 @@ + +// plane package imports +import { useTranslation } from '@plane/i18n'; +import { IAnalyticsResponseV2, TAnalyticsTabsV2Base } from '@plane/types'; +import useSWR from 'swr'; +//hooks +import { useAnalyticsV2 } from '@/hooks/store/use-analytics-v2'; +//services +import { AnalyticsV2Service } from '@/services/analytics-v2.service'; +// plane web components +import InsightCard from './insight-card'; +import { observer } from 'mobx-react-lite'; +import { useParams } from 'next/navigation'; +import { insightsFields } from '@plane/constants'; + + +const analyticsV2Service = new AnalyticsV2Service(); + +const TotalInsights: React.FC<{ analyticsType: TAnalyticsTabsV2Base }> = observer(({ analyticsType }) => { + const params = useParams(); + const workspaceSlug = params.workspaceSlug as string; + const { t } = useTranslation() + const { selectedDuration, selectedProject } = useAnalyticsV2() + + const { data: totalInsightsData, isLoading } = useSWR(`total-insights-${analyticsType}-${selectedDuration}-${selectedProject}`, + () => analyticsV2Service.getAdvanceAnalytics(workspaceSlug, analyticsType, { + date_filter: selectedDuration, + ...(selectedProject ? { project_ids: selectedProject } : {}) + })) + + return ( +
+ {insightsFields[analyticsType].map((item: string) => ( + + ))} +
+ ) +}) + +export default TotalInsights; \ No newline at end of file diff --git a/web/core/components/analytics-v2/trend-piece.tsx b/web/core/components/analytics-v2/trend-piece.tsx new file mode 100644 index 00000000000..d70527dcdef --- /dev/null +++ b/web/core/components/analytics-v2/trend-piece.tsx @@ -0,0 +1,54 @@ +// plane package imports +import { cn } from '@plane/utils' +// plane web components +import { TrendingDown, TrendingUp } from 'lucide-react' +import React from 'react' + +type Props = { + percentage: number + className?: string + size?: 'xs' | 'sm' | 'md' | 'lg' +} + +const sizeConfig = { + xs: { + text: 'text-xs', + icon: 'w-3 h-3' + }, + sm: { + text: 'text-sm', + icon: 'w-4 h-4' + }, + md: { + text: 'text-base', + icon: 'w-5 h-5' + }, + lg: { + text: 'text-lg', + icon: 'w-6 h-6' + } +} as const + +const TrendPiece = (props: Props) => { + const { percentage, className, size = 'sm' } = props + const isPositive = percentage > 0 + const config = sizeConfig[size] + + return ( +
+ {isPositive ? ( + + ) : ( + + )} + {Math.abs(percentage)}% +
+ ) +} + +export default TrendPiece \ No newline at end of file diff --git a/web/core/components/analytics-v2/work-items/customized-insights.tsx b/web/core/components/analytics-v2/work-items/customized-insights.tsx new file mode 100644 index 00000000000..224d28c87c6 --- /dev/null +++ b/web/core/components/analytics-v2/work-items/customized-insights.tsx @@ -0,0 +1,32 @@ +import { BarChart } from '@plane/propel/charts/bar-chart' +import { LineChart } from '@plane/propel/charts/line-chart' +import React from 'react' +import AnalyticsSectionWrapper from '../analytics-section-wrapper' +import { overviewDummyData } from '../temp-dummy-data' +import { useAnalyticsV2 } from '@/hooks/store/use-analytics-v2' +import { observer } from 'mobx-react' + +type Props = {} + +const CustomizedInsights = observer((props: Props) => { + const { selectedDuration, selectedDurationLabel } = useAnalyticsV2() + return ( + + + + ) +}) + +export default CustomizedInsights \ No newline at end of file diff --git a/web/core/components/analytics-v2/work-items/index.ts b/web/core/components/analytics-v2/work-items/index.ts new file mode 100644 index 00000000000..c8711b96a4c --- /dev/null +++ b/web/core/components/analytics-v2/work-items/index.ts @@ -0,0 +1 @@ +export * from './root' \ No newline at end of file diff --git a/web/core/components/analytics-v2/work-items/root.tsx b/web/core/components/analytics-v2/work-items/root.tsx new file mode 100644 index 00000000000..3f839a3b450 --- /dev/null +++ b/web/core/components/analytics-v2/work-items/root.tsx @@ -0,0 +1,22 @@ +import React from 'react' +import AnalyticsWrapper from '../analytics-wrapper' +import TotalInsights from '../total-insights' +import CustomizedInsights from './customized-insights' +import WorkItemsInsightTable from './workitems-insight-table' + + +type Props = {} + +const WorkItems: React.FC = (props) => { + return ( + +
+ + + +
+
+ ) +} + +export { WorkItems } \ No newline at end of file diff --git a/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx b/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx new file mode 100644 index 00000000000..7582e133e07 --- /dev/null +++ b/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx @@ -0,0 +1,92 @@ +import { useAnalyticsV2 } from '@/hooks/store/use-analytics-v2'; +import { AnalyticsV2Service } from '@/services/analytics-v2.service'; +import { WorkItemInsightColumns, AnalyticsTableDataMap } from '@plane/types'; +import { observer } from 'mobx-react'; +import { useParams } from 'next/navigation'; +import useSWR from 'swr'; +import { InsightTable } from '../insight-table'; +import { useMemo } from 'react'; +import { ColumnDef } from '@tanstack/react-table'; +import { Logo } from '@/components/common/logo'; +import { Briefcase } from 'lucide-react'; +import { useProject } from '@/hooks/store/use-project'; + +const analyticsV2Service = new AnalyticsV2Service(); + +const WorkItemsInsightTable = observer(() => { + const params = useParams(); + const workspaceSlug = params.workspaceSlug as string; + const { getProjectById } = useProject(); + const { selectedDuration, selectedProject } = useAnalyticsV2() + const { data: workItemsData, isLoading } = useSWR(`insights-table-work-items-${selectedDuration}-${selectedProject}`, + () => analyticsV2Service.getAdvanceAnalyticsStats(workspaceSlug, "work-items", { + date_filter: selectedDuration, + ...(selectedProject ? { project_ids: selectedProject } : {}) + })) + + const columns = useMemo(() => { + return [ + { + accessorKey: "project_id", + header: () =>
Project
, + cell: ({ row }) => { + const project = getProjectById(row.original.project_id); + return
+ {project?.logo_props ? ( + + ) : ( + + )} + {project?.name} +
+ } + }, + { + accessorKey: "backlog_work_items", + header: () =>
Backlog
, + cell: ({ row }) => { + return
{row.original.backlog_work_items}
+ } + }, + { + accessorKey: "started_work_items", + header: () =>
Started
, + cell: ({ row }) => { + return
{row.original.started_work_items}
+ } + }, + { + accessorKey: "un_started_work_items", + header: () =>
Unstarted
, + cell: ({ row }) => { + return
{row.original.un_started_work_items}
+ } + }, + { + accessorKey: "completed_work_items", + header: () =>
Completed
, + cell: ({ row }) => { + return
{row.original.completed_work_items}
+ } + }, + { + accessorKey: "cancelled_work_items", + header: () =>
Cancelled
, + cell: ({ row }) => { + return
{row.original.cancelled_work_items}
+ } + } + ] as ColumnDef[] + }, [getProjectById]) + + return ( + + analyticsType="work-items" + data={workItemsData} + isLoading={isLoading} + columns={columns} + /> + ) +}) + +export default WorkItemsInsightTable \ No newline at end of file diff --git a/web/core/hooks/store/index.ts b/web/core/hooks/store/index.ts index 7f2d3b89d88..6a1b91e5d0f 100644 --- a/web/core/hooks/store/index.ts +++ b/web/core/hooks/store/index.ts @@ -31,3 +31,4 @@ export * from "./use-workspace"; export * from "./user"; export * from "./use-transient"; export * from "./workspace-draft"; +export * from "./use-analytics-v2"; diff --git a/web/core/hooks/store/use-analytics-v2.ts b/web/core/hooks/store/use-analytics-v2.ts new file mode 100644 index 00000000000..c8c13ba6130 --- /dev/null +++ b/web/core/hooks/store/use-analytics-v2.ts @@ -0,0 +1,11 @@ +import { useContext } from "react"; +// mobx store +import { StoreContext } from "@/lib/store-context"; +// types +import { IAnalyticsStoreV2 } from "@/store/analytics-v2.store"; + +export const useAnalyticsV2 = (): IAnalyticsStoreV2 => { + const context = useContext(StoreContext); + if (context === undefined) throw new Error("useAnalyticsV2 must be used within StoreProvider"); + return context.analyticsV2; +}; diff --git a/web/core/services/analytics-v2.service.ts b/web/core/services/analytics-v2.service.ts new file mode 100644 index 00000000000..e5b794c2cb7 --- /dev/null +++ b/web/core/services/analytics-v2.service.ts @@ -0,0 +1,37 @@ +import { API_BASE_URL } from "@plane/constants"; +import { APIService } from "./api.service"; +import { IAnalyticsResponseV2, TAnalyticsTabsV2Base } from "@plane/types"; + + +export class AnalyticsV2Service extends APIService { + constructor() { + super(API_BASE_URL) + } + + async getAdvanceAnalytics(workspaceSlug: string, tab: TAnalyticsTabsV2Base, params?: Record): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/advance-analytics/`, { + params: { + tab, + ...params + } + }) + .then((res) => res?.data) + .catch((err) => { + throw err?.response?.data; + }) + } + + async getAdvanceAnalyticsStats(workspaceSlug: string, tab: Exclude, params?: Record): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/advance-analytics-stats/`, { + params: { + type: tab, + ...params + } + }) + .then((res) => res?.data) + .catch((err) => { + throw err?.response?.data; + }) + } +} + diff --git a/web/core/store/analytics-v2.store.ts b/web/core/store/analytics-v2.store.ts new file mode 100644 index 00000000000..f58c2ef3bcb --- /dev/null +++ b/web/core/store/analytics-v2.store.ts @@ -0,0 +1,76 @@ +import { TAnalyticsTabsV2Base } from "@plane/types"; +import { CoreRootStore } from "./root.store"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; +import { PROJECT_CREATED_AT_FILTER_OPTIONS } from "@plane/constants"; + + +type DurationType = typeof PROJECT_CREATED_AT_FILTER_OPTIONS[number]['value'] + +export interface IAnalyticsStoreV2 { + //observables + currentTab: TAnalyticsTabsV2Base + selectedProject: string | null + selectedDuration: DurationType, + + //computed + selectedDurationLabel: string | null, + + //actions + updateSelectedProject: (project: string) => void, + updateSelectedDuration: (duration: DurationType) => void, +} + +export class AnalyticsStoreV2 implements IAnalyticsStoreV2 { + //observables + currentTab: TAnalyticsTabsV2Base = "overview"; + selectedProject: string | null = null; + selectedDuration: typeof PROJECT_CREATED_AT_FILTER_OPTIONS[number]['value'] = "today;custom;custom"; + + constructor(_rootStore: CoreRootStore) { + makeObservable(this, { + // observables + currentTab: observable, + selectedDuration: observable, + selectedProject: observable, + // computed + selectedProjectLabel: computed, + selectedDurationLabel: computed, + // actions + updateSelectedProject: action, + updateSelectedDuration: action + }) + } + + get selectedProjectLabel() { // TODO: get the project label from the project id + return "All Projects" + } + + + get selectedDurationLabel() { + return PROJECT_CREATED_AT_FILTER_OPTIONS.find(item => item.value === this.selectedDuration)?.name ?? null + } + + updateSelectedProject = (project: string) => { + const initialState = this.selectedProject; + try { + runInAction(() => { + this.selectedProject = project; + }) + } catch (error) { + console.error("Failed to update selected project"); + throw error; + } + } + + updateSelectedDuration = (duration: DurationType) => { + const initialState = this.selectedDuration; + try { + runInAction(() => { + this.selectedDuration = duration; + }) + } catch (error) { + console.error("Failed to update selected duration"); + throw error; + } + } +} \ No newline at end of file diff --git a/web/core/store/root.store.ts b/web/core/store/root.store.ts index d06ed2418d0..2e80579e4f9 100644 --- a/web/core/store/root.store.ts +++ b/web/core/store/root.store.ts @@ -32,6 +32,7 @@ import { ThemeStore, IThemeStore } from "./theme.store"; import { ITransientStore, TransientStore } from "./transient.store"; import { IUserStore, UserStore } from "./user"; import { IWorkspaceRootStore, WorkspaceRootStore } from "./workspace"; +import { IAnalyticsStoreV2, AnalyticsStoreV2 } from "./analytics-v2.store"; enableStaticRendering(typeof window === "undefined"); @@ -49,6 +50,7 @@ export class CoreRootStore { state: IStateStore; label: ILabelStore; dashboard: IDashboardStore; + analyticsV2: IAnalyticsStoreV2; projectPages: IProjectPageStore; router: IRouterStore; commandPalette: ICommandPaletteStore; @@ -94,6 +96,7 @@ export class CoreRootStore { this.transient = new TransientStore(); this.stickyStore = new StickyStore(); this.editorAssetStore = new EditorAssetStore(); + this.analyticsV2 = new AnalyticsStoreV2(this); } resetOnSignOut() { diff --git a/web/package.json b/web/package.json index c04564b3023..6de24e8790f 100644 --- a/web/package.json +++ b/web/package.json @@ -39,6 +39,7 @@ "@plane/utils": "*", "@popperjs/core": "^2.11.8", "@react-pdf/renderer": "^3.4.5", + "@tanstack/react-table": "^8.21.3", "axios": "^1.8.3", "clsx": "^2.0.0", "cmdk": "^1.0.0", diff --git a/yarn.lock b/yarn.lock index 4063af1c44e..af87bb81c54 100644 --- a/yarn.lock +++ b/yarn.lock @@ -257,7 +257,7 @@ "@babel/traverse" "^7.25.9" "@babel/types" "^7.25.9" -"@babel/helpers@7.26.10", "@babel/helpers@^7.26.7": +"@babel/helpers@^7.26.7": version "7.26.10" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.26.10.tgz#6baea3cd62ec2d0c1068778d63cb1314f6637384" integrity sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g== @@ -852,7 +852,7 @@ "@babel/plugin-transform-modules-commonjs" "^7.25.9" "@babel/plugin-transform-typescript" "^7.25.9" -"@babel/runtime@7.26.10", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.13", "@babel/runtime@^7.23.9", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.13", "@babel/runtime@^7.23.9", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": version "7.26.10" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.10.tgz#a07b4d8fa27af131a633d7b3524db803eb4764c2" integrity sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw== @@ -1124,126 +1124,251 @@ resolved "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz#5e13fac887f08c44f76b0ccaf3370eb00fec9bb6" integrity sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg== +"@esbuild/aix-ppc64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz#38848d3e25afe842a7943643cbcd387cc6e13461" + integrity sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA== + "@esbuild/aix-ppc64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz#499600c5e1757a524990d5d92601f0ac3ce87f64" integrity sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ== +"@esbuild/android-arm64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz#f592957ae8b5643129fa889c79e69cd8669bb894" + integrity sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg== + "@esbuild/android-arm64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz#b9b8231561a1dfb94eb31f4ee056b92a985c324f" integrity sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g== +"@esbuild/android-arm@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.24.2.tgz#72d8a2063aa630308af486a7e5cbcd1e134335b3" + integrity sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q== + "@esbuild/android-arm@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.0.tgz#ca6e7888942505f13e88ac9f5f7d2a72f9facd2b" integrity sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g== +"@esbuild/android-x64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.24.2.tgz#9a7713504d5f04792f33be9c197a882b2d88febb" + integrity sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw== + "@esbuild/android-x64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.0.tgz#e765ea753bac442dfc9cb53652ce8bd39d33e163" integrity sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg== +"@esbuild/darwin-arm64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz#02ae04ad8ebffd6e2ea096181b3366816b2b5936" + integrity sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA== + "@esbuild/darwin-arm64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz#fa394164b0d89d4fdc3a8a21989af70ef579fa2c" integrity sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw== +"@esbuild/darwin-x64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz#9ec312bc29c60e1b6cecadc82bd504d8adaa19e9" + integrity sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA== + "@esbuild/darwin-x64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz#91979d98d30ba6e7d69b22c617cc82bdad60e47a" integrity sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg== +"@esbuild/freebsd-arm64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz#5e82f44cb4906d6aebf24497d6a068cfc152fa00" + integrity sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg== + "@esbuild/freebsd-arm64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz#b97e97073310736b430a07b099d837084b85e9ce" integrity sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w== +"@esbuild/freebsd-x64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz#3fb1ce92f276168b75074b4e51aa0d8141ecce7f" + integrity sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q== + "@esbuild/freebsd-x64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz#f3b694d0da61d9910ec7deff794d444cfbf3b6e7" integrity sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A== +"@esbuild/linux-arm64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz#856b632d79eb80aec0864381efd29de8fd0b1f43" + integrity sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg== + "@esbuild/linux-arm64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz#f921f699f162f332036d5657cad9036f7a993f73" integrity sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg== +"@esbuild/linux-arm@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz#c846b4694dc5a75d1444f52257ccc5659021b736" + integrity sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA== + "@esbuild/linux-arm@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz#cc49305b3c6da317c900688995a4050e6cc91ca3" integrity sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg== +"@esbuild/linux-ia32@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz#f8a16615a78826ccbb6566fab9a9606cfd4a37d5" + integrity sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw== + "@esbuild/linux-ia32@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz#3e0736fcfab16cff042dec806247e2c76e109e19" integrity sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg== +"@esbuild/linux-loong64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz#1c451538c765bf14913512c76ed8a351e18b09fc" + integrity sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ== + "@esbuild/linux-loong64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz#ea2bf730883cddb9dfb85124232b5a875b8020c7" integrity sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw== +"@esbuild/linux-mips64el@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz#0846edeefbc3d8d50645c51869cc64401d9239cb" + integrity sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw== + "@esbuild/linux-mips64el@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz#4cababb14eede09248980a2d2d8b966464294ff1" integrity sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ== +"@esbuild/linux-ppc64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz#8e3fc54505671d193337a36dfd4c1a23b8a41412" + integrity sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw== + "@esbuild/linux-ppc64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz#8860a4609914c065373a77242e985179658e1951" integrity sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw== +"@esbuild/linux-riscv64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz#6a1e92096d5e68f7bb10a0d64bb5b6d1daf9a694" + integrity sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q== + "@esbuild/linux-riscv64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz#baf26e20bb2d38cfb86ee282dff840c04f4ed987" integrity sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA== +"@esbuild/linux-s390x@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz#ab18e56e66f7a3c49cb97d337cd0a6fea28a8577" + integrity sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw== + "@esbuild/linux-s390x@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz#8323afc0d6cb1b6dc6e9fd21efd9e1542c3640a4" integrity sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA== +"@esbuild/linux-x64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz#8140c9b40da634d380b0b29c837a0b4267aff38f" + integrity sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q== + "@esbuild/linux-x64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz#08fcf60cb400ed2382e9f8e0f5590bac8810469a" integrity sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw== +"@esbuild/netbsd-arm64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz#65f19161432bafb3981f5f20a7ff45abb2e708e6" + integrity sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw== + "@esbuild/netbsd-arm64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz#935c6c74e20f7224918fbe2e6c6fe865b6c6ea5b" integrity sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw== +"@esbuild/netbsd-x64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz#7a3a97d77abfd11765a72f1c6f9b18f5396bcc40" + integrity sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw== + "@esbuild/netbsd-x64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz#414677cef66d16c5a4d210751eb2881bb9c1b62b" integrity sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA== +"@esbuild/openbsd-arm64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz#58b00238dd8f123bfff68d3acc53a6ee369af89f" + integrity sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A== + "@esbuild/openbsd-arm64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz#8fd55a4d08d25cdc572844f13c88d678c84d13f7" integrity sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw== +"@esbuild/openbsd-x64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz#0ac843fda0feb85a93e288842936c21a00a8a205" + integrity sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA== + "@esbuild/openbsd-x64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz#0c48ddb1494bbc2d6bcbaa1429a7f465fa1dedde" integrity sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg== +"@esbuild/sunos-x64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz#8b7aa895e07828d36c422a4404cc2ecf27fb15c6" + integrity sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig== + "@esbuild/sunos-x64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz#86ff9075d77962b60dd26203d7352f92684c8c92" integrity sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg== +"@esbuild/win32-arm64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz#c023afb647cabf0c3ed13f0eddfc4f1d61c66a85" + integrity sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ== + "@esbuild/win32-arm64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz#849c62327c3229467f5b5cd681bf50588442e96c" integrity sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw== +"@esbuild/win32-ia32@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz#96c356132d2dda990098c8b8b951209c3cd743c2" + integrity sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA== + "@esbuild/win32-ia32@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz#f62eb480cd7cca088cb65bb46a6db25b725dc079" integrity sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA== +"@esbuild/win32-x64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz#34aa0b52d0fbb1a654b596acfa595f0c7b77a77b" + integrity sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg== + "@esbuild/win32-x64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz#c8e119a30a7c8d60b9d2e22d2073722dde3b710b" @@ -2775,6 +2900,13 @@ lodash.merge "^4.6.2" postcss-selector-parser "6.0.10" +"@tanstack/react-table@^8.21.3": + version "8.21.3" + resolved "https://registry.yarnpkg.com/@tanstack/react-table/-/react-table-8.21.3.tgz#2c38c747a5731c1a07174fda764b9c2b1fb5e91b" + integrity sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww== + dependencies: + "@tanstack/table-core" "8.21.3" + "@tanstack/react-virtual@^3.0.0-beta.60": version "3.13.0" resolved "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.0.tgz#f50bccdfbb792cb11fdc0342fd3ec6945c730389" @@ -2782,6 +2914,11 @@ dependencies: "@tanstack/virtual-core" "3.13.0" +"@tanstack/table-core@8.21.3": + version "8.21.3" + resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.21.3.tgz#2977727d8fc8dfa079112d9f4d4c019110f1732c" + integrity sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg== + "@tanstack/virtual-core@3.13.0": version "3.13.0" resolved "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.0.tgz#8db0ccc9d6c32b6393551a6d19c87dbb259a8828" @@ -5918,7 +6055,38 @@ esbuild-register@^3.5.0: dependencies: debug "^4.3.4" -esbuild@0.25.0, "esbuild@^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0", esbuild@^0.25.0: +"esbuild@^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0": + version "0.24.2" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.24.2.tgz#b5b55bee7de017bff5fb8a4e3e44f2ebe2c3567d" + integrity sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA== + optionalDependencies: + "@esbuild/aix-ppc64" "0.24.2" + "@esbuild/android-arm" "0.24.2" + "@esbuild/android-arm64" "0.24.2" + "@esbuild/android-x64" "0.24.2" + "@esbuild/darwin-arm64" "0.24.2" + "@esbuild/darwin-x64" "0.24.2" + "@esbuild/freebsd-arm64" "0.24.2" + "@esbuild/freebsd-x64" "0.24.2" + "@esbuild/linux-arm" "0.24.2" + "@esbuild/linux-arm64" "0.24.2" + "@esbuild/linux-ia32" "0.24.2" + "@esbuild/linux-loong64" "0.24.2" + "@esbuild/linux-mips64el" "0.24.2" + "@esbuild/linux-ppc64" "0.24.2" + "@esbuild/linux-riscv64" "0.24.2" + "@esbuild/linux-s390x" "0.24.2" + "@esbuild/linux-x64" "0.24.2" + "@esbuild/netbsd-arm64" "0.24.2" + "@esbuild/netbsd-x64" "0.24.2" + "@esbuild/openbsd-arm64" "0.24.2" + "@esbuild/openbsd-x64" "0.24.2" + "@esbuild/sunos-x64" "0.24.2" + "@esbuild/win32-arm64" "0.24.2" + "@esbuild/win32-ia32" "0.24.2" + "@esbuild/win32-x64" "0.24.2" + +esbuild@^0.25.0: version "0.25.0" resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.0.tgz#0de1787a77206c5a79eeb634a623d39b5006ce92" integrity sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw== @@ -8379,7 +8547,7 @@ mz@^2.7.0: object-assign "^4.0.1" thenify-all "^1.0.0" -nanoid@3.3.8, nanoid@^3.3.6, nanoid@^3.3.8: +nanoid@^3.3.6, nanoid@^3.3.8: version "3.3.8" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf" integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w== From 0266a0572edef38b7181fa32e9ae955bba61c6b3 Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Fri, 25 Apr 2025 16:22:33 +0530 Subject: [PATCH 03/69] updated status icons --- packages/ui/src/icons/at-risk-icon.tsx | 21 ++++---------- packages/ui/src/icons/off-track-icon.tsx | 21 ++++---------- packages/ui/src/icons/on-track-icon.tsx | 36 ++++++++++-------------- 3 files changed, 27 insertions(+), 51 deletions(-) diff --git a/packages/ui/src/icons/at-risk-icon.tsx b/packages/ui/src/icons/at-risk-icon.tsx index bb4437e6d9b..65e5ae63d60 100644 --- a/packages/ui/src/icons/at-risk-icon.tsx +++ b/packages/ui/src/icons/at-risk-icon.tsx @@ -3,27 +3,18 @@ import * as React from "react"; import { ISvgIcons } from "./type"; export const AtRiskIcon: React.FC = ({ width = "16", height = "16" }) => ( - - - - + + - - + + diff --git a/packages/ui/src/icons/off-track-icon.tsx b/packages/ui/src/icons/off-track-icon.tsx index 0d93d1b6057..cbb8ba1f868 100644 --- a/packages/ui/src/icons/off-track-icon.tsx +++ b/packages/ui/src/icons/off-track-icon.tsx @@ -3,27 +3,18 @@ import * as React from "react"; import { ISvgIcons } from "./type"; export const OffTrackIcon: React.FC = ({ width = "16", height = "16" }) => ( - - - - + + - - + + diff --git a/packages/ui/src/icons/on-track-icon.tsx b/packages/ui/src/icons/on-track-icon.tsx index c384d4c8d76..5dcabcec956 100644 --- a/packages/ui/src/icons/on-track-icon.tsx +++ b/packages/ui/src/icons/on-track-icon.tsx @@ -3,45 +3,39 @@ import * as React from "react"; import { ISvgIcons } from "./type"; export const OnTrackIcon: React.FC = ({ width = "16", height = "16" }) => ( - - - - + + - - + + From d8536d179c083af2447cbfc616d3641de9a4a14b Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Mon, 28 Apr 2025 14:29:24 +0530 Subject: [PATCH 04/69] added area chart in workitems and en translations --- .../i18n/src/locales/en/translations.json | 69 ++++++------------- .../propel/src/charts/area-chart/root.tsx | 7 +- .../analytics-v2/overview/active-projects.tsx | 6 +- .../overview/project-insights.tsx | 27 ++++++-- .../analytics-v2/temp-dummy-data.ts | 29 ++++++++ .../work-items/created-vs-resolved.tsx | 47 +++++++++++++ .../work-items/customized-insights.tsx | 19 ++++- .../analytics-v2/work-items/root.tsx | 2 + web/core/services/analytics-v2.service.ts | 13 ++++ 9 files changed, 157 insertions(+), 62 deletions(-) create mode 100644 web/core/components/analytics-v2/work-items/created-vs-resolved.tsx diff --git a/packages/i18n/src/locales/en/translations.json b/packages/i18n/src/locales/en/translations.json index d8b3c5c9331..15d34564093 100644 --- a/packages/i18n/src/locales/en/translations.json +++ b/packages/i18n/src/locales/en/translations.json @@ -334,7 +334,6 @@ "new_password_must_be_different_from_old_password": "New password must be different from old password", "edited": "edited", "bot": "Bot", - "project_view": { "sort_by": { "created_at": "Created at", @@ -342,12 +341,10 @@ "name": "Name" } }, - "toast": { "success": "Success!", "error": "Error!" }, - "links": { "toasts": { "created": { @@ -376,7 +373,6 @@ } } }, - "home": { "empty": { "quickstart_guide": "Your quickstart guide", @@ -444,7 +440,6 @@ "title": "Home", "star_us_on_github": "Star us on GitHub" }, - "link": { "modal": { "url": { @@ -458,7 +453,6 @@ } } }, - "common": { "all": "All", "states": "States", @@ -703,22 +697,20 @@ "pending": "Pending", "invite": "Invite", "view": "View", - "deactivated_user": "Deactivated user" + "deactivated_user": "Deactivated user", + "overview": "Overview" }, - "chart": { "x_axis": "X-axis", "y_axis": "Y-axis", "metric": "Metric" }, - "form": { "title": { "required": "Title is required", "max_length": "Title should be less than {length} characters" } }, - "entity": { "grouping_title": "{entity} Grouping", "priority": "{entity} Priority", @@ -742,7 +734,6 @@ "failed": "Error adding {entity}" } }, - "epic": { "all": "All Epics", "label": "{count, plural, one {Epic} other {Epics}}", @@ -760,7 +751,6 @@ "required": "Epic title is required." } }, - "issue": { "label": "{count, plural, one {Work item} other {Work items}}", "all": "All Work items", @@ -927,7 +917,6 @@ }, "open_in_full_screen": "Open work item in full screen" }, - "attachment": { "error": "File could not be attached. Try uploading again.", "only_one_file_allowed": "Only one file can be uploaded at a time.", @@ -935,7 +924,6 @@ "drag_and_drop": "Drag and drop anywhere to upload", "delete": "Delete attachment" }, - "label": { "select": "Select label", "create": { @@ -945,7 +933,6 @@ "type": "Type to add a new label" } }, - "sub_work_item": { "update": { "success": "Sub-work item updated successfully", @@ -956,7 +943,6 @@ "error": "Error removing sub-work item" } }, - "view": { "label": "{count, plural, one {View} other {Views}}", "create": { @@ -966,7 +952,6 @@ "label": "Update View" } }, - "inbox_issue": { "status": { "pending": { @@ -1052,7 +1037,6 @@ } } }, - "workspace_creation": { "heading": "Create your workspace", "subheading": "To start using Plane, you need to create or join a workspace.", @@ -1104,7 +1088,6 @@ } } }, - "workspace_dashboard": { "empty_state": { "general": { @@ -1120,7 +1103,6 @@ } } }, - "workspace_analytics": { "label": "Analytics", "page_label": "{workspace} - Analytics", @@ -1163,9 +1145,25 @@ } } } - } + }, + "total_work_items": "Total work items", + "started_work_items": "Started work items", + "backlog_work_items": "Backlog work items", + "un_started_work_items": "Un started work items", + "completed_work_items": "Completed work items", + "total_guests": "Total Guests", + "total_intake": "Total Intake", + "total_users": "Total Users", + "total_admins": "Total Admins", + "total_projects": "Total Projects", + "project_insights": "Project Insights", + "summary_of_projects": "Summary of Projects", + "all_projects": "All Projects", + "trend_on_charts": "Trend on charts", + "active_projects": "Active Projects", + "customized_insights": "Customized Insights", + "created_vs_resolved": "Created vs Resolved" }, - "workspace_projects": { "label": "{count, plural, one {Project} other {Projects}}", "create": { @@ -1240,7 +1238,6 @@ } } }, - "workspace_views": { "add_view": "Add view", "empty_state": { @@ -1275,7 +1272,6 @@ } } }, - "workspace_settings": { "label": "Workspace settings", "page_label": "{workspace} - General settings", @@ -1457,7 +1453,6 @@ } } }, - "profile": { "label": "Profile", "page_label": "Your work", @@ -1520,7 +1515,6 @@ } } }, - "project_settings": { "general": { "enter_project_id": "Enter project ID", @@ -1666,7 +1660,6 @@ "auto_close_status": "Auto-close status" } }, - "empty_state": { "labels": { "title": "No labels yet", @@ -1679,7 +1672,6 @@ } } }, - "project_cycles": { "add_cycle": "Add cycle", "more_details": "More details", @@ -1805,7 +1797,6 @@ } } }, - "project_issues": { "empty_state": { "no_issues": { @@ -1834,7 +1825,6 @@ } } }, - "project_module": { "add_module": "Add Module", "update_module": "Update Module", @@ -1888,7 +1878,6 @@ } } }, - "project_views": { "empty_state": { "general": { @@ -1908,7 +1897,6 @@ } } }, - "project_page": { "empty_state": { "general": { @@ -1938,7 +1926,6 @@ } } }, - "command_k": { "empty_state": { "search": { @@ -1946,7 +1933,6 @@ } } }, - "issue_relation": { "empty_state": { "search": { @@ -1957,7 +1943,6 @@ } } }, - "issue_comment": { "empty_state": { "general": { @@ -1966,7 +1951,6 @@ } } }, - "notification": { "label": "Inbox", "page_label": "{workspace} - Inbox", @@ -2023,7 +2007,6 @@ "custom": "Custom" } }, - "active_cycle": { "empty_state": { "progress": { @@ -2043,7 +2026,6 @@ } } }, - "disabled_project": { "empty_state": { "inbox": { @@ -2106,7 +2088,6 @@ } } }, - "stickies": { "title": "Your stickies", "placeholder": "click to type here", @@ -2164,7 +2145,6 @@ } } }, - "role_details": { "guest": { "title": "Guest", @@ -2179,7 +2159,6 @@ "description": "All permissions set to true within the workspace." } }, - "user_roles": { "product_or_project_manager": "Product / Project Manager", "development_or_engineering": "Development / Engineering", @@ -2192,7 +2171,6 @@ "human_resources": "Human / Resources", "other": "Other" }, - "importer": { "github": { "title": "Github", @@ -2203,7 +2181,6 @@ "description": "Import work items and epics from Jira projects and epics." } }, - "exporter": { "csv": { "title": "CSV", @@ -2232,7 +2209,6 @@ "created": "Created", "subscribed": "Subscribed" }, - "themes": { "theme_options": { "system_preference": { @@ -2278,20 +2254,17 @@ "manual": "Manual" } }, - "cycle": { "label": "{count, plural, one {Cycle} other {Cycles}}", "no_cycle": "No cycle" }, - "module": { "label": "{count, plural, one {Module} other {Modules}}", "no_module": "No module" }, - "description_versions": { "last_edited_by": "Last edited by", "previously_edited_by": "Previously edited by", "edited_by": "Edited by" } -} +} \ No newline at end of file diff --git a/packages/propel/src/charts/area-chart/root.tsx b/packages/propel/src/charts/area-chart/root.tsx index 7d4e9e6ba97..33379fa5606 100644 --- a/packages/propel/src/charts/area-chart/root.tsx +++ b/packages/propel/src/charts/area-chart/root.tsx @@ -55,9 +55,9 @@ export const AreaChart = React.memo((props: dot={ area.showDot ? { - fill: area.fill, - fillOpacity: 1, - } + fill: area.fill, + fillOpacity: 1, + } : false } activeDot={{ @@ -91,7 +91,6 @@ export const AreaChart = React.memo((props: }; }); }, [data, xAxis.key]); - return (
diff --git a/web/core/components/analytics-v2/overview/active-projects.tsx b/web/core/components/analytics-v2/overview/active-projects.tsx index 09764005a45..59c8edfd365 100644 --- a/web/core/components/analytics-v2/overview/active-projects.tsx +++ b/web/core/components/analytics-v2/overview/active-projects.tsx @@ -3,13 +3,13 @@ import AnalyticsSectionWrapper from '../analytics-section-wrapper' import { overviewDummyData } from '../temp-dummy-data' import ActiveProjectItem from './active-project-item' import { EUpdateStatus } from '@plane/types/src/enums' - +import { useTranslation } from '@plane/i18n' type Props = {} const ActiveProjects = (props: Props) => { - + const { t } = useTranslation() return ( - +
{overviewDummyData.active_projects.map((project) => ( diff --git a/web/core/components/analytics-v2/overview/project-insights.tsx b/web/core/components/analytics-v2/overview/project-insights.tsx index 9692406ba48..38998c226fd 100644 --- a/web/core/components/analytics-v2/overview/project-insights.tsx +++ b/web/core/components/analytics-v2/overview/project-insights.tsx @@ -6,6 +6,11 @@ import TrendPiece from '../trend-piece' import { useAnalyticsV2 } from '@/hooks/store/use-analytics-v2' import { PROJECT_CREATED_AT_FILTER_OPTIONS } from '@plane/constants' import { observer } from 'mobx-react' +import { AnalyticsV2Service } from '@/services/analytics-v2.service' +import useSWR from 'swr' +import { useParams } from 'next/navigation' +import { useTranslation } from '@plane/i18n' + const RadarChart = dynamic(() => import("@plane/propel/charts/radar-chart").then((mod) => ({ default: mod.RadarChart, @@ -13,13 +18,25 @@ const RadarChart = dynamic(() => ) type Props = {} +const analyticsV2Service = new AnalyticsV2Service() const ProjectInsights = observer((props: Props) => { + const params = useParams(); + const { t } = useTranslation() + const workspaceSlug = params.workspaceSlug as string; const { selectedProject, selectedDuration } = useAnalyticsV2() const selectedDurationLabel = useMemo(() => PROJECT_CREATED_AT_FILTER_OPTIONS.find(item => item.value === selectedDuration)?.name, [selectedDuration]) + + const { data: projectInsightsData } = useSWR( + analyticsV2Service.getAdvanceAnalyticsCharts(workspaceSlug, 'overview', { + created_at: selectedDuration + }), + analyticsV2Service.getAdvanceAnalyticsCharts + ) + return (
- + { /> -
Summary of projects
-
All Projects
+
{t('workspace_analytics.summary_of_projects')}
+
{t('workspace_analytics.all_projects')}
-
Trend on charts
-
Work items
+
{t('workspace_analytics.trend_on_charts')}
+
{t('common.work_items')}
{overviewDummyData.graph_data.map((item) => (
diff --git a/web/core/components/analytics-v2/temp-dummy-data.ts b/web/core/components/analytics-v2/temp-dummy-data.ts index b1eef6b810a..66851ed9c51 100644 --- a/web/core/components/analytics-v2/temp-dummy-data.ts +++ b/web/core/components/analytics-v2/temp-dummy-data.ts @@ -264,6 +264,35 @@ export const overviewDummyData = { { states: "Low", low: 35 }, { states: "None", none: 70 } ] as TChartData[], + work_areas: [ + { + key: "resolved", + label: "Resolved", + fill: "#19803833", + fillOpacity: 1, + stackId: "bar-one", + showDot: false, + smoothCurves: true, + strokeColor: "#198038", + strokeOpacity: 1, + }, + { + key: "unresolved", + label: "Unresolved", + fill: "#1192E833", + fillOpacity: 1, + stackId: "bar-one", + showDot: false, + smoothCurves: true, + strokeColor: "#1192E8", + strokeOpacity: 1, + }, + + ], + work_area_data: Array.from({ length: 10 }, (_, i) => ({ + resolved: Math.floor(Math.random() * 20), + unresolved: Math.floor(Math.random() * 20), + })), sampleScatterData: { data: [ { x: 10, y: 20, z: 5, "epics": 15, "dashboard": 10 }, diff --git a/web/core/components/analytics-v2/work-items/created-vs-resolved.tsx b/web/core/components/analytics-v2/work-items/created-vs-resolved.tsx new file mode 100644 index 00000000000..5cafe675fe1 --- /dev/null +++ b/web/core/components/analytics-v2/work-items/created-vs-resolved.tsx @@ -0,0 +1,47 @@ +import { BarChart } from '@plane/propel/charts/bar-chart' +import { LineChart } from '@plane/propel/charts/line-chart' +import React from 'react' +import AnalyticsSectionWrapper from '../analytics-section-wrapper' +import { overviewDummyData } from '../temp-dummy-data' +import { useAnalyticsV2 } from '@/hooks/store/use-analytics-v2' +import { observer } from 'mobx-react' +import useSWR from 'swr' +import { AnalyticsV2Service } from '@/services/analytics-v2.service' +import { useParams } from 'next/navigation' +import { AreaChart } from '@plane/propel/charts/area-chart' +import { useTranslation } from '@plane/i18n' + +type Props = {} +const analyticsV2Service = new AnalyticsV2Service() +const CreatedVsResolved = observer((props: Props) => { + const { selectedDuration, selectedDurationLabel } = useAnalyticsV2() + const params = useParams(); + const { t } = useTranslation() + const workspaceSlug = params.workspaceSlug as string; + const { data: customizedInsightsChartData } = useSWR( + analyticsV2Service.getAdvanceAnalyticsCharts(workspaceSlug, 'work-items', { + created_at: selectedDuration + }), + analyticsV2Service.getAdvanceAnalyticsCharts + ) + + return ( + + + + ) +}) + +export default CreatedVsResolved \ No newline at end of file diff --git a/web/core/components/analytics-v2/work-items/customized-insights.tsx b/web/core/components/analytics-v2/work-items/customized-insights.tsx index 224d28c87c6..5a05992192d 100644 --- a/web/core/components/analytics-v2/work-items/customized-insights.tsx +++ b/web/core/components/analytics-v2/work-items/customized-insights.tsx @@ -5,13 +5,28 @@ import AnalyticsSectionWrapper from '../analytics-section-wrapper' import { overviewDummyData } from '../temp-dummy-data' import { useAnalyticsV2 } from '@/hooks/store/use-analytics-v2' import { observer } from 'mobx-react' +import useSWR from 'swr' +import { AnalyticsV2Service } from '@/services/analytics-v2.service' +import { useParams } from 'next/navigation' +import { AreaChart } from '@plane/propel/charts/area-chart' +import { useTranslation } from '@plane/i18n' type Props = {} - +const analyticsV2Service = new AnalyticsV2Service() const CustomizedInsights = observer((props: Props) => { const { selectedDuration, selectedDurationLabel } = useAnalyticsV2() + const params = useParams(); + const { t } = useTranslation() + const workspaceSlug = params.workspaceSlug as string; + const { data: customizedInsightsChartData } = useSWR( + analyticsV2Service.getAdvanceAnalyticsCharts(workspaceSlug, 'work-items', { + created_at: selectedDuration + }), + analyticsV2Service.getAdvanceAnalyticsCharts + ) + return ( - + = (props) => {
+
diff --git a/web/core/services/analytics-v2.service.ts b/web/core/services/analytics-v2.service.ts index e5b794c2cb7..89dc89209fa 100644 --- a/web/core/services/analytics-v2.service.ts +++ b/web/core/services/analytics-v2.service.ts @@ -33,5 +33,18 @@ export class AnalyticsV2Service extends APIService { throw err?.response?.data; }) } + + async getAdvanceAnalyticsCharts(workspaceSlug: string, tab: TAnalyticsTabsV2Base, params?: Record): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/advance-analytics-charts/`, { + params: { + type: tab, + ...params + } + }) + .then((res) => res?.data) + .catch((err) => { + throw err?.response?.data; + }) + } } From 3b254b35666cba3ba377f0265250d6aeb83d8a61 Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Mon, 28 Apr 2025 20:03:45 +0530 Subject: [PATCH 05/69] active projects --- packages/propel/src/table/index.ts | 3 +- web/app/profile/sidebar.tsx | 35 +++++-------- .../insight-table}/data-table.tsx | 22 ++++++-- .../analytics-v2/insight-table/root.tsx | 3 +- .../overview/active-project-item.tsx | 51 ++++++++++++------- .../analytics-v2/overview/active-projects.tsx | 26 +++++++--- .../work-items/workitems-insight-table.tsx | 2 +- 7 files changed, 88 insertions(+), 54 deletions(-) rename {packages/propel/src/table => web/core/components/analytics-v2/insight-table}/data-table.tsx (83%) diff --git a/packages/propel/src/table/index.ts b/packages/propel/src/table/index.ts index b3170e296c6..8b83d73fe97 100644 --- a/packages/propel/src/table/index.ts +++ b/packages/propel/src/table/index.ts @@ -1,2 +1 @@ -export * from "./core"; -export * from "./data-table"; \ No newline at end of file +export * from "./core"; \ No newline at end of file diff --git a/web/app/profile/sidebar.tsx b/web/app/profile/sidebar.tsx index 59e3daa4855..a92ff8343a3 100644 --- a/web/app/profile/sidebar.tsx +++ b/web/app/profile/sidebar.tsx @@ -134,9 +134,8 @@ export const ProfileLayoutSidebar = observer(() => {
@@ -195,20 +194,17 @@ export const ProfileLayoutSidebar = observer(() => { {workspace?.logo_url && workspace.logo_url !== "" ? ( { isMobile={isMobile} >
{} {!sidebarCollapsed && t(link.i18n_label)} @@ -253,9 +248,8 @@ export const ProfileLayoutSidebar = observer(() => {
+); diff --git a/web/core/components/analytics-v2/overview/active-project-item.tsx b/web/core/components/analytics-v2/overview/active-project-item.tsx index c2dd8be4d41..b904cefacc3 100644 --- a/web/core/components/analytics-v2/overview/active-project-item.tsx +++ b/web/core/components/analytics-v2/overview/active-project-item.tsx @@ -1,7 +1,7 @@ -import { useProject } from '@/hooks/store'; +import { Briefcase } from 'lucide-react'; import { Loader, Logo } from '@plane/ui'; import { cn } from '@plane/utils'; -import { Briefcase } from 'lucide-react'; +import { useProject } from '@/hooks/store'; type Props = { diff --git a/web/core/components/analytics-v2/overview/root.tsx b/web/core/components/analytics-v2/overview/root.tsx index 5931c6dfed8..d44508bfbb6 100644 --- a/web/core/components/analytics-v2/overview/root.tsx +++ b/web/core/components/analytics-v2/overview/root.tsx @@ -1,23 +1,22 @@ import React from 'react' import AnalyticsWrapper from '../analytics-wrapper' import TotalInsights from '../total-insights' -import ProjectInsights from './project-insights' import ActiveProjects from "./active-projects" +import ProjectInsights from './project-insights' -type Props = {} -const Overview: React.FC = (props) => { - return ( - -
- -
- - -
+ +const Overview: React.FC = () => ( + +
+ +
+ +
- - ) -} +
+
+) + export { Overview } \ No newline at end of file diff --git a/web/core/components/analytics-v2/select/analytics-params.tsx b/web/core/components/analytics-v2/select/analytics-params.tsx index b700fcfa01f..55e4b954cf3 100644 --- a/web/core/components/analytics-v2/select/analytics-params.tsx +++ b/web/core/components/analytics-v2/select/analytics-params.tsx @@ -2,7 +2,7 @@ import { observer } from "mobx-react"; import { Control, Controller } from "react-hook-form"; // plane imports import { ANALYTICS_X_AXIS_VALUES, ChartXAxisProperty, ChartYAxisMetric } from "@plane/constants"; -import { IAnalyticsV2Params, TBarItem } from "@plane/types"; +import { IAnalyticsV2Params } from "@plane/types"; import { Row } from "@plane/ui"; // components import { SelectXAxis } from "./select-x-axis"; diff --git a/web/core/components/analytics-v2/total-insights.tsx b/web/core/components/analytics-v2/total-insights.tsx index c9ae5fe064e..319ed101710 100644 --- a/web/core/components/analytics-v2/total-insights.tsx +++ b/web/core/components/analytics-v2/total-insights.tsx @@ -1,17 +1,17 @@ // plane package imports +import { observer } from 'mobx-react-lite'; +import { useParams } from 'next/navigation'; +import useSWR from 'swr'; +import { insightsFields } from '@plane/constants'; import { useTranslation } from '@plane/i18n'; import { IAnalyticsResponseV2, TAnalyticsTabsV2Base } from '@plane/types'; -import useSWR from 'swr'; //hooks import { useAnalyticsV2 } from '@/hooks/store/use-analytics-v2'; //services import { AnalyticsV2Service } from '@/services/analytics-v2.service'; // plane web components import InsightCard from './insight-card'; -import { observer } from 'mobx-react-lite'; -import { useParams } from 'next/navigation'; -import { insightsFields } from '@plane/constants'; const analyticsV2Service = new AnalyticsV2Service(); diff --git a/web/core/components/analytics-v2/trend-piece.tsx b/web/core/components/analytics-v2/trend-piece.tsx index d70527dcdef..d5f3181b968 100644 --- a/web/core/components/analytics-v2/trend-piece.tsx +++ b/web/core/components/analytics-v2/trend-piece.tsx @@ -1,8 +1,8 @@ // plane package imports +import React from 'react' +import { TrendingDown, TrendingUp } from 'lucide-react' import { cn } from '@plane/utils' // plane web components -import { TrendingDown, TrendingUp } from 'lucide-react' -import React from 'react' type Props = { percentage: number diff --git a/web/core/components/analytics-v2/utils.ts b/web/core/components/analytics-v2/utils.ts deleted file mode 100644 index 114ba28d674..00000000000 --- a/web/core/components/analytics-v2/utils.ts +++ /dev/null @@ -1,42 +0,0 @@ -export const priorityBars = [ - { - key: "urgent", - label: "Urgent", - name: "Urgent", - fill: "#DC3545", // Red color for urgent - stackId: "bar-one", - textClassName: "text-custom-text-200", - }, - { - key: "high", - label: "High", - name: "High", - fill: "#FF6B00", // Orange color for high - stackId: "bar-one", - textClassName: "text-custom-text-200", - }, - { - key: "medium", - label: "Medium", - name: "Medium", - fill: "#F5C000", // Yellow/Gold color for medium - stackId: "bar-one", - textClassName: "text-custom-text-200", - }, - { - key: "low", - label: "Low", - name: "Low", - fill: "#00B8D9", // Blue color for low - stackId: "bar-one", - textClassName: "text-custom-text-200", - }, - { - key: "none", - label: "None", - name: "None", - fill: "#808080", // Gray color for none - stackId: "bar-one", - textClassName: "text-custom-text-200", - }, -] \ No newline at end of file From 82cf1597c276611fa35c7af00ced718c1339c050 Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Thu, 1 May 2025 19:25:27 +0530 Subject: [PATCH 21/69] reverted to void onchange --- .../components/analytics/custom-analytics/select/y-axis.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/core/components/analytics/custom-analytics/select/y-axis.tsx b/web/core/components/analytics/custom-analytics/select/y-axis.tsx index c97fc415aca..42c9145e899 100644 --- a/web/core/components/analytics/custom-analytics/select/y-axis.tsx +++ b/web/core/components/analytics/custom-analytics/select/y-axis.tsx @@ -13,7 +13,7 @@ import { EEstimateSystem } from "@/plane-web/constants/estimates"; type Props = { value: TYAxisValues; - onChange: (val: string) => void; + onChange: () => void; }; export const SelectYAxis: React.FC = observer(({ value, onChange }) => { From 2ed772d989265b3bb3db53f3d56a48e2c7154852 Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Thu, 1 May 2025 19:36:05 +0530 Subject: [PATCH 22/69] fixed some contant exports --- packages/constants/src/analytics-v2/common.ts | 75 +++++++++++++++++++ packages/constants/src/analytics.ts | 37 +++++---- .../analytics-v2/select/analytics-params.tsx | 4 +- .../analytics-v2/select/select-y-axis.tsx | 6 +- .../work-items/priority-chart.tsx | 4 +- 5 files changed, 99 insertions(+), 27 deletions(-) diff --git a/packages/constants/src/analytics-v2/common.ts b/packages/constants/src/analytics-v2/common.ts index 17148cd7000..9183322172e 100644 --- a/packages/constants/src/analytics-v2/common.ts +++ b/packages/constants/src/analytics-v2/common.ts @@ -1,4 +1,5 @@ import { TAnalyticsTabsV2Base } from "@plane/types"; +import { ChartXAxisProperty, ChartYAxisMetric } from "../chart"; export const insightsFields: Record = { "overview": ["total_users", "total_admins", "total_members", "total_guests", "total_projects", "total_work_items", "total_cycles", "total_intake"], @@ -24,3 +25,77 @@ export const ANALYTICS_V2_DURATION_FILTER_OPTIONS = [ value: "last_3_months", } ]; + +// TODO: add translations of the labels +export const ANALYTICS_V2_X_AXIS_VALUES: { value: ChartXAxisProperty; label: string }[] = + [ + { + value: ChartXAxisProperty.STATES, + label: "State name", + }, + { + value: ChartXAxisProperty.STATE_GROUPS, + label: "State group", + }, + { + value: ChartXAxisProperty.PRIORITY, + label: "Priority", + }, + { + value: ChartXAxisProperty.LABELS, + label: "Label", + }, + { + value: ChartXAxisProperty.ASSIGNEES, + label: "Assignee", + }, + { + value: ChartXAxisProperty.ESTIMATE_POINTS, + label: "Estimate point", + }, + { + value: ChartXAxisProperty.CYCLES, + label: "Cycle", + }, + { + value: ChartXAxisProperty.MODULES, + label: "Module", + }, + { + value: ChartXAxisProperty.COMPLETED_AT, + label: "Completed date", + }, + { + value: ChartXAxisProperty.TARGET_DATE, + label: "Due date", + }, + { + value: ChartXAxisProperty.START_DATE, + label: "Start date", + }, + { + value: ChartXAxisProperty.CREATED_AT, + label: "Created date", + }, + ]; + +export const ANALYTICS_V2_Y_AXIS_VALUES: { value: ChartYAxisMetric; label: string }[] = + [ + { + value: ChartYAxisMetric.WORK_ITEM_COUNT, + label: "Work item", + }, + { + value: ChartYAxisMetric.ESTIMATE_POINT_COUNT, + label: "Estimate", + }, + ]; + +export const ANALYTICS_V2_DATE_KEYS = [ + "completed_at", + "target_date", + "start_date", + "created_at", +]; + + diff --git a/packages/constants/src/analytics.ts b/packages/constants/src/analytics.ts index 86c01e036d5..6c8211ae0ab 100644 --- a/packages/constants/src/analytics.ts +++ b/packages/constants/src/analytics.ts @@ -1,6 +1,5 @@ // types import { TXAxisValues, TYAxisValues } from "@plane/types"; -import { ChartXAxisProperty, ChartYAxisMetric } from "./chart"; export const ANALYTICS_TABS = [ { @@ -10,68 +9,66 @@ export const ANALYTICS_TABS = [ { key: "custom", i18n_title: "workspace_analytics.tabs.custom" }, ]; - -// TODO: add translations of the labels -export const ANALYTICS_X_AXIS_VALUES: { value: ChartXAxisProperty; label: string }[] = +export const ANALYTICS_X_AXIS_VALUES: { value: TXAxisValues; label: string }[] = [ { - value: ChartXAxisProperty.STATES, + value: "state_id", label: "State name", }, { - value: ChartXAxisProperty.STATE_GROUPS, + value: "state__group", label: "State group", }, { - value: ChartXAxisProperty.PRIORITY, + value: "priority", label: "Priority", }, { - value: ChartXAxisProperty.LABELS, + value: "labels__id", label: "Label", }, { - value: ChartXAxisProperty.ASSIGNEES, + value: "assignees__id", label: "Assignee", }, { - value: ChartXAxisProperty.ESTIMATE_POINTS, + value: "estimate_point__value", label: "Estimate point", }, { - value: ChartXAxisProperty.CYCLES, + value: "issue_cycle__cycle_id", label: "Cycle", }, { - value: ChartXAxisProperty.MODULES, + value: "issue_module__module_id", label: "Module", }, { - value: ChartXAxisProperty.COMPLETED_AT, + value: "completed_at", label: "Completed date", }, { - value: ChartXAxisProperty.TARGET_DATE, + value: "target_date", label: "Due date", }, { - value: ChartXAxisProperty.START_DATE, + value: "start_date", label: "Start date", }, { - value: ChartXAxisProperty.CREATED_AT, + value: "created_at", label: "Created date", }, ]; -export const ANALYTICS_Y_AXIS_VALUES: { value: ChartYAxisMetric; label: string }[] = +export const ANALYTICS_Y_AXIS_VALUES: { value: TYAxisValues; label: string }[] = [ { - value: ChartYAxisMetric.WORK_ITEM_COUNT, - label: "Work item", + value: "issue_count", + label: "Work item Count", }, { - value: ChartYAxisMetric.ESTIMATE_POINT_COUNT, + value: "estimate", label: "Estimate", }, ]; diff --git a/web/core/components/analytics-v2/select/analytics-params.tsx b/web/core/components/analytics-v2/select/analytics-params.tsx index 55e4b954cf3..1328658ef59 100644 --- a/web/core/components/analytics-v2/select/analytics-params.tsx +++ b/web/core/components/analytics-v2/select/analytics-params.tsx @@ -1,7 +1,7 @@ import { observer } from "mobx-react"; import { Control, Controller } from "react-hook-form"; // plane imports -import { ANALYTICS_X_AXIS_VALUES, ChartXAxisProperty, ChartYAxisMetric } from "@plane/constants"; +import { ANALYTICS_V2_X_AXIS_VALUES, ChartXAxisProperty, ChartYAxisMetric } from "@plane/constants"; import { IAnalyticsV2Params } from "@plane/types"; import { Row } from "@plane/ui"; // components @@ -16,7 +16,7 @@ type Props = { export const AnalyticsV2SelectParams: React.FC = observer((props) => { const { control, params } = props; - const analyticsOptions = ANALYTICS_X_AXIS_VALUES; + const analyticsOptions = ANALYTICS_V2_X_AXIS_VALUES; return ( = observer(({ value, onChange, hiddenO return ( {ANALYTICS_Y_AXIS_VALUES.find((v) => v.value === value)?.label ?? "Add Metric"}} + label={{ANALYTICS_V2_Y_AXIS_VALUES.find((v) => v.value === value)?.label ?? "Add Metric"}} onChange={onChange} maxHeight="lg" > - {ANALYTICS_Y_AXIS_VALUES.filter((item) => !hiddenOptions?.includes(item.value)).map( + {ANALYTICS_V2_Y_AXIS_VALUES.filter((item) => !hiddenOptions?.includes(item.value)).map( (item) => isEstimateEnabled(item.value) && ( diff --git a/web/core/components/analytics-v2/work-items/priority-chart.tsx b/web/core/components/analytics-v2/work-items/priority-chart.tsx index aa25ed2952c..f0456a3e6a0 100644 --- a/web/core/components/analytics-v2/work-items/priority-chart.tsx +++ b/web/core/components/analytics-v2/work-items/priority-chart.tsx @@ -4,7 +4,7 @@ import { observer } from 'mobx-react' import { useParams } from 'next/navigation' import { useTheme } from 'next-themes' import useSWR from 'swr' -import { ANALYTICS_Y_AXIS_VALUES, ChartXAxisDateGrouping, ChartXAxisProperty, ChartYAxisMetric, EChartModels } from '@plane/constants' +import { ANALYTICS_V2_Y_AXIS_VALUES, ChartXAxisDateGrouping, ChartXAxisProperty, ChartYAxisMetric, EChartModels } from '@plane/constants' import { BarChart } from '@plane/propel/charts/bar-chart' import { IChartResponseV2 } from '@plane/types' import { TBarItem, TChartDatum } from '@plane/types/src/charts' @@ -110,7 +110,7 @@ const PriorityChart = observer((props: Props) => { header: () => parsedData.schema[key], })), [parsedData.schema]); - const getYAxisLabel = useMemo(() => ANALYTICS_Y_AXIS_VALUES.find((item) => item.value === props.y_axis)?.label ?? props.y_axis, [props.y_axis]); + const getYAxisLabel = useMemo(() => ANALYTICS_V2_Y_AXIS_VALUES.find((item) => item.value === props.y_axis)?.label ?? props.y_axis, [props.y_axis]); return (
From edaf34f6dfa573a272ae466380cc6f7eabe0845e Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Thu, 1 May 2025 19:45:52 +0530 Subject: [PATCH 23/69] fixed type issues --- packages/types/src/analytics.d.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/types/src/analytics.d.ts b/packages/types/src/analytics.d.ts index 9a4b16e9b15..ec417e73fe3 100644 --- a/packages/types/src/analytics.d.ts +++ b/packages/types/src/analytics.d.ts @@ -1,5 +1,3 @@ -import { ChartXAxisProperty } from "@plane/constants"; - export interface IAnalyticsResponse { total: number; distribution: IAnalyticsData; @@ -68,9 +66,9 @@ export type TXAxisValues = export type TYAxisValues = "issue_count" | "estimate"; export interface IAnalyticsParams { - x_axis: ChartXAxisProperty; - y_axis: ChartYAxisMetric; - segment?: ChartXAxisProperty | null; + x_axis: TXAxisValues; + y_axis: TYAxisValues; + segment?: TXAxisValues | null; project?: string[] | null; cycle?: string | null; module?: string | null; From fcbc96b8110c3f402bdfb0ab2b4b7783b3a0d3e4 Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Thu, 1 May 2025 19:59:48 +0530 Subject: [PATCH 24/69] fixed some type and build issues --- .../components/analytics-v2/select/analytics-params.tsx | 7 ++----- web/core/components/analytics-v2/select/select-x-axis.tsx | 6 ++---- .../analytics-v2/work-items/customized-insights.tsx | 1 - 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/web/core/components/analytics-v2/select/analytics-params.tsx b/web/core/components/analytics-v2/select/analytics-params.tsx index 1328658ef59..39abc349b59 100644 --- a/web/core/components/analytics-v2/select/analytics-params.tsx +++ b/web/core/components/analytics-v2/select/analytics-params.tsx @@ -1,7 +1,7 @@ import { observer } from "mobx-react"; import { Control, Controller } from "react-hook-form"; // plane imports -import { ANALYTICS_V2_X_AXIS_VALUES, ChartXAxisProperty, ChartYAxisMetric } from "@plane/constants"; +import { ANALYTICS_V2_X_AXIS_VALUES, ChartYAxisMetric } from "@plane/constants"; import { IAnalyticsV2Params } from "@plane/types"; import { Row } from "@plane/ui"; // components @@ -11,11 +11,10 @@ import { SelectYAxis } from "./select-y-axis"; type Props = { control: Control; - params: IAnalyticsV2Params; }; export const AnalyticsV2SelectParams: React.FC = observer((props) => { - const { control, params } = props; + const { control } = props; const analyticsOptions = ANALYTICS_V2_X_AXIS_VALUES; return ( @@ -44,7 +43,6 @@ export const AnalyticsV2SelectParams: React.FC = observer((props) => { onChange={(val: string) => { onChange(val); }} - params={params} analyticsOptions={analyticsOptions} /> )} @@ -58,7 +56,6 @@ export const AnalyticsV2SelectParams: React.FC = observer((props) => { onChange={(val: string) => { onChange(val); }} - params={params} analyticsOptions={analyticsOptions} /> )} diff --git a/web/core/components/analytics-v2/select/select-x-axis.tsx b/web/core/components/analytics-v2/select/select-x-axis.tsx index d7188fc3091..dfb1196ca4b 100644 --- a/web/core/components/analytics-v2/select/select-x-axis.tsx +++ b/web/core/components/analytics-v2/select/select-x-axis.tsx @@ -1,20 +1,19 @@ "use client"; import { ChartXAxisProperty } from "@plane/constants"; -import { IAnalyticsParams } from "@plane/types"; +import { IAnalyticsV2Params } from "@plane/types"; // ui import { CustomSelect } from "@plane/ui"; type Props = { value?: ChartXAxisProperty; onChange: (val: string) => void; - params: IAnalyticsParams; analyticsOptions: { value: ChartXAxisProperty; label: string }[]; hiddenOptions?: ChartXAxisProperty[]; }; export const SelectXAxis: React.FC = (props) => { - const { value, onChange, params, analyticsOptions, hiddenOptions } = props; + const { value, onChange, analyticsOptions, hiddenOptions } = props; return ( = (props) => { maxHeight="lg" > {analyticsOptions.map((item) => { - if (params.segment === item.value) return null; if (hiddenOptions?.includes(item.value)) return null; return ( diff --git a/web/core/components/analytics-v2/work-items/customized-insights.tsx b/web/core/components/analytics-v2/work-items/customized-insights.tsx index 7da5c1fecde..70c18a33dcb 100644 --- a/web/core/components/analytics-v2/work-items/customized-insights.tsx +++ b/web/core/components/analytics-v2/work-items/customized-insights.tsx @@ -27,7 +27,6 @@ const CustomizedInsights = observer(() => { actions={ } > From 5eb42d9204439278ad06465a176a5e5127b4daab Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Fri, 2 May 2025 13:46:29 +0530 Subject: [PATCH 25/69] chore: updated the filtering logic for analytics --- apiserver/plane/app/views/analytic/advance.py | 177 ++++++++---------- 1 file changed, 78 insertions(+), 99 deletions(-) diff --git a/apiserver/plane/app/views/analytic/advance.py b/apiserver/plane/app/views/analytic/advance.py index bc68d60d4e0..809e98bb55e 100644 --- a/apiserver/plane/app/views/analytic/advance.py +++ b/apiserver/plane/app/views/analytic/advance.py @@ -1,3 +1,5 @@ +from datetime import datetime + from rest_framework.response import Response from rest_framework import status @@ -48,20 +50,7 @@ def initialize_workspace(self, slug): def get_date_filters(self, date_filter): now = timezone.now() - if date_filter == "today": - return { - "current": { - "gte": now.replace(hour=0, minute=0, second=0, microsecond=0), - "lte": now, - }, - "previous": { - "gte": (now - timedelta(days=1)).replace( - hour=0, minute=0, second=0, microsecond=0 - ), - "lte": now - timedelta(days=1), - }, - } - elif date_filter == "yesterday": + if date_filter == "yesterday": return { "current": { "gte": (now - timedelta(days=1)).replace( @@ -92,6 +81,14 @@ def get_date_filters(self, date_filter): "lte": now - timedelta(days=30), }, } + elif date_filter == "last_3_months": + return { + "current": {"gte": now - timedelta(days=90), "lte": now}, + "previous": { + "gte": now - timedelta(days=180), + "lte": now - timedelta(days=90), + }, + } return None def get_filtered_counts(self, queryset, date_filters): @@ -193,7 +190,7 @@ def get(self, request, slug): self.initialize_workspace(slug) tab = request.GET.get("tab", "overview") project_ids = request.GET.get("project_ids", None) - date_filter = request.GET.get("date_filter", "today") + date_filter = request.GET.get("date_filter", "yesterday") if project_ids: project_ids = [str(project_id) for project_id in project_ids.split(",")] @@ -217,64 +214,29 @@ class AdvanceAnalyticsStatsEndpoint(BaseAPIView): def initialize_workspace(self, slug): self._workspace_slug = slug + project_ids = self.request.GET.get("project_ids", None) self.base_filters = { "workspace__slug": slug, "project__project_projectmember__member": self.request.user, "project__project_projectmember__is_active": True, } - def project_stats(self, filters): - return ( - Project.objects.filter( - workspace__slug=self._workspace_slug, - project_projectmember__member=self.request.user, - project_projectmember__is_active=True, - ) - .annotate( - total_work_items=Count("project_issue", distinct=True), - total_cycles=Count("project_cycle", distinct=True), - total_modules=Count("project_module", distinct=True), - total_intake=Count( - "project_issue", - filter=Q(project_issue__issue_intake__isnull=False), - distinct=True, - ), - total_members=Count( - "project_projectmember", - filter=Q( - project_projectmember__is_active=True, - ), - distinct=True, - ), - total_epics=Count( - "project_issue", - filter=Q(project_issue__type__is_epic=True), - distinct=True, - ), - total_pages=Count("project_pages", distinct=True), - total_views=Count("project_issueview", distinct=True), - ) - .values( - "id", - "name", - "logo_props", - "total_work_items", - "total_cycles", - "total_modules", - "total_intake", - "total_members", - "total_epics", - "total_pages", - "total_views", - ) - ) + self.project_filters = { + "workspace__slug": slug, + "project_projectmember__member": self.request.user, + "project_projectmember__is_active": True, + } + + if project_ids: + if isinstance(project_ids, str): + project_ids = [str(project_id) for project_id in project_ids.split(",")] + self.base_filters["project_id__in"] = project_ids + self.project_filters["id__in"] = project_ids def get_project_issues_stats(self, filters): qs = Issue.objects.filter( - workspace__slug=self._workspace_slug, - project__project_projectmember__member=self.request.user, - project__project_projectmember__is_active=True, **filters, + **self.base_filters, ) return ( @@ -307,36 +269,42 @@ class AdvanceAnalyticsChartEndpoint(BaseAPIView): def initialize_workspace(self, slug): self._workspace_slug = slug + project_ids = self.request.GET.get("project_ids", None) self.base_filters = { + "workspace__slug": slug, + "project__project_projectmember__member": self.request.user, + "project__project_projectmember__is_active": True, + } + + self.project_filters = { "workspace__slug": slug, "project_projectmember__member": self.request.user, "project_projectmember__is_active": True, } + if project_ids: + if isinstance(project_ids, str): + project_ids = [str(project_id) for project_id in project_ids.split(",")] + self.base_filters["project_id__in"] = project_ids + self.project_filters["id__in"] = project_ids + def project_chart(self, filters): - project_ids = ( - Project.objects.filter(**self.base_filters) - .values_list("id", flat=True) - .distinct() - ) - total_work_items = Issue.issue_objects.filter( - project_id__in=project_ids - ).count() - total_cycles = Cycle.objects.filter(project_id__in=project_ids).count() - total_modules = Module.objects.filter(project_id__in=project_ids).count() + total_work_items = Issue.issue_objects.filter(**self.base_filters).count() + total_cycles = Cycle.objects.filter(**self.base_filters).count() + total_modules = Module.objects.filter(**self.base_filters).count() total_intake = Issue.objects.filter( - project_id__in=project_ids, issue_intake__isnull=False + issue_intake__isnull=False, **self.base_filters ).count() total_members = WorkspaceMember.objects.filter( workspace__slug=self._workspace_slug, is_active=True ).count() total_epics = Issue.objects.filter( - project_id__in=project_ids, type__is_epic=True + type__is_epic=True, **self.base_filters ).count() - total_pages = ProjectPage.objects.filter(project_id__in=project_ids).count() - total_views = IssueView.objects.filter(project_id__in=project_ids).count() + total_pages = ProjectPage.objects.filter(**self.base_filters).count() + total_views = IssueView.objects.filter(**self.base_filters).count() data = { "work_items": total_work_items, @@ -361,11 +329,7 @@ def project_chart(self, filters): def work_item_completion_chart(self, filters, date_filter="last_30_days"): # Get the base queryset queryset = ( - Issue.issue_objects.filter( - workspace__slug=self._workspace_slug, - project__project_projectmember__member=self.request.user, - project__project_projectmember__is_active=True, - ) + Issue.issue_objects.filter(**self.base_filters) .select_related("workspace", "project", "state", "parent") .prefetch_related( "assignees", "labels", "issue_module__module", "issue_cycle__cycle" @@ -379,13 +343,13 @@ def work_item_completion_chart(self, filters, date_filter="last_30_days"): # Get the date range today = timezone.now().date() date_ranges = { - "today": (today, today), "yesterday": ( - today - timezone.timedelta(days=1), - today - timezone.timedelta(days=1), + today - timedelta(days=1), + today - timedelta(days=1), ), - "last_30_days": (today - timezone.timedelta(days=30), today), - "last_3_months": (today - timezone.timedelta(days=90), today), + "last_7_days": (today - timedelta(days=7), today), + "last_30_days": (today - timedelta(days=30), today), + "last_3_months": (today - timedelta(days=90), today), } # Handle custom date range if provided in filters @@ -395,12 +359,8 @@ def work_item_completion_chart(self, filters, date_filter="last_30_days"): and "end_date" in filters ): try: - start_date = timezone.datetime.strptime( - filters["start_date"], "%Y-%m-%d" - ).date() - end_date = timezone.datetime.strptime( - filters["end_date"], "%Y-%m-%d" - ).date() + start_date = datetime.strptime(filters["start_date"], "%Y-%m-%d").date() + end_date = datetime.strptime(filters["end_date"], "%Y-%m-%d").date() except ValueError: return Response( {"error": "Invalid date format. Use YYYY-MM-DD"}, @@ -463,7 +423,7 @@ def work_item_completion_chart(self, filters, date_filter="last_30_days"): "created_issues": stats["created_count"], } ) - current_date += timezone.timedelta(days=1) + current_date += timedelta(days=1) schema = { "completed_issues": "completed_issues", @@ -486,11 +446,7 @@ def get(self, request, slug): elif type == "custom-work-items": queryset = ( - Issue.issue_objects.filter( - workspace__slug=self._workspace_slug, - project__project_projectmember__member=self.request.user, - project__project_projectmember__is_active=True, - ) + Issue.issue_objects.filter(**self.base_filters) .select_related("workspace", "project", "state", "parent") .prefetch_related( "assignees", "labels", "issue_module__module", "issue_cycle__cycle" @@ -511,6 +467,28 @@ def get(self, request, slug): class AdvanceAnalyticsExportEndpoint(BaseAPIView): + + def initialize_workspace(self, slug): + self._workspace_slug = slug + project_ids = self.request.GET.get("project_ids", None) + self.base_filters = { + "workspace__slug": slug, + "project__project_projectmember__member": self.request.user, + "project__project_projectmember__is_active": True, + } + + self.project_filters = { + "workspace__slug": slug, + "project_projectmember__member": self.request.user, + "project_projectmember__is_active": True, + } + + if project_ids: + if isinstance(project_ids, str): + project_ids = [str(project_id) for project_id in project_ids.split(",")] + self.base_filters["project_id__in"] = project_ids + self.project_filters["id__in"] = project_ids + @allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE") def post(self, request, slug): filters = request.GET.get("filters", {}) @@ -521,6 +499,7 @@ def post(self, request, slug): project__project_projectmember__member=self.request.user, project__project_projectmember__is_active=True, **filters, + **self.base_filters, ) .values("project_id", "project__name") .annotate( From f7229386792484e96a077c7834483c55f4e07d80 Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Fri, 2 May 2025 13:53:31 +0530 Subject: [PATCH 26/69] updated default value to last_30_days --- web/core/store/analytics-v2.store.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/core/store/analytics-v2.store.ts b/web/core/store/analytics-v2.store.ts index 47cd2ef5ae7..7205b44bce2 100644 --- a/web/core/store/analytics-v2.store.ts +++ b/web/core/store/analytics-v2.store.ts @@ -24,7 +24,7 @@ export class AnalyticsStoreV2 implements IAnalyticsStoreV2 { //observables currentTab: TAnalyticsTabsV2Base = "overview"; selectedProject: string | null = null; - selectedDuration: typeof ANALYTICS_V2_DURATION_FILTER_OPTIONS[number]['value'] = "today"; + selectedDuration: typeof ANALYTICS_V2_DURATION_FILTER_OPTIONS[number]['value'] = "last_30_days"; constructor(_rootStore: CoreRootStore) { makeObservable(this, { From a4c8aebe2c0d87c4cadf4be84e3a9d88caec6605 Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Fri, 2 May 2025 15:39:22 +0530 Subject: [PATCH 27/69] percentage value whole number and added some rules for axis options --- .../analytics-v2/select/analytics-params.tsx | 25 ++++++++++++------- .../analytics-v2/select/select-x-axis.tsx | 14 +++++++---- .../analytics-v2/select/select-y-axis.tsx | 11 ++++---- .../components/analytics-v2/trend-piece.tsx | 2 +- .../work-items/customized-insights.tsx | 12 ++++++--- 5 files changed, 41 insertions(+), 23 deletions(-) diff --git a/web/core/components/analytics-v2/select/analytics-params.tsx b/web/core/components/analytics-v2/select/analytics-params.tsx index 39abc349b59..18b90fa71cf 100644 --- a/web/core/components/analytics-v2/select/analytics-params.tsx +++ b/web/core/components/analytics-v2/select/analytics-params.tsx @@ -1,21 +1,25 @@ import { observer } from "mobx-react"; -import { Control, Controller } from "react-hook-form"; +import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form"; // plane imports -import { ANALYTICS_V2_X_AXIS_VALUES, ChartYAxisMetric } from "@plane/constants"; +import { ANALYTICS_V2_X_AXIS_VALUES, ANALYTICS_V2_Y_AXIS_VALUES, ChartYAxisMetric } from "@plane/constants"; import { IAnalyticsV2Params } from "@plane/types"; import { Row } from "@plane/ui"; // components import { SelectXAxis } from "./select-x-axis"; import { SelectYAxis } from "./select-y-axis"; +import { useMemo } from "react"; // hooks type Props = { control: Control; + setValue: UseFormSetValue; + params: IAnalyticsV2Params; }; export const AnalyticsV2SelectParams: React.FC = observer((props) => { - const { control } = props; - const analyticsOptions = ANALYTICS_V2_X_AXIS_VALUES; + const { control, setValue, params } = props; + const xAxisOptions = useMemo(() => ANALYTICS_V2_X_AXIS_VALUES.filter((option) => option.value !== params.group_by), [params.group_by]); + const groupByOptions = useMemo(() => ANALYTICS_V2_X_AXIS_VALUES.filter((option) => option.value !== params.x_axis), [params.x_axis]); return ( = observer((props) => { render={({ field: { value, onChange } }) => ( { + onChange={(val: ChartYAxisMetric | null) => { onChange(val); }} + options={ANALYTICS_V2_Y_AXIS_VALUES} hiddenOptions={[ChartYAxisMetric.ESTIMATE_POINT_COUNT]} /> )} @@ -40,10 +45,10 @@ export const AnalyticsV2SelectParams: React.FC = observer((props) => { render={({ field: { value, onChange } }) => ( { + onChange={(val) => { onChange(val); }} - analyticsOptions={analyticsOptions} + options={xAxisOptions} /> )} /> @@ -53,10 +58,12 @@ export const AnalyticsV2SelectParams: React.FC = observer((props) => { render={({ field: { value, onChange } }) => ( { + onChange={(val) => { onChange(val); }} - analyticsOptions={analyticsOptions} + options={groupByOptions} + placeholder="Group By" + allowNoValue /> )} /> diff --git a/web/core/components/analytics-v2/select/select-x-axis.tsx b/web/core/components/analytics-v2/select/select-x-axis.tsx index dfb1196ca4b..a2163d7cda5 100644 --- a/web/core/components/analytics-v2/select/select-x-axis.tsx +++ b/web/core/components/analytics-v2/select/select-x-axis.tsx @@ -4,24 +4,28 @@ import { ChartXAxisProperty } from "@plane/constants"; import { IAnalyticsV2Params } from "@plane/types"; // ui import { CustomSelect } from "@plane/ui"; +import { cn } from "@plane/utils"; type Props = { value?: ChartXAxisProperty; - onChange: (val: string) => void; - analyticsOptions: { value: ChartXAxisProperty; label: string }[]; + onChange: (val: ChartXAxisProperty | null) => void; + options: { value: ChartXAxisProperty; label: string }[]; + placeholder?: string; hiddenOptions?: ChartXAxisProperty[]; + allowNoValue?: boolean; }; export const SelectXAxis: React.FC = (props) => { - const { value, onChange, analyticsOptions, hiddenOptions } = props; + const { value, onChange, options, hiddenOptions, placeholder, allowNoValue } = props; return ( {analyticsOptions.find((v) => v.value === value)?.label || "Add Property"}} + label={{options.find((v) => v.value === value)?.label || placeholder || "Add Property"}} onChange={onChange} maxHeight="lg" > - {analyticsOptions.map((item) => { + {allowNoValue && No value} + {options.map((item) => { if (hiddenOptions?.includes(item.value)) return null; return ( diff --git a/web/core/components/analytics-v2/select/select-y-axis.tsx b/web/core/components/analytics-v2/select/select-y-axis.tsx index 94d98769b2c..5ac607e5041 100644 --- a/web/core/components/analytics-v2/select/select-y-axis.tsx +++ b/web/core/components/analytics-v2/select/select-y-axis.tsx @@ -3,7 +3,7 @@ import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // plane imports -import { ANALYTICS_V2_Y_AXIS_VALUES, ChartYAxisMetric } from "@plane/constants"; +import { ChartYAxisMetric } from "@plane/constants"; import { CustomSelect } from "@plane/ui"; // hooks import { useProjectEstimates } from "@/hooks/store"; @@ -12,11 +12,12 @@ import { EEstimateSystem } from "@/plane-web/constants/estimates"; type Props = { value: ChartYAxisMetric; - onChange: (val: string) => void; + onChange: (val: ChartYAxisMetric | null) => void; hiddenOptions?: ChartYAxisMetric[]; + options: { value: ChartYAxisMetric; label: string }[]; }; -export const SelectYAxis: React.FC = observer(({ value, onChange, hiddenOptions }) => { +export const SelectYAxis: React.FC = observer(({ value, onChange, hiddenOptions, options }) => { // hooks const { projectId } = useParams(); const { areEstimateEnabledByProjectId, currentActiveEstimateId, estimateById } = useProjectEstimates(); @@ -41,11 +42,11 @@ export const SelectYAxis: React.FC = observer(({ value, onChange, hiddenO return ( {ANALYTICS_V2_Y_AXIS_VALUES.find((v) => v.value === value)?.label ?? "Add Metric"}} + label={{options.find((v) => v.value === value)?.label ?? "Add Metric"}} onChange={onChange} maxHeight="lg" > - {ANALYTICS_V2_Y_AXIS_VALUES.filter((item) => !hiddenOptions?.includes(item.value)).map( + {options.map( (item) => isEstimateEnabled(item.value) && ( diff --git a/web/core/components/analytics-v2/trend-piece.tsx b/web/core/components/analytics-v2/trend-piece.tsx index d5f3181b968..896003b4c26 100644 --- a/web/core/components/analytics-v2/trend-piece.tsx +++ b/web/core/components/analytics-v2/trend-piece.tsx @@ -46,7 +46,7 @@ const TrendPiece = (props: Props) => { ) : ( )} - {Math.abs(percentage)}% + {Math.round(Math.abs(percentage))}%
) } diff --git a/web/core/components/analytics-v2/work-items/customized-insights.tsx b/web/core/components/analytics-v2/work-items/customized-insights.tsx index 70c18a33dcb..4537dd04b6b 100644 --- a/web/core/components/analytics-v2/work-items/customized-insights.tsx +++ b/web/core/components/analytics-v2/work-items/customized-insights.tsx @@ -7,12 +7,16 @@ import AnalyticsSectionWrapper from '../analytics-section-wrapper' import { AnalyticsV2SelectParams } from '../select/analytics-params' import PriorityChart from './priority-chart' +const defaultValues: IAnalyticsV2Params = { + x_axis: ChartXAxisProperty.PRIORITY, + y_axis: ChartYAxisMetric.WORK_ITEM_COUNT, +} + const CustomizedInsights = observer(() => { const { t } = useTranslation() - const { control, watch } = useForm({ + const { control, watch, setValue } = useForm({ defaultValues: { - x_axis: ChartXAxisProperty.PRIORITY, - y_axis: ChartYAxisMetric.WORK_ITEM_COUNT, + ...defaultValues, }, }) @@ -27,6 +31,8 @@ const CustomizedInsights = observer(() => { actions={ } > From 02fcc642712d3964b6321ee130bfbce1f3b6e022 Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Fri, 2 May 2025 15:45:54 +0530 Subject: [PATCH 28/69] fixed some translations --- packages/i18n/src/locales/en/translations.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/i18n/src/locales/en/translations.json b/packages/i18n/src/locales/en/translations.json index dacd380730f..bd86c34ad30 100644 --- a/packages/i18n/src/locales/en/translations.json +++ b/packages/i18n/src/locales/en/translations.json @@ -1151,7 +1151,7 @@ "total_work_items": "Total work items", "started_work_items": "Started work items", "backlog_work_items": "Backlog work items", - "un_started_work_items": "Un started work items", + "un_started_work_items": "Unstarted work items", "completed_work_items": "Completed work items", "total_guests": "Total Guests", "total_intake": "Total Intake", From 59c3b9945b48c12d42d8a6e96325e9ba3ce53d74 Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Fri, 2 May 2025 17:48:06 +0530 Subject: [PATCH 29/69] added - custom tick for radar, calc of insight cards, filter labels --- .../propel/src/charts/components/tick.tsx | 23 ++++ .../propel/src/charts/radar-chart/root.tsx | 6 +- .../analytics-section-wrapper.tsx | 2 +- .../overview/project-insights.tsx | 7 +- .../analytics-v2/select/analytics-params.tsx | 127 +++++++++++------- .../analytics-v2/total-insights.tsx | 5 +- .../work-items/customized-insights.tsx | 3 + web/core/services/analytics-v2.service.ts | 10 ++ web/core/store/analytics-v2.store.ts | 4 +- 9 files changed, 127 insertions(+), 60 deletions(-) diff --git a/packages/propel/src/charts/components/tick.tsx b/packages/propel/src/charts/components/tick.tsx index 092ac6490bd..4b64e83736c 100644 --- a/packages/propel/src/charts/components/tick.tsx +++ b/packages/propel/src/charts/components/tick.tsx @@ -22,3 +22,26 @@ export const CustomYAxisTick = React.memo(({ x, y, payload }: any) => ( )); CustomYAxisTick.displayName = "CustomYAxisTick"; + +export const CustomRadarAxisTick = React.memo( + ({ x, y, payload, getLabel, cx, cy, offset = 16 }: any) => { + // Calculate direction vector from center to tick + const dx = x - cx; + const dy = y - cy; + // Normalize and apply offset + const length = Math.sqrt(dx * dx + dy * dy); + const normX = dx / length; + const normY = dy / length; + const labelX = x + normX * offset; + const labelY = y + normY * offset; + + return ( + + + {getLabel ? getLabel(payload.value) : payload.value} + + + ); + } +); +CustomRadarAxisTick.displayName = "CustomRadarAxisTick"; diff --git a/packages/propel/src/charts/radar-chart/root.tsx b/packages/propel/src/charts/radar-chart/root.tsx index 55f5161f59c..56ef7f44c40 100644 --- a/packages/propel/src/charts/radar-chart/root.tsx +++ b/packages/propel/src/charts/radar-chart/root.tsx @@ -1,9 +1,9 @@ import { TRadarChartProps } from '@plane/types'; -import React, { useMemo, useState } from 'react' +import { useMemo, useState } from 'react' import { PolarGrid, Radar, RadarChart as CoreRadarChart, ResponsiveContainer, PolarAngleAxis, RadarProps, Tooltip, Legend } from 'recharts'; import { CustomTooltip } from '../components/tooltip'; import { getLegendProps } from '../components/legend'; -import { CustomXAxisTick } from '../components/tick'; +import { CustomRadarAxisTick } from '../components/tick'; const RadarChart = (props: TRadarChartProps) => { const { data, radars, dataKey, margin, showTooltip, legend, className, angleAxis } = props; @@ -22,7 +22,7 @@ const RadarChart = (props: TRadarChartProps< } + tick={(props) => } /> {showTooltip && ( = (props) => { const { title, children, className, subtitle, actions } = props return (
-
+
{title &&

{title}

{subtitle &&

• {subtitle}

} diff --git a/web/core/components/analytics-v2/overview/project-insights.tsx b/web/core/components/analytics-v2/overview/project-insights.tsx index 9d66523c109..41e9a5b0866 100644 --- a/web/core/components/analytics-v2/overview/project-insights.tsx +++ b/web/core/components/analytics-v2/overview/project-insights.tsx @@ -24,8 +24,7 @@ const ProjectInsights = observer(() => { const params = useParams(); const { t } = useTranslation() const workspaceSlug = params.workspaceSlug as string; - const { selectedDuration } = useAnalyticsV2() - const selectedDurationLabel = useMemo(() => PROJECT_CREATED_AT_FILTER_OPTIONS.find(item => item.value === selectedDuration)?.name, [selectedDuration]) + const { selectedDuration, selectedDurationLabel } = useAnalyticsV2() const { data: projectInsightsData } = useSWR( `radar-chart-${workspaceSlug}`, @@ -33,9 +32,7 @@ const ProjectInsights = observer(() => { created_at: selectedDuration }), ) - return ( -
{projectInsightsData && {
{t('workspace_analytics.summary_of_projects')}
{t('workspace_analytics.all_projects')}
-
+
{t('workspace_analytics.trend_on_charts')}
{t('common.work_items')}
diff --git a/web/core/components/analytics-v2/select/analytics-params.tsx b/web/core/components/analytics-v2/select/analytics-params.tsx index 18b90fa71cf..8f4a6227940 100644 --- a/web/core/components/analytics-v2/select/analytics-params.tsx +++ b/web/core/components/analytics-v2/select/analytics-params.tsx @@ -3,70 +3,101 @@ import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form" // plane imports import { ANALYTICS_V2_X_AXIS_VALUES, ANALYTICS_V2_Y_AXIS_VALUES, ChartYAxisMetric } from "@plane/constants"; import { IAnalyticsV2Params } from "@plane/types"; -import { Row } from "@plane/ui"; +import { Button, Row, setToast, TOAST_TYPE } from "@plane/ui"; // components import { SelectXAxis } from "./select-x-axis"; import { SelectYAxis } from "./select-y-axis"; import { useMemo } from "react"; +import { Download } from "lucide-react"; +import { useTranslation } from "@plane/i18n"; +import { AnalyticsV2Service } from "@/services/analytics-v2.service"; // hooks type Props = { control: Control; setValue: UseFormSetValue; params: IAnalyticsV2Params; + workspaceSlug: string; }; +const analyticsV2Service = new AnalyticsV2Service() + export const AnalyticsV2SelectParams: React.FC = observer((props) => { - const { control, setValue, params } = props; + const { control, setValue, params, workspaceSlug } = props; + const { t } = useTranslation(); const xAxisOptions = useMemo(() => ANALYTICS_V2_X_AXIS_VALUES.filter((option) => option.value !== params.group_by), [params.group_by]); const groupByOptions = useMemo(() => ANALYTICS_V2_X_AXIS_VALUES.filter((option) => option.value !== params.x_axis), [params.x_axis]); + const exportAnalytics = () => { + analyticsV2Service + .exportAnalytics(workspaceSlug, params) + .then((res) => { + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Success!", + message: res.message, + }); + }) + .catch(() => + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "There was some error in exporting the analytics. Please try again.", + }) + ); + }; return ( - - ( - { - onChange(val); - }} - options={ANALYTICS_V2_Y_AXIS_VALUES} - hiddenOptions={[ChartYAxisMetric.ESTIMATE_POINT_COUNT]} - /> - )} - /> - ( - { - onChange(val); - }} - options={xAxisOptions} - /> - )} - /> - ( - { - onChange(val); - }} - options={groupByOptions} - placeholder="Group By" - allowNoValue - /> - )} - /> - +
+ + ( + { + onChange(val); + }} + options={ANALYTICS_V2_Y_AXIS_VALUES} + hiddenOptions={[ChartYAxisMetric.ESTIMATE_POINT_COUNT]} + /> + )} + /> + ( + { + onChange(val); + }} + options={xAxisOptions} + /> + )} + /> + ( + { + onChange(val); + }} + options={groupByOptions} + placeholder="Group By" + allowNoValue + /> + )} + /> + + +
+ ); }); diff --git a/web/core/components/analytics-v2/total-insights.tsx b/web/core/components/analytics-v2/total-insights.tsx index 319ed101710..5511f05bfa3 100644 --- a/web/core/components/analytics-v2/total-insights.tsx +++ b/web/core/components/analytics-v2/total-insights.tsx @@ -12,6 +12,7 @@ import { useAnalyticsV2 } from '@/hooks/store/use-analytics-v2'; import { AnalyticsV2Service } from '@/services/analytics-v2.service'; // plane web components import InsightCard from './insight-card'; +import { cn } from '@plane/utils'; const analyticsV2Service = new AnalyticsV2Service(); @@ -29,7 +30,9 @@ const TotalInsights: React.FC<{ analyticsType: TAnalyticsTabsV2Base }> = observe })) return ( -
+
{insightsFields[analyticsType].map((item: string) => ( ))} diff --git a/web/core/components/analytics-v2/work-items/customized-insights.tsx b/web/core/components/analytics-v2/work-items/customized-insights.tsx index 4537dd04b6b..de9a66cea5a 100644 --- a/web/core/components/analytics-v2/work-items/customized-insights.tsx +++ b/web/core/components/analytics-v2/work-items/customized-insights.tsx @@ -6,6 +6,7 @@ import { IAnalyticsV2Params } from '@plane/types' import AnalyticsSectionWrapper from '../analytics-section-wrapper' import { AnalyticsV2SelectParams } from '../select/analytics-params' import PriorityChart from './priority-chart' +import { useParams } from 'next/navigation' const defaultValues: IAnalyticsV2Params = { x_axis: ChartXAxisProperty.PRIORITY, @@ -14,6 +15,7 @@ const defaultValues: IAnalyticsV2Params = { const CustomizedInsights = observer(() => { const { t } = useTranslation() + const { workspaceSlug } = useParams(); const { control, watch, setValue } = useForm({ defaultValues: { ...defaultValues, @@ -33,6 +35,7 @@ const CustomizedInsights = observer(() => { control={control} setValue={setValue} params={params} + workspaceSlug={workspaceSlug.toString()} /> } > diff --git a/web/core/services/analytics-v2.service.ts b/web/core/services/analytics-v2.service.ts index adb3b694e23..9c8d0139045 100644 --- a/web/core/services/analytics-v2.service.ts +++ b/web/core/services/analytics-v2.service.ts @@ -46,5 +46,15 @@ export class AnalyticsV2Service extends APIService { throw err?.response?.data; }) } + + async exportAnalytics(workspaceSlug: string, params?: Record): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/advance-analytics-export/`, { + params + }) + .then((res) => res?.data) + .catch((err) => { + throw err?.response?.data; + }) + } } diff --git a/web/core/store/analytics-v2.store.ts b/web/core/store/analytics-v2.store.ts index 7205b44bce2..6259d382093 100644 --- a/web/core/store/analytics-v2.store.ts +++ b/web/core/store/analytics-v2.store.ts @@ -1,5 +1,5 @@ import { action, computed, makeObservable, observable, runInAction } from "mobx"; -import { ANALYTICS_V2_DURATION_FILTER_OPTIONS, PROJECT_CREATED_AT_FILTER_OPTIONS } from "@plane/constants"; +import { ANALYTICS_V2_DURATION_FILTER_OPTIONS, DURATION_FILTER_OPTIONS, PROJECT_CREATED_AT_FILTER_OPTIONS } from "@plane/constants"; import { TAnalyticsTabsV2Base } from "@plane/types"; import { CoreRootStore } from "./root.store"; @@ -47,7 +47,7 @@ export class AnalyticsStoreV2 implements IAnalyticsStoreV2 { get selectedDurationLabel() { - return PROJECT_CREATED_AT_FILTER_OPTIONS.find(item => item.value === this.selectedDuration)?.name ?? null + return ANALYTICS_V2_DURATION_FILTER_OPTIONS.find(item => item.value === this.selectedDuration)?.name ?? null } updateSelectedProject = (project: string) => { From c792fffcac4699e040fbcf5f4430554411a148fd Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Fri, 2 May 2025 18:07:16 +0530 Subject: [PATCH 30/69] chore: opitmised the analytics endpoint --- apiserver/plane/app/views/analytic/advance.py | 389 ++++++------------ apiserver/plane/utils/date_utils.py | 172 ++++++++ 2 files changed, 307 insertions(+), 254 deletions(-) create mode 100644 apiserver/plane/utils/date_utils.py diff --git a/apiserver/plane/app/views/analytic/advance.py b/apiserver/plane/app/views/analytic/advance.py index 809e98bb55e..8dca649a081 100644 --- a/apiserver/plane/app/views/analytic/advance.py +++ b/apiserver/plane/app/views/analytic/advance.py @@ -1,5 +1,3 @@ -from datetime import datetime - from rest_framework.response import Response from rest_framework import status @@ -15,7 +13,6 @@ ProjectPage, ) -from django.utils import timezone from django.db.models import ( Q, Count, @@ -24,87 +21,45 @@ from datetime import timedelta from django.db.models.functions import TruncDay from plane.bgtasks.analytic_plot_export import export_analytics_to_csv_email +from plane.utils.date_utils import ( + get_analytics_filters, +) class AdvanceAnalyticsEndpoint(BaseAPIView): def initialize_workspace(self, slug): self._workspace_slug = slug - project_ids = self.request.GET.get("project_ids", None) - self.base_filters = { - "workspace__slug": slug, - "project__project_projectmember__member": self.request.user, - "project__project_projectmember__is_active": True, - } - - self.project_filters = { - "workspace__slug": slug, - "project_projectmember__member": self.request.user, - "project_projectmember__is_active": True, - } - - if project_ids: - if isinstance(project_ids, str): - project_ids = [str(project_id) for project_id in project_ids.split(",")] - self.base_filters["project_id__in"] = project_ids - self.project_filters["id__in"] = project_ids - - def get_date_filters(self, date_filter): - now = timezone.now() - if date_filter == "yesterday": - return { - "current": { - "gte": (now - timedelta(days=1)).replace( - hour=0, minute=0, second=0, microsecond=0 - ), - "lte": now - timedelta(days=1), - }, - "previous": { - "gte": (now - timedelta(days=2)).replace( - hour=0, minute=0, second=0, microsecond=0 - ), - "lte": now - timedelta(days=2), - }, - } - elif date_filter == "last_7_days": - return { - "current": {"gte": now - timedelta(days=7), "lte": now}, - "previous": { - "gte": now - timedelta(days=14), - "lte": now - timedelta(days=7), - }, - } - elif date_filter == "last_30_days": - return { - "current": {"gte": now - timedelta(days=30), "lte": now}, - "previous": { - "gte": now - timedelta(days=60), - "lte": now - timedelta(days=30), - }, - } - elif date_filter == "last_3_months": - return { - "current": {"gte": now - timedelta(days=90), "lte": now}, - "previous": { - "gte": now - timedelta(days=180), - "lte": now - timedelta(days=90), - }, - } - return None + self.filters = get_analytics_filters( + slug=slug, + user=self.request.user, + date_filter=self.request.GET.get("date_filter", None), + project_ids=self.request.GET.get("project_ids", None), + ) - def get_filtered_counts(self, queryset, date_filters): + def get_filtered_counts(self, queryset): def get_filtered_count(): - if date_filters: + if self.filters["analytics_date_range"]: return queryset.filter( - created_at__gte=date_filters["current"]["gte"], - created_at__lte=date_filters["current"]["lte"], + created_at__gte=self.filters["analytics_date_range"]["current"][ + "gte" + ], + created_at__lte=self.filters["analytics_date_range"]["current"][ + "lte" + ], ).count() return queryset.count() def get_previous_count(): - if date_filters: + if self.filters["analytics_date_range"] and self.filters[ + "analytics_date_range" + ].get("previous"): return queryset.filter( - created_at__gte=date_filters["previous"]["gte"], - created_at__lte=date_filters["previous"]["lte"], + created_at__gte=self.filters["analytics_date_range"]["previous"][ + "gte" + ], + created_at__lte=self.filters["analytics_date_range"]["previous"][ + "lte" + ], ).count() return 0 @@ -113,75 +68,66 @@ def get_previous_count(): "filter_count": get_previous_count(), } - def get_overview_data(self, date_filter, project_ids): - date_filters = self.get_date_filters(date_filter) - + def get_overview_data(self): return { "total_users": self.get_filtered_counts( WorkspaceMember.objects.filter( workspace__slug=self._workspace_slug, is_active=True - ), - date_filters, + ) ), "total_admins": self.get_filtered_counts( WorkspaceMember.objects.filter( workspace__slug=self._workspace_slug, role=ROLE.ADMIN.value, is_active=True, - ), - date_filters, + ) ), "total_members": self.get_filtered_counts( WorkspaceMember.objects.filter( workspace__slug=self._workspace_slug, role=ROLE.MEMBER.value, is_active=True, - ), - date_filters, + ) ), "total_guests": self.get_filtered_counts( WorkspaceMember.objects.filter( workspace__slug=self._workspace_slug, role=ROLE.GUEST.value, is_active=True, - ), - date_filters, + ) ), "total_projects": self.get_filtered_counts( - Project.objects.filter(**self.project_filters), - date_filters, + Project.objects.filter(**self.filters["project_filters"]) ), "total_work_items": self.get_filtered_counts( - Issue.issue_objects.filter(**self.base_filters), date_filters + Issue.issue_objects.filter(**self.filters["base_filters"]) ), "total_cycles": self.get_filtered_counts( - Cycle.objects.filter(**self.base_filters), date_filters + Cycle.objects.filter(**self.filters["base_filters"]) ), "total_intake": self.get_filtered_counts( - Issue.objects.filter(**self.base_filters).filter( + Issue.objects.filter(**self.filters["base_filters"]).filter( issue_intake__isnull=False - ), - date_filters, + ) ), } - def get_work_items_stats(self, date_filter, project_ids): - date_filters = self.get_date_filters(date_filter) - base_queryset = Issue.objects.filter(**self.base_filters) + def get_work_items_stats(self): + base_queryset = Issue.objects.filter(**self.filters["base_filters"]) return { - "total_work_items": self.get_filtered_counts(base_queryset, date_filters), + "total_work_items": self.get_filtered_counts(base_queryset), "started_work_items": self.get_filtered_counts( - base_queryset.filter(state__group="started"), date_filters + base_queryset.filter(state__group="started") ), "backlog_work_items": self.get_filtered_counts( - base_queryset.filter(state__group="backlog"), date_filters + base_queryset.filter(state__group="backlog") ), "un_started_work_items": self.get_filtered_counts( - base_queryset.filter(state__group="un-started"), date_filters + base_queryset.filter(state__group="un-started") ), "completed_work_items": self.get_filtered_counts( - base_queryset.filter(state__group="completed"), date_filters + base_queryset.filter(state__group="completed") ), } @@ -189,21 +135,16 @@ def get_work_items_stats(self, date_filter, project_ids): def get(self, request, slug): self.initialize_workspace(slug) tab = request.GET.get("tab", "overview") - project_ids = request.GET.get("project_ids", None) - date_filter = request.GET.get("date_filter", "yesterday") - - if project_ids: - project_ids = [str(project_id) for project_id in project_ids.split(",")] if tab == "overview": return Response( - self.get_overview_data(date_filter, project_ids), + self.get_overview_data(), status=status.HTTP_200_OK, ) elif tab == "work-items": return Response( - self.get_work_items_stats(date_filter, project_ids), + self.get_work_items_stats(), status=status.HTTP_200_OK, ) @@ -211,36 +152,28 @@ def get(self, request, slug): class AdvanceAnalyticsStatsEndpoint(BaseAPIView): - def initialize_workspace(self, slug): self._workspace_slug = slug - project_ids = self.request.GET.get("project_ids", None) - self.base_filters = { - "workspace__slug": slug, - "project__project_projectmember__member": self.request.user, - "project__project_projectmember__is_active": True, - } - - self.project_filters = { - "workspace__slug": slug, - "project_projectmember__member": self.request.user, - "project_projectmember__is_active": True, - } + self.filters = get_analytics_filters( + slug=slug, + user=self.request.user, + date_filter=self.request.GET.get("date_filter", None), + project_ids=self.request.GET.get("project_ids", None), + ) - if project_ids: - if isinstance(project_ids, str): - project_ids = [str(project_id) for project_id in project_ids.split(",")] - self.base_filters["project_id__in"] = project_ids - self.project_filters["id__in"] = project_ids + def get_project_issues_stats(self): + # Get the base queryset with workspace and project filters + base_queryset = Issue.issue_objects.filter(**self.filters["base_filters"]) - def get_project_issues_stats(self, filters): - qs = Issue.objects.filter( - **filters, - **self.base_filters, - ) + # Apply date range filter if available + if self.filters["chart_period_range"]: + start_date, end_date = self.filters["chart_period_range"] + base_queryset = base_queryset.filter( + created_at__date__gte=start_date, created_at__date__lte=end_date + ) return ( - qs.values("project_id", "project__name") + base_queryset.values("project_id", "project__name") .annotate( cancelled_work_items=Count("id", filter=Q(state__group="cancelled")), completed_work_items=Count("id", filter=Q(state__group="completed")), @@ -254,57 +187,58 @@ def get_project_issues_stats(self, filters): @allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE") def get(self, request, slug): self.initialize_workspace(slug) - type = request.GET.get("type", "overview") - filters = request.GET.get("filters", {}) + type = request.GET.get("type", "work-items") if type == "work-items": return Response( - self.get_project_issues_stats(filters), status=status.HTTP_200_OK + self.get_project_issues_stats(), + status=status.HTTP_200_OK, ) return Response({"message": "Invalid type"}, status=status.HTTP_400_BAD_REQUEST) class AdvanceAnalyticsChartEndpoint(BaseAPIView): - def initialize_workspace(self, slug): self._workspace_slug = slug - project_ids = self.request.GET.get("project_ids", None) - self.base_filters = { - "workspace__slug": slug, - "project__project_projectmember__member": self.request.user, - "project__project_projectmember__is_active": True, - } - - self.project_filters = { - "workspace__slug": slug, - "project_projectmember__member": self.request.user, - "project_projectmember__is_active": True, - } + self.filters = get_analytics_filters( + slug=slug, + user=self.request.user, + date_filter=self.request.GET.get("date_filter", None), + project_ids=self.request.GET.get("project_ids", None), + ) - if project_ids: - if isinstance(project_ids, str): - project_ids = [str(project_id) for project_id in project_ids.split(",")] - self.base_filters["project_id__in"] = project_ids - self.project_filters["id__in"] = project_ids + def project_chart(self): + # Get the base queryset with workspace and project filters + base_queryset = Issue.issue_objects.filter(**self.filters["base_filters"]) - def project_chart(self, filters): + # Apply date range filter if available + if self.filters["chart_period_range"]: + start_date, end_date = self.filters["chart_period_range"] + date_filter = { + "created_at__date__gte": start_date, + "created_at__date__lte": end_date, + } - total_work_items = Issue.issue_objects.filter(**self.base_filters).count() - total_cycles = Cycle.objects.filter(**self.base_filters).count() - total_modules = Module.objects.filter(**self.base_filters).count() + total_work_items = base_queryset.count() + total_cycles = Cycle.objects.filter( + **self.filters["base_filters"], **date_filter + ).count() + total_modules = Module.objects.filter( + **self.filters["base_filters"], **date_filter + ).count() total_intake = Issue.objects.filter( - issue_intake__isnull=False, **self.base_filters + issue_intake__isnull=False, **self.filters["base_filters"], **date_filter ).count() total_members = WorkspaceMember.objects.filter( - workspace__slug=self._workspace_slug, is_active=True + workspace__slug=self._workspace_slug, is_active=True, **date_filter ).count() - - total_epics = Issue.objects.filter( - type__is_epic=True, **self.base_filters + total_pages = ProjectPage.objects.filter( + **self.filters["base_filters"], **date_filter + ).count() + total_views = IssueView.objects.filter( + **self.filters["base_filters"], **date_filter ).count() - total_pages = ProjectPage.objects.filter(**self.base_filters).count() - total_views = IssueView.objects.filter(**self.base_filters).count() data = { "work_items": total_work_items, @@ -312,7 +246,6 @@ def project_chart(self, filters): "modules": total_modules, "intake": total_intake, "members": total_members, - "epics": total_epics, "pages": total_pages, "views": total_views, } @@ -326,74 +259,28 @@ def project_chart(self, filters): for key, value in data.items() ] - def work_item_completion_chart(self, filters, date_filter="last_30_days"): + def work_item_completion_chart(self): # Get the base queryset queryset = ( - Issue.issue_objects.filter(**self.base_filters) - .select_related("workspace", "project", "state", "parent") + Issue.issue_objects.filter(**self.filters["base_filters"]) + .select_related("workspace", "state", "parent") .prefetch_related( "assignees", "labels", "issue_module__module", "issue_cycle__cycle" ) ) - - # Apply any additional filters - if filters: - queryset = queryset.filter(**filters) - - # Get the date range - today = timezone.now().date() - date_ranges = { - "yesterday": ( - today - timedelta(days=1), - today - timedelta(days=1), - ), - "last_7_days": (today - timedelta(days=7), today), - "last_30_days": (today - timedelta(days=30), today), - "last_3_months": (today - timedelta(days=90), today), - } - - # Handle custom date range if provided in filters - if ( - isinstance(filters, dict) - and "start_date" in filters - and "end_date" in filters - ): - try: - start_date = datetime.strptime(filters["start_date"], "%Y-%m-%d").date() - end_date = datetime.strptime(filters["end_date"], "%Y-%m-%d").date() - except ValueError: - return Response( - {"error": "Invalid date format. Use YYYY-MM-DD"}, - status=status.HTTP_400_BAD_REQUEST, - ) - else: - start_date, end_date = date_ranges.get( - date_filter, date_ranges["last_30_days"] + # Apply date range filter if available + if self.filters["chart_period_range"]: + start_date, end_date = self.filters["chart_period_range"] + queryset = queryset.filter( + created_at__date__gte=start_date, created_at__date__lte=end_date ) # Get daily stats daily_stats = ( - queryset.filter( - Q(created_at__date__gte=start_date, created_at__date__lte=end_date) - | Q( - completed_at__date__gte=start_date, completed_at__date__lte=end_date - ) - ) - .annotate( + queryset.annotate( date=TruncDay("created_at"), - created_count=Count( - "id", - filter=Q( - created_at__date__gte=start_date, created_at__date__lte=end_date - ), - ), - completed_count=Count( - "id", - filter=Q( - completed_at__date__gte=start_date, - completed_at__date__lte=end_date, - ), - ), + created_count=Count("id", filter=Q(created_at__isnull=False)), + completed_count=Count("id", filter=Q(completed_at__isnull=False)), ) .values("date", "created_count", "completed_count") .order_by("date") @@ -435,23 +322,30 @@ def work_item_completion_chart(self, filters, date_filter="last_30_days"): @allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE") def get(self, request, slug): self.initialize_workspace(slug) - type = request.GET.get("type", "overview") - filters = request.GET.get("filters", {}) + type = request.GET.get("type", "projects") group_by = request.GET.get("group_by", None) x_axis = request.GET.get("x_axis", "PRIORITY") - date_filter = request.GET.get("date_filter", "last_30_days") if type == "projects": - return Response(self.project_chart(filters), status=status.HTTP_200_OK) + return Response(self.project_chart(), status=status.HTTP_200_OK) elif type == "custom-work-items": + # Get the base queryset queryset = ( - Issue.issue_objects.filter(**self.base_filters) - .select_related("workspace", "project", "state", "parent") + Issue.issue_objects.filter(**self.filters["base_filters"]) + .select_related("workspace", "state", "parent") .prefetch_related( "assignees", "labels", "issue_module__module", "issue_cycle__cycle" ) ) + + # Apply date range filter if available + if self.filters["chart_period_range"]: + start_date, end_date = self.filters["chart_period_range"] + queryset = queryset.filter( + created_at__date__gte=start_date, created_at__date__lte=end_date + ) + return Response( build_analytics_chart(queryset, x_axis, group_by), status=status.HTTP_200_OK, @@ -459,7 +353,7 @@ def get(self, request, slug): elif type == "work-items": return Response( - self.work_item_completion_chart(filters, date_filter), + self.work_item_completion_chart(), status=status.HTTP_200_OK, ) @@ -467,40 +361,20 @@ def get(self, request, slug): class AdvanceAnalyticsExportEndpoint(BaseAPIView): - def initialize_workspace(self, slug): self._workspace_slug = slug - project_ids = self.request.GET.get("project_ids", None) - self.base_filters = { - "workspace__slug": slug, - "project__project_projectmember__member": self.request.user, - "project__project_projectmember__is_active": True, - } - - self.project_filters = { - "workspace__slug": slug, - "project_projectmember__member": self.request.user, - "project_projectmember__is_active": True, - } - - if project_ids: - if isinstance(project_ids, str): - project_ids = [str(project_id) for project_id in project_ids.split(",")] - self.base_filters["project_id__in"] = project_ids - self.project_filters["id__in"] = project_ids + self.filters = get_analytics_filters( + slug=slug, + user=self.request.user, + date_filter=self.request.GET.get("date_filter", None), + project_ids=self.request.GET.get("project_ids", None), + ) @allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE") - def post(self, request, slug): - filters = request.GET.get("filters", {}) - + def get(self, request, slug): + self.initialize_workspace(slug) data = ( - Issue.objects.filter( - workspace__slug=slug, - project__project_projectmember__member=self.request.user, - project__project_projectmember__is_active=True, - **filters, - **self.base_filters, - ) + Issue.issue_objects.filter(**self.filters["base_filters"]) .values("project_id", "project__name") .annotate( cancelled_work_items=Count("id", filter=Q(state__group="cancelled")), @@ -512,6 +386,13 @@ def post(self, request, slug): .order_by("project_id") ) + # Apply date range filter if available + if self.filters["chart_period_range"]: + start_date, end_date = self.filters["chart_period_range"] + data = data.filter( + created_at__date__gte=start_date, created_at__date__lte=end_date + ) + # Convert QuerySet to list of dictionaries for serialization serialized_data = list(data) diff --git a/apiserver/plane/utils/date_utils.py b/apiserver/plane/utils/date_utils.py new file mode 100644 index 00000000000..dfb6639ac86 --- /dev/null +++ b/apiserver/plane/utils/date_utils.py @@ -0,0 +1,172 @@ +from datetime import datetime, timedelta +from django.utils import timezone + + +def get_analytics_date_range(date_filter=None, start_date=None, end_date=None): + """ + Get date range for analytics with current and previous periods for comparison. + Returns a dictionary with current and previous date ranges. + + Args: + date_filter (str): The type of date filter to apply + start_date (str): Start date for custom range (format: YYYY-MM-DD) + end_date (str): End date for custom range (format: YYYY-MM-DD) + + Returns: + dict: Dictionary containing current and previous date ranges + """ + if not date_filter: + return None + + today = timezone.now().date() + + if date_filter == "yesterday": + yesterday = today - timedelta(days=1) + return { + "current": { + "gte": datetime.combine(yesterday, datetime.min.time()), + "lte": datetime.combine(yesterday, datetime.max.time()), + } + } + elif date_filter == "last_7_days": + return { + "current": { + "gte": datetime.combine(today - timedelta(days=7), datetime.min.time()), + "lte": datetime.combine(today, datetime.max.time()), + }, + "previous": { + "gte": datetime.combine( + today - timedelta(days=14), datetime.min.time() + ), + "lte": datetime.combine(today - timedelta(days=8), datetime.max.time()), + }, + } + elif date_filter == "last_30_days": + return { + "current": { + "gte": datetime.combine( + today - timedelta(days=30), datetime.min.time() + ), + "lte": datetime.combine(today, datetime.max.time()), + }, + "previous": { + "gte": datetime.combine( + today - timedelta(days=60), datetime.min.time() + ), + "lte": datetime.combine( + today - timedelta(days=31), datetime.max.time() + ), + }, + } + elif date_filter == "last_3_months": + return { + "current": { + "gte": datetime.combine( + today - timedelta(days=90), datetime.min.time() + ), + "lte": datetime.combine(today, datetime.max.time()), + }, + "previous": { + "gte": datetime.combine( + today - timedelta(days=180), datetime.min.time() + ), + "lte": datetime.combine( + today - timedelta(days=91), datetime.max.time() + ), + }, + } + elif date_filter == "custom" and start_date and end_date: + try: + start = datetime.strptime(start_date, "%Y-%m-%d").date() + end = datetime.strptime(end_date, "%Y-%m-%d").date() + return { + "current": { + "gte": datetime.combine(start, datetime.min.time()), + "lte": datetime.combine(end, datetime.max.time()), + } + } + except (ValueError, TypeError): + return None + return None + + +def get_chart_period_range(date_filter=None): + """ + Get date range for chart visualization. + Returns a tuple of (start_date, end_date) for the specified period. + + Args: + date_filter (str): The type of date filter to apply. Options are: + - "yesterday": Yesterday's date + - "last_7_days": Last 7 days + - "last_30_days": Last 30 days + - "last_3_months": Last 90 days + Defaults to "last_7_days" if not specified or invalid. + + Returns: + tuple: A tuple containing (start_date, end_date) as date objects + """ + today = timezone.now().date() + period_ranges = { + "yesterday": ( + today - timedelta(days=1), + today - timedelta(days=1), + ), + "last_7_days": (today - timedelta(days=7), today), + "last_30_days": (today - timedelta(days=30), today), + "last_3_months": (today - timedelta(days=90), today), + } + + return period_ranges.get(date_filter, period_ranges["last_7_days"]) + + +def get_analytics_filters(slug, user, date_filter=None, project_ids=None): + """ + Get combined project and date filters for analytics endpoints + + Args: + slug: The workspace slug + user: The current user + date_filter: Optional date filter string + project_ids: Optional list of project IDs + + Returns: + dict: A dictionary containing: + - base_filters: Base filters for the workspace and user + - project_filters: Project-specific filters + - analytics_date_range: Date range filters for analytics comparison + - chart_period_range: Date range for chart visualization + """ + # Get project IDs from request + if project_ids and isinstance(project_ids, str): + project_ids = [str(project_id) for project_id in project_ids.split(",")] + + # Base filters for workspace and user + base_filters = { + "workspace__slug": slug, + "project__project_projectmember__member": user, + "project__project_projectmember__is_active": True, + } + + # Project filters + project_filters = { + "workspace__slug": slug, + "project_projectmember__member": user, + "project_projectmember__is_active": True, + } + + # Add project IDs to filters if provided + if project_ids: + base_filters["project_id__in"] = project_ids + project_filters["id__in"] = project_ids + + # Get date range filters + analytics_date_range = get_analytics_date_range(date_filter) + chart_period_range = get_chart_period_range(date_filter) + + return { + "base_filters": base_filters, + "project_filters": project_filters, + "analytics_date_range": analytics_date_range, + "chart_period_range": chart_period_range, + } From 7f02300f547875f9c032a9ac6b90db66c3f62ba8 Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Fri, 2 May 2025 19:02:43 +0530 Subject: [PATCH 31/69] replace old analytics path with new , updated labels of insight card, done some store fixes --- packages/constants/src/workspace.ts | 50 +++++++++---------- .../propel/src/charts/area-chart/root.tsx | 2 +- packages/propel/src/charts/bar-chart/root.tsx | 7 +-- packages/types/src/charts/index.d.ts | 1 + .../components/analytics-v2/insight-card.tsx | 6 +-- .../analytics-v2/total-insights.tsx | 10 +++- .../work-items/priority-chart.tsx | 13 +++-- .../workspace/sidebar/workspace-menu.tsx | 2 +- 8 files changed, 52 insertions(+), 39 deletions(-) diff --git a/packages/constants/src/workspace.ts b/packages/constants/src/workspace.ts index c1c60f392a5..257a1247b87 100644 --- a/packages/constants/src/workspace.ts +++ b/packages/constants/src/workspace.ts @@ -134,13 +134,13 @@ export const WORKSPACE_SETTINGS_LINKS: { access: EUserWorkspaceRoles[]; highlight: (pathname: string, baseUrl: string) => boolean; }[] = [ - WORKSPACE_SETTINGS["general"], - WORKSPACE_SETTINGS["members"], - WORKSPACE_SETTINGS["billing-and-plans"], - WORKSPACE_SETTINGS["export"], - WORKSPACE_SETTINGS["webhooks"], - WORKSPACE_SETTINGS["api-tokens"], -]; + WORKSPACE_SETTINGS["general"], + WORKSPACE_SETTINGS["members"], + WORKSPACE_SETTINGS["billing-and-plans"], + WORKSPACE_SETTINGS["export"], + WORKSPACE_SETTINGS["webhooks"], + WORKSPACE_SETTINGS["api-tokens"], + ]; export const ROLE = { [EUserWorkspaceRoles.GUEST]: "Guest", @@ -237,23 +237,23 @@ export const DEFAULT_GLOBAL_VIEWS_LIST: { key: TStaticViewTypes; i18n_label: string; }[] = [ - { - key: "all-issues", - i18n_label: "default_global_view.all_issues", - }, - { - key: "assigned", - i18n_label: "default_global_view.assigned", - }, - { - key: "created", - i18n_label: "default_global_view.created", - }, - { - key: "subscribed", - i18n_label: "default_global_view.subscribed", - }, -]; + { + key: "all-issues", + i18n_label: "default_global_view.all_issues", + }, + { + key: "assigned", + i18n_label: "default_global_view.assigned", + }, + { + key: "created", + i18n_label: "default_global_view.created", + }, + { + key: "subscribed", + i18n_label: "default_global_view.subscribed", + }, + ]; export interface IWorkspaceSidebarNavigationItem { key: string; @@ -278,7 +278,7 @@ export const WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS: Record(props: xAxis.label && { value: xAxis.label, dy: 28, - className: AXIS_LABEL_CLASSNAME, + className: AXIS_LABEL_CLASSNAME } } tickCount={tickCount.x} diff --git a/packages/propel/src/charts/bar-chart/root.tsx b/packages/propel/src/charts/bar-chart/root.tsx index 741403e98d3..889064ece74 100644 --- a/packages/propel/src/charts/bar-chart/root.tsx +++ b/packages/propel/src/charts/bar-chart/root.tsx @@ -79,6 +79,7 @@ export const BarChart = React.memo((props: T )), [activeLegend, stackKeys, bars] ); + console.log(xAxis.label, "test data") return (
@@ -101,7 +102,7 @@ export const BarChart = React.memo((props: T axisLine={false} label={{ value: xAxis.label, - dy: 28, + dy: xAxis.dy || 28, className: AXIS_LABEL_CLASSNAME, }} tickCount={tickCount.x} @@ -114,8 +115,8 @@ export const BarChart = React.memo((props: T value: yAxis.label, angle: -90, position: "bottom", - offset: yAxis.offset || -24, - dx: yAxis.dx || -16, + offset: -24, + dx: -16, className: AXIS_LABEL_CLASSNAME, }} tick={(props) => } diff --git a/packages/types/src/charts/index.d.ts b/packages/types/src/charts/index.d.ts index cb2890b4387..2747973aa78 100644 --- a/packages/types/src/charts/index.d.ts +++ b/packages/types/src/charts/index.d.ts @@ -29,6 +29,7 @@ type TChartProps = { key: keyof TChartData; label?: string; strokeColor?: string; + dy?: number; }; yAxis: { allowDecimals?: boolean; diff --git a/web/core/components/analytics-v2/insight-card.tsx b/web/core/components/analytics-v2/insight-card.tsx index d54acd8483a..7c8d34d6e11 100644 --- a/web/core/components/analytics-v2/insight-card.tsx +++ b/web/core/components/analytics-v2/insight-card.tsx @@ -9,10 +9,11 @@ export type InsightCardProps = { data?: IAnalyticsResponseFieldsV2; label: string; isLoading?: boolean; + versus?: string | null; } const InsightCard = (props: InsightCardProps) => { - const { data, label, isLoading } = props; + const { data, label, isLoading, versus } = props; const { count, filter_count } = data || {}; const percentage = useMemo(() => { if (count != null && filter_count != null) { @@ -21,7 +22,6 @@ const InsightCard = (props: InsightCardProps) => { } return null; }, [count, filter_count]); - const versus = "last month"; return (
@@ -32,7 +32,7 @@ const InsightCard = (props: InsightCardProps) => { {percentage && (
-
vs {versus}
+ {versus &&
vs {versus}
}
)}
diff --git a/web/core/components/analytics-v2/total-insights.tsx b/web/core/components/analytics-v2/total-insights.tsx index 5511f05bfa3..3090e8e9512 100644 --- a/web/core/components/analytics-v2/total-insights.tsx +++ b/web/core/components/analytics-v2/total-insights.tsx @@ -21,7 +21,7 @@ const TotalInsights: React.FC<{ analyticsType: TAnalyticsTabsV2Base }> = observe const params = useParams(); const workspaceSlug = params.workspaceSlug as string; const { t } = useTranslation() - const { selectedDuration, selectedProject } = useAnalyticsV2() + const { selectedDuration, selectedProject, selectedDurationLabel } = useAnalyticsV2() const { data: totalInsightsData, isLoading } = useSWR(`total-insights-${analyticsType}-${selectedDuration}-${selectedProject}`, () => analyticsV2Service.getAdvanceAnalytics(workspaceSlug, analyticsType, { @@ -34,7 +34,13 @@ const TotalInsights: React.FC<{ analyticsType: TAnalyticsTabsV2Base }> = observe insightsFields[analyticsType].length % 5 === 0 ? 'lg:grid-cols-5 gap-10' : 'lg:grid-cols-4 gap-8' )}> {insightsFields[analyticsType].map((item: string) => ( - + ))}
) diff --git a/web/core/components/analytics-v2/work-items/priority-chart.tsx b/web/core/components/analytics-v2/work-items/priority-chart.tsx index f0456a3e6a0..158f8dc0080 100644 --- a/web/core/components/analytics-v2/work-items/priority-chart.tsx +++ b/web/core/components/analytics-v2/work-items/priority-chart.tsx @@ -110,20 +110,25 @@ const PriorityChart = observer((props: Props) => { header: () => parsedData.schema[key], })), [parsedData.schema]); - const getYAxisLabel = useMemo(() => ANALYTICS_V2_Y_AXIS_VALUES.find((item) => item.value === props.y_axis)?.label ?? props.y_axis, [props.y_axis]); - + const yAxisLabel = useMemo(() => ANALYTICS_V2_Y_AXIS_VALUES.find((item) => item.value === props.y_axis)?.label ?? props.y_axis, [props.y_axis]); + const xAxisLabel = useMemo(() => props.x_axis === ChartXAxisProperty.PRIORITY ? "Priority" : props.x_axis, [props.x_axis]); return (
{
diff --git a/web/core/components/workspace/sidebar/workspace-menu.tsx b/web/core/components/workspace/sidebar/workspace-menu.tsx index 2b2fa90b317..dd2ba54990a 100644 --- a/web/core/components/workspace/sidebar/workspace-menu.tsx +++ b/web/core/components/workspace/sidebar/workspace-menu.tsx @@ -55,7 +55,7 @@ export const SidebarWorkspaceMenu = observer(() => { { key: "analytics", labelTranslationKey: "sidebar.analytics", - href: `/${workspaceSlug}/analytics/`, + href: `/${workspaceSlug}/analytics-v2/`, access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER], Icon: BarChart2, }, From c8040f28acb6a215d75a160b1f22750c7e3d5e6b Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Fri, 2 May 2025 20:04:52 +0530 Subject: [PATCH 32/69] chore: updated the export request --- apiserver/plane/app/views/analytic/advance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/plane/app/views/analytic/advance.py b/apiserver/plane/app/views/analytic/advance.py index 8dca649a081..021779de16a 100644 --- a/apiserver/plane/app/views/analytic/advance.py +++ b/apiserver/plane/app/views/analytic/advance.py @@ -371,7 +371,7 @@ def initialize_workspace(self, slug): ) @allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE") - def get(self, request, slug): + def post(self, request, slug): self.initialize_workspace(slug) data = ( Issue.issue_objects.filter(**self.filters["base_filters"]) From 4739f91c97f97afdfed27a463c18849ae29bdced Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Fri, 2 May 2025 20:06:54 +0530 Subject: [PATCH 33/69] Enhanced ProjectSelect to support multi-select, improved state management, and optimized data fetching and component structure. --- .../analytics-v2/analytics-filter-actions.tsx | 20 ++++--- .../analytics-v2/select/analytics-params.tsx | 8 +-- .../analytics-v2/select/project.tsx | 52 +++++++++++++++++++ .../analytics-v2/select/select-y-axis.tsx | 6 ++- .../analytics-v2/total-insights.tsx | 8 +-- .../work-items/customized-insights.tsx | 2 +- .../work-items/priority-chart.tsx | 10 ++-- .../work-items/workitems-insight-table.tsx | 6 +-- web/core/store/analytics-v2.store.ts | 18 +++---- 9 files changed, 92 insertions(+), 38 deletions(-) create mode 100644 web/core/components/analytics-v2/select/project.tsx diff --git a/web/core/components/analytics-v2/analytics-filter-actions.tsx b/web/core/components/analytics-v2/analytics-filter-actions.tsx index 352f8c0a7d9..bcc5955e762 100644 --- a/web/core/components/analytics-v2/analytics-filter-actions.tsx +++ b/web/core/components/analytics-v2/analytics-filter-actions.tsx @@ -1,26 +1,24 @@ // plane web components import { observer } from "mobx-react-lite"; -// components -import { ProjectDropdown } from "@/components/dropdowns"; // hooks +import { useProject } from "@/hooks/store"; import { useAnalyticsV2 } from "@/hooks/store/use-analytics-v2"; +// components import DurationDropdown from "./select/duration"; +import { ProjectSelect } from "./select/project"; const AnalyticsFilterActions = observer(() => { - const { selectedProject, selectedDuration, updateSelectedProject, updateSelectedDuration } = useAnalyticsV2() - + const { selectedProjects, selectedDuration, updateSelectedProjects, updateSelectedDuration } = useAnalyticsV2() + const { workspaceProjectIds } = useProject() return (
- { - updateSelectedProject(val) + updateSelectedProjects(val ?? []) }} - buttonVariant="border-with-text" - multiple={false} - dropdownArrow - + projectIds={workspaceProjectIds} /> void; + projectIds: string[] | undefined; +}; + +export const ProjectSelect: React.FC = observer((props) => { + const { value, onChange, projectIds } = props; + const { getProjectById } = useProject(); + + const options = projectIds?.map((projectId) => { + const projectDetails = getProjectById(projectId); + + return { + value: projectDetails?.id, + query: `${projectDetails?.name} ${projectDetails?.identifier}`, + content: ( +
+ {projectDetails?.identifier} + {projectDetails?.name} +
+ ), + }; + }); + + return ( + onChange(val)} + options={options} + label={ +
+ {value && value.length > 0 + ? projectIds + ?.filter((p) => value.includes(p)) + .map((p) => getProjectById(p)?.name) + .join(", ") + : "All projects"} +
+ } + multiple + /> + ); +}); diff --git a/web/core/components/analytics-v2/select/select-y-axis.tsx b/web/core/components/analytics-v2/select/select-y-axis.tsx index 5ac607e5041..d8f9a31bbee 100644 --- a/web/core/components/analytics-v2/select/select-y-axis.tsx +++ b/web/core/components/analytics-v2/select/select-y-axis.tsx @@ -47,12 +47,14 @@ export const SelectYAxis: React.FC = observer(({ value, onChange, hiddenO maxHeight="lg" > {options.map( - (item) => - isEstimateEnabled(item.value) && ( + (item) => { + if (hiddenOptions?.includes(item.value)) return null; + return isEstimateEnabled(item.value) && ( {item.label} ) + } )} ); diff --git a/web/core/components/analytics-v2/total-insights.tsx b/web/core/components/analytics-v2/total-insights.tsx index 3090e8e9512..d937bccb0d5 100644 --- a/web/core/components/analytics-v2/total-insights.tsx +++ b/web/core/components/analytics-v2/total-insights.tsx @@ -7,12 +7,12 @@ import { insightsFields } from '@plane/constants'; import { useTranslation } from '@plane/i18n'; import { IAnalyticsResponseV2, TAnalyticsTabsV2Base } from '@plane/types'; //hooks +import { cn } from '@/helpers/common.helper'; import { useAnalyticsV2 } from '@/hooks/store/use-analytics-v2'; //services import { AnalyticsV2Service } from '@/services/analytics-v2.service'; // plane web components import InsightCard from './insight-card'; -import { cn } from '@plane/utils'; const analyticsV2Service = new AnalyticsV2Service(); @@ -21,12 +21,12 @@ const TotalInsights: React.FC<{ analyticsType: TAnalyticsTabsV2Base }> = observe const params = useParams(); const workspaceSlug = params.workspaceSlug as string; const { t } = useTranslation() - const { selectedDuration, selectedProject, selectedDurationLabel } = useAnalyticsV2() + const { selectedDuration, selectedProjects, selectedDurationLabel } = useAnalyticsV2() - const { data: totalInsightsData, isLoading } = useSWR(`total-insights-${analyticsType}-${selectedDuration}-${selectedProject}`, + const { data: totalInsightsData, isLoading } = useSWR(`total-insights-${analyticsType}-${selectedDuration}-${selectedProjects}`, () => analyticsV2Service.getAdvanceAnalytics(workspaceSlug, analyticsType, { date_filter: selectedDuration, - ...(selectedProject ? { project_ids: selectedProject } : {}) + ...(selectedProjects ? { project_ids: selectedProjects } : {}) })) return ( diff --git a/web/core/components/analytics-v2/work-items/customized-insights.tsx b/web/core/components/analytics-v2/work-items/customized-insights.tsx index de9a66cea5a..a753760a0ee 100644 --- a/web/core/components/analytics-v2/work-items/customized-insights.tsx +++ b/web/core/components/analytics-v2/work-items/customized-insights.tsx @@ -1,4 +1,5 @@ import { observer } from 'mobx-react' +import { useParams } from 'next/navigation' import { useForm } from 'react-hook-form' import { ChartXAxisProperty, ChartYAxisMetric } from '@plane/constants' import { useTranslation } from '@plane/i18n' @@ -6,7 +7,6 @@ import { IAnalyticsV2Params } from '@plane/types' import AnalyticsSectionWrapper from '../analytics-section-wrapper' import { AnalyticsV2SelectParams } from '../select/analytics-params' import PriorityChart from './priority-chart' -import { useParams } from 'next/navigation' const defaultValues: IAnalyticsV2Params = { x_axis: ChartXAxisProperty.PRIORITY, diff --git a/web/core/components/analytics-v2/work-items/priority-chart.tsx b/web/core/components/analytics-v2/work-items/priority-chart.tsx index 158f8dc0080..29fd4fefef0 100644 --- a/web/core/components/analytics-v2/work-items/priority-chart.tsx +++ b/web/core/components/analytics-v2/work-items/priority-chart.tsx @@ -21,7 +21,7 @@ interface Props { const analyticsV2Service = new AnalyticsV2Service() const PriorityChart = observer((props: Props) => { - const { selectedDuration, selectedProject } = useAnalyticsV2() + const { selectedDuration, selectedProjects } = useAnalyticsV2() const params = useParams(); const { resolvedTheme } = useTheme(); const workspaceSlug = params.workspaceSlug as string; @@ -29,7 +29,7 @@ const PriorityChart = observer((props: Props) => { `customized-insights-chart-${workspaceSlug}-${selectedDuration}-${props.x_axis}-${props.y_axis}-${props.group_by}`, () => analyticsV2Service.getAdvanceAnalyticsCharts(workspaceSlug, 'custom-work-items', { date_filter: selectedDuration, - project_ids: selectedProject, + project_ids: selectedProjects, ...props }), ) @@ -101,13 +101,15 @@ const PriorityChart = observer((props: Props) => { }, { accessorKey: "count", - header: () => "Count", + header: () =>
Count
, + cell: ({ row }) =>
{row.original.count}
}, ], []); const columns: ColumnDef[] = useMemo(() => Object.keys(parsedData.schema).map((key) => ({ accessorKey: key, - header: () => parsedData.schema[key], + header: () =>
{parsedData.schema[key]}
, + cell: ({ row }) =>
{row.original[key]}
})), [parsedData.schema]); const yAxisLabel = useMemo(() => ANALYTICS_V2_Y_AXIS_VALUES.find((item) => item.value === props.y_axis)?.label ?? props.y_axis, [props.y_axis]); diff --git a/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx b/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx index c402209d69a..823836060b6 100644 --- a/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx +++ b/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx @@ -17,11 +17,11 @@ const WorkItemsInsightTable = observer(() => { const params = useParams(); const workspaceSlug = params.workspaceSlug as string; const { getProjectById } = useProject(); - const { selectedDuration, selectedProject } = useAnalyticsV2() - const { data: workItemsData, isLoading } = useSWR(`insights-table-work-items-${selectedDuration}-${selectedProject}`, + const { selectedDuration, selectedProjects } = useAnalyticsV2() + const { data: workItemsData, isLoading } = useSWR(`insights-table-work-items-${selectedDuration}-${selectedProjects}`, () => analyticsV2Service.getAdvanceAnalyticsStats(workspaceSlug, "work-items", { date_filter: selectedDuration, - ...(selectedProject ? { project_ids: selectedProject } : {}) + ...(selectedProjects ? { project_ids: selectedProjects } : {}) })) const columns = useMemo(() => [ diff --git a/web/core/store/analytics-v2.store.ts b/web/core/store/analytics-v2.store.ts index 6259d382093..da540eda448 100644 --- a/web/core/store/analytics-v2.store.ts +++ b/web/core/store/analytics-v2.store.ts @@ -1,5 +1,5 @@ import { action, computed, makeObservable, observable, runInAction } from "mobx"; -import { ANALYTICS_V2_DURATION_FILTER_OPTIONS, DURATION_FILTER_OPTIONS, PROJECT_CREATED_AT_FILTER_OPTIONS } from "@plane/constants"; +import { ANALYTICS_V2_DURATION_FILTER_OPTIONS, PROJECT_CREATED_AT_FILTER_OPTIONS } from "@plane/constants"; import { TAnalyticsTabsV2Base } from "@plane/types"; import { CoreRootStore } from "./root.store"; @@ -9,21 +9,21 @@ type DurationType = typeof PROJECT_CREATED_AT_FILTER_OPTIONS[number]['value'] export interface IAnalyticsStoreV2 { //observables currentTab: TAnalyticsTabsV2Base - selectedProject: string | null + selectedProjects: string[] selectedDuration: DurationType, //computed selectedDurationLabel: string | null, //actions - updateSelectedProject: (project: string) => void, + updateSelectedProjects: (projects: string[]) => void, updateSelectedDuration: (duration: DurationType) => void, } export class AnalyticsStoreV2 implements IAnalyticsStoreV2 { //observables currentTab: TAnalyticsTabsV2Base = "overview"; - selectedProject: string | null = null; + selectedProjects: string[] = []; selectedDuration: typeof ANALYTICS_V2_DURATION_FILTER_OPTIONS[number]['value'] = "last_30_days"; constructor(_rootStore: CoreRootStore) { @@ -31,12 +31,12 @@ export class AnalyticsStoreV2 implements IAnalyticsStoreV2 { // observables currentTab: observable, selectedDuration: observable, - selectedProject: observable, + selectedProjects: observable, // computed selectedProjectLabel: computed, selectedDurationLabel: computed, // actions - updateSelectedProject: action, + updateSelectedProjects: action, updateSelectedDuration: action }) } @@ -50,11 +50,11 @@ export class AnalyticsStoreV2 implements IAnalyticsStoreV2 { return ANALYTICS_V2_DURATION_FILTER_OPTIONS.find(item => item.value === this.selectedDuration)?.name ?? null } - updateSelectedProject = (project: string) => { - const initialState = this.selectedProject; + updateSelectedProjects = (projects: string[]) => { + const initialState = this.selectedProjects; try { runInAction(() => { - this.selectedProject = project; + this.selectedProjects = projects; }) } catch (error) { console.error("Failed to update selected project"); From acd4c605f9e447c4cd2630ad50038b8c198daa55 Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Fri, 2 May 2025 20:09:14 +0530 Subject: [PATCH 34/69] fix: round completion percentage calculation in ActiveProjectItem --- .../components/analytics-v2/overview/active-project-item.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/core/components/analytics-v2/overview/active-project-item.tsx b/web/core/components/analytics-v2/overview/active-project-item.tsx index b904cefacc3..6e92d8d8617 100644 --- a/web/core/components/analytics-v2/overview/active-project-item.tsx +++ b/web/core/components/analytics-v2/overview/active-project-item.tsx @@ -45,7 +45,7 @@ const ActiveProjectItem = (props: Props) => {

{projectDetails?.name}

- +
) } From d7b915c18a70bb73382b0daa3277d071ce469dc5 Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Sun, 4 May 2025 12:19:44 +0530 Subject: [PATCH 35/69] added empty states in project insights --- .../i18n/src/locales/en/translations.json | 8 +- web/app/page.tsx | 1 - .../analytics-v2/overview/loader.tsx | 9 ++ .../overview/project-insights.tsx | 94 ++++++++++--------- 4 files changed, 68 insertions(+), 44 deletions(-) create mode 100644 web/core/components/analytics-v2/overview/loader.tsx diff --git a/packages/i18n/src/locales/en/translations.json b/packages/i18n/src/locales/en/translations.json index bd86c34ad30..04de5d97680 100644 --- a/packages/i18n/src/locales/en/translations.json +++ b/packages/i18n/src/locales/en/translations.json @@ -1164,7 +1164,13 @@ "trend_on_charts": "Trend on charts", "active_projects": "Active Projects", "customized_insights": "Customized Insights", - "created_vs_resolved": "Created vs Resolved" + "created_vs_resolved": "Created vs Resolved", + "empty_state_v2": { + "project_insights": { + "title": "No data yet", + "description": "Work items assigned to you, broken down by state, will show up here." + } + } }, "workspace_projects": { "label": "{count, plural, one {Project} other {Projects}}", diff --git a/web/app/page.tsx b/web/app/page.tsx index 8d52af80c61..aac36f0a1ea 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -1,5 +1,4 @@ "use client"; - import React from "react"; import { observer } from "mobx-react"; import Image from "next/image"; diff --git a/web/core/components/analytics-v2/overview/loader.tsx b/web/core/components/analytics-v2/overview/loader.tsx new file mode 100644 index 00000000000..f2a4ab5323b --- /dev/null +++ b/web/core/components/analytics-v2/overview/loader.tsx @@ -0,0 +1,9 @@ +export const ProjectInsightsLoader = () => ( +
+
+
+
+
+
+
+); diff --git a/web/core/components/analytics-v2/overview/project-insights.tsx b/web/core/components/analytics-v2/overview/project-insights.tsx index 41e9a5b0866..76ecc158029 100644 --- a/web/core/components/analytics-v2/overview/project-insights.tsx +++ b/web/core/components/analytics-v2/overview/project-insights.tsx @@ -10,6 +10,8 @@ import { useAnalyticsV2 } from '@/hooks/store/use-analytics-v2' import { AnalyticsV2Service } from '@/services/analytics-v2.service' // import useSWR from 'swr' import AnalyticsSectionWrapper from '../analytics-section-wrapper' +import AnalyticsV2EmptyState from '../empty-state' +import { ProjectInsightsLoader } from './loader' const RadarChart = dynamic(() => import("@plane/propel/charts/radar-chart").then((mod) => ({ @@ -26,57 +28,65 @@ const ProjectInsights = observer(() => { const workspaceSlug = params.workspaceSlug as string; const { selectedDuration, selectedDurationLabel } = useAnalyticsV2() - const { data: projectInsightsData } = useSWR( + const { data: projectInsightsData, isLoading: isLoadingProjectInsight } = useSWR( `radar-chart-${workspaceSlug}`, () => analyticsV2Service.getAdvanceAnalyticsCharts[]>(workspaceSlug, 'projects', { created_at: selectedDuration }), ) + + return ( -
- {projectInsightsData && } -
-
{t('workspace_analytics.summary_of_projects')}
-
{t('workspace_analytics.all_projects')}
-
-
-
{t('workspace_analytics.trend_on_charts')}
-
{t('common.work_items')}
-
- {projectInsightsData?.map((item) => ( -
-
{item.name}
-
- {/* */} -
{item.count}
+ {isLoadingProjectInsight ? : + projectInsightsData && projectInsightsData?.length == 0 ? + : +
+ {projectInsightsData && } +
+
{t('workspace_analytics.summary_of_projects')}
+
{t('workspace_analytics.all_projects')}
+
+
+
{t('workspace_analytics.trend_on_charts')}
+
{t('common.work_items')}
+ {projectInsightsData?.map((item) => ( +
+
{item.name}
+
+ {/* */} +
{item.count}
+
+
+ ))}
- ))} -
-
-
- +
+
} ) From facbe70e19d22d03f4162b248280ec5c6bafb187 Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Sun, 4 May 2025 13:58:08 +0530 Subject: [PATCH 36/69] Added loader and empty state in created/resolved chart --- .../i18n/src/locales/en/translations.json | 4 + .../components/analytics-v2/empty-state.tsx | 4 +- .../components/analytics-v2/insight-card.tsx | 3 +- .../{overview/loader.tsx => loaders.tsx} | 5 + .../analytics-v2/overview/active-projects.tsx | 3 +- .../overview/project-insights.tsx | 2 +- .../work-items/created-vs-resolved.tsx | 155 +++++++++--------- 7 files changed, 98 insertions(+), 78 deletions(-) rename web/core/components/analytics-v2/{overview/loader.tsx => loaders.tsx} (77%) diff --git a/packages/i18n/src/locales/en/translations.json b/packages/i18n/src/locales/en/translations.json index 04de5d97680..d27ea369fa1 100644 --- a/packages/i18n/src/locales/en/translations.json +++ b/packages/i18n/src/locales/en/translations.json @@ -1169,6 +1169,10 @@ "project_insights": { "title": "No data yet", "description": "Work items assigned to you, broken down by state, will show up here." + }, + "created_vs_resolved": { + "title": "No data yet", + "description": "Work items created and resolved over time will show up here." } } }, diff --git a/web/core/components/analytics-v2/empty-state.tsx b/web/core/components/analytics-v2/empty-state.tsx index 59f259fe13f..d1ed8ee181e 100644 --- a/web/core/components/analytics-v2/empty-state.tsx +++ b/web/core/components/analytics-v2/empty-state.tsx @@ -34,4 +34,6 @@ const AnalyticsV2EmptyState = ({
) -export default AnalyticsV2EmptyState \ No newline at end of file +export default AnalyticsV2EmptyState + + diff --git a/web/core/components/analytics-v2/insight-card.tsx b/web/core/components/analytics-v2/insight-card.tsx index 7c8d34d6e11..12300d49062 100644 --- a/web/core/components/analytics-v2/insight-card.tsx +++ b/web/core/components/analytics-v2/insight-card.tsx @@ -18,7 +18,8 @@ const InsightCard = (props: InsightCardProps) => { const percentage = useMemo(() => { if (count != null && filter_count != null) { const result = ((count - filter_count) / count) * 100; - return isFinite(result) ? result : null; + const isFiniteAndNotNaNOrZero = isFinite(result) && !isNaN(result) && result !== 0; + return isFiniteAndNotNaNOrZero ? result : null; } return null; }, [count, filter_count]); diff --git a/web/core/components/analytics-v2/overview/loader.tsx b/web/core/components/analytics-v2/loaders.tsx similarity index 77% rename from web/core/components/analytics-v2/overview/loader.tsx rename to web/core/components/analytics-v2/loaders.tsx index f2a4ab5323b..bc251a35a3a 100644 --- a/web/core/components/analytics-v2/overview/loader.tsx +++ b/web/core/components/analytics-v2/loaders.tsx @@ -7,3 +7,8 @@ export const ProjectInsightsLoader = () => (
); + + +export const AreaChartLoader = () => ( +
+); diff --git a/web/core/components/analytics-v2/overview/active-projects.tsx b/web/core/components/analytics-v2/overview/active-projects.tsx index 5af0034ca9e..9798491122f 100644 --- a/web/core/components/analytics-v2/overview/active-projects.tsx +++ b/web/core/components/analytics-v2/overview/active-projects.tsx @@ -1,9 +1,8 @@ -import React, { useEffect } from 'react' +import React from 'react' import { observer } from 'mobx-react' import { useParams } from 'next/navigation' import useSWR from 'swr' import { useTranslation } from '@plane/i18n' -import { EUpdateStatus } from '@plane/types/src/enums' import { Loader } from '@plane/ui' import { useAnalyticsV2, useProject } from '@/hooks/store' import AnalyticsSectionWrapper from '../analytics-section-wrapper' diff --git a/web/core/components/analytics-v2/overview/project-insights.tsx b/web/core/components/analytics-v2/overview/project-insights.tsx index 76ecc158029..56c38a3e01a 100644 --- a/web/core/components/analytics-v2/overview/project-insights.tsx +++ b/web/core/components/analytics-v2/overview/project-insights.tsx @@ -11,7 +11,7 @@ import { AnalyticsV2Service } from '@/services/analytics-v2.service' // import useSWR from 'swr' import AnalyticsSectionWrapper from '../analytics-section-wrapper' import AnalyticsV2EmptyState from '../empty-state' -import { ProjectInsightsLoader } from './loader' +import { ProjectInsightsLoader } from '../loaders' const RadarChart = dynamic(() => import("@plane/propel/charts/radar-chart").then((mod) => ({ diff --git a/web/core/components/analytics-v2/work-items/created-vs-resolved.tsx b/web/core/components/analytics-v2/work-items/created-vs-resolved.tsx index d3af6f44b6c..ebec0de6145 100644 --- a/web/core/components/analytics-v2/work-items/created-vs-resolved.tsx +++ b/web/core/components/analytics-v2/work-items/created-vs-resolved.tsx @@ -10,86 +10,95 @@ import { renderFormattedDate } from '@plane/utils' import { useAnalyticsV2 } from '@/hooks/store/use-analytics-v2' import { AnalyticsV2Service } from '@/services/analytics-v2.service' import AnalyticsSectionWrapper from '../analytics-section-wrapper' +import AnalyticsV2EmptyState from '../empty-state' +import { AreaChartLoader } from '../loaders' const analyticsV2Service = new AnalyticsV2Service() const CreatedVsResolved = observer(() => { - const { selectedDuration, selectedDurationLabel } = useAnalyticsV2() - const params = useParams(); - const { t } = useTranslation() - const workspaceSlug = params.workspaceSlug as string; - const { data: createdVsResolvedData } = useSWR( - `created-vs-resolved-${workspaceSlug}-${selectedDuration}`, - () => analyticsV2Service.getAdvanceAnalyticsCharts(workspaceSlug, 'work-items', { - date_filter: selectedDuration - }), - ) - const parsedData = useMemo(() => { - if (!createdVsResolvedData?.data) return [] - return createdVsResolvedData.data.map((datum) => ({ - ...datum, - [datum.key]: datum.count, - name: renderFormattedDate(datum.key) - })) - }, [createdVsResolvedData]) + const { selectedDuration, selectedDurationLabel } = useAnalyticsV2() + const params = useParams(); + const { t } = useTranslation() + const workspaceSlug = params.workspaceSlug as string; + const { data: createdVsResolvedData, isLoading: isCreatedVsResolvedLoading } = useSWR( + `created-vs-resolved-${workspaceSlug}-${selectedDuration}`, + () => analyticsV2Service.getAdvanceAnalyticsCharts(workspaceSlug, 'work-items', { + date_filter: selectedDuration + }), + ) + const parsedData = useMemo(() => { + if (!createdVsResolvedData?.data) return [] + return createdVsResolvedData.data.map((datum) => ({ + ...datum, + [datum.key]: datum.count, + name: renderFormattedDate(datum.key) + })) + }, [createdVsResolvedData]) - const areas: TAreaItem[] = [ - { - key: "completed_issues", - label: "Resolved", - fill: "#19803833", - fillOpacity: 1, - stackId: "bar-one", - showDot: false, - smoothCurves: true, - strokeColor: "#198038", - strokeOpacity: 1, - }, - { - key: "created_issues", - label: "Created", - fill: "#1192E833", - fillOpacity: 1, - stackId: "bar-one", - showDot: false, - smoothCurves: true, - strokeColor: "#1192E8", - strokeOpacity: 1, - }, + const areas = useMemo(() => [ + { + key: "completed_issues", + label: "Resolved", + fill: "#19803833", + fillOpacity: 1, + stackId: "bar-one", + showDot: false, + smoothCurves: true, + strokeColor: "#198038", + strokeOpacity: 1, + }, + { + key: "created_issues", + label: "Created", + fill: "#1192E833", + fillOpacity: 1, + stackId: "bar-one", + showDot: false, + smoothCurves: true, + strokeColor: "#1192E8", + strokeOpacity: 1, + }, + ], []); - ] - - return ( - - - - ) + return ( + + {isCreatedVsResolvedLoading ? : + parsedData && parsedData.length > 0 ? + : + + } + + ) }) export default CreatedVsResolved \ No newline at end of file From 80d23f1572dd145aa40ec23d98480bca0b682009 Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Mon, 5 May 2025 02:14:02 +0530 Subject: [PATCH 37/69] added loaders --- .../i18n/src/locales/en/translations.json | 4 + .../components/analytics-v2/empty-state.tsx | 2 +- .../analytics-v2/insight-table/data-table.tsx | 236 +++++++++--------- web/core/components/analytics-v2/loaders.tsx | 4 +- .../work-items/created-vs-resolved.tsx | 4 +- .../work-items/priority-chart.tsx | 69 +++-- 6 files changed, 173 insertions(+), 146 deletions(-) diff --git a/packages/i18n/src/locales/en/translations.json b/packages/i18n/src/locales/en/translations.json index d27ea369fa1..9dc861be7ea 100644 --- a/packages/i18n/src/locales/en/translations.json +++ b/packages/i18n/src/locales/en/translations.json @@ -1173,6 +1173,10 @@ "created_vs_resolved": { "title": "No data yet", "description": "Work items created and resolved over time will show up here." + }, + "customized_insights": { + "title": "No data yet", + "description": "Work items assigned to you, broken down by state, will show up here." } } }, diff --git a/web/core/components/analytics-v2/empty-state.tsx b/web/core/components/analytics-v2/empty-state.tsx index d1ed8ee181e..f6f130e1c55 100644 --- a/web/core/components/analytics-v2/empty-state.tsx +++ b/web/core/components/analytics-v2/empty-state.tsx @@ -17,7 +17,7 @@ const AnalyticsV2EmptyState = ({ }: Props) => (
diff --git a/web/core/components/analytics-v2/insight-table/data-table.tsx b/web/core/components/analytics-v2/insight-table/data-table.tsx index 17efadf63e4..7ebbde6920f 100644 --- a/web/core/components/analytics-v2/insight-table/data-table.tsx +++ b/web/core/components/analytics-v2/insight-table/data-table.tsx @@ -2,133 +2,139 @@ import * as React from "react" import { - ColumnDef, - ColumnFiltersState, - SortingState, - VisibilityState, - flexRender, - getCoreRowModel, - getFacetedRowModel, - getFacetedUniqueValues, - getFilteredRowModel, - getPaginationRowModel, - getSortedRowModel, - useReactTable, + ColumnDef, + ColumnFiltersState, + SortingState, + VisibilityState, + flexRender, + getCoreRowModel, + getFacetedRowModel, + getFacetedUniqueValues, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, } from "@tanstack/react-table" import { Search } from "lucide-react" +import { useTranslation } from "@plane/i18n" import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, } from "@plane/propel/table" import { Input } from "@plane/ui" +import AnalyticsV2EmptyState from "../empty-state" interface DataTableProps { - columns: ColumnDef[] - data: TData[] - searchPlaceholder: string + columns: ColumnDef[] + data: TData[] + searchPlaceholder: string } export function DataTable({ - columns, - data, - searchPlaceholder, + columns, + data, + searchPlaceholder, }: DataTableProps) { - const [rowSelection, setRowSelection] = React.useState({}) - const [columnVisibility, setColumnVisibility] = - React.useState({}) - const [columnFilters, setColumnFilters] = React.useState( - [] - ) - const [sorting, setSorting] = React.useState([]) + const [rowSelection, setRowSelection] = React.useState({}) + const [columnVisibility, setColumnVisibility] = + React.useState({}) + const [columnFilters, setColumnFilters] = React.useState( + [] + ) + const [sorting, setSorting] = React.useState([]) + const { t } = useTranslation() - const table = useReactTable({ - data, - columns, - state: { - sorting, - columnVisibility, - rowSelection, - columnFilters, - }, - enableRowSelection: true, - onRowSelectionChange: setRowSelection, - onSortingChange: setSorting, - onColumnFiltersChange: setColumnFilters, - onColumnVisibilityChange: setColumnVisibility, - getCoreRowModel: getCoreRowModel(), - getFilteredRowModel: getFilteredRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getSortedRowModel: getSortedRowModel(), - getFacetedRowModel: getFacetedRowModel(), - getFacetedUniqueValues: getFacetedUniqueValues(), - }) + const table = useReactTable({ + data, + columns, + state: { + sorting, + columnVisibility, + rowSelection, + columnFilters, + }, + enableRowSelection: true, + onRowSelectionChange: setRowSelection, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + onColumnVisibilityChange: setColumnVisibility, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFacetedRowModel: getFacetedRowModel(), + getFacetedUniqueValues: getFacetedUniqueValues(), + }) - return ( -
- {table.getHeaderGroups()?.[0]?.headers?.[0]?.id &&
- { - table.getColumn(table.getHeaderGroups()?.[0].headers[0].id)?.setFilterValue(event.target.value) - }} - className="w-30 border-none" - /> - -
} -
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - ) as any} - - ))} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - ) as any} - - ))} - - )) - ) : ( - - - No results. - - - )} - -
-
-
- ) + return ( +
+ {table.getHeaderGroups()?.[0]?.headers?.[0]?.id &&
+ { + table.getColumn(table.getHeaderGroups()?.[0].headers[0].id)?.setFilterValue(event.target.value) + }} + className="w-30 border-none" + /> + +
} +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + ) as any} + + ))} + + ))} + + + {table.getRowModel().rows?.length > 0 ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + ) as any} + + ))} + + )) + ) : ( + + +
+ +
+
+
+ )} +
+
+
+
+ ) } \ No newline at end of file diff --git a/web/core/components/analytics-v2/loaders.tsx b/web/core/components/analytics-v2/loaders.tsx index bc251a35a3a..ebd51225b61 100644 --- a/web/core/components/analytics-v2/loaders.tsx +++ b/web/core/components/analytics-v2/loaders.tsx @@ -9,6 +9,8 @@ export const ProjectInsightsLoader = () => ( ); -export const AreaChartLoader = () => ( +export const ChartLoader = () => (
); + + diff --git a/web/core/components/analytics-v2/work-items/created-vs-resolved.tsx b/web/core/components/analytics-v2/work-items/created-vs-resolved.tsx index ebec0de6145..0f26e03cc44 100644 --- a/web/core/components/analytics-v2/work-items/created-vs-resolved.tsx +++ b/web/core/components/analytics-v2/work-items/created-vs-resolved.tsx @@ -11,7 +11,7 @@ import { useAnalyticsV2 } from '@/hooks/store/use-analytics-v2' import { AnalyticsV2Service } from '@/services/analytics-v2.service' import AnalyticsSectionWrapper from '../analytics-section-wrapper' import AnalyticsV2EmptyState from '../empty-state' -import { AreaChartLoader } from '../loaders' +import { ChartLoader } from '../loaders' @@ -63,7 +63,7 @@ const CreatedVsResolved = observer(() => { return ( - {isCreatedVsResolvedLoading ? : + {isCreatedVsResolvedLoading ? : parsedData && parsedData.length > 0 ? { const { selectedDuration, selectedProjects } = useAnalyticsV2() const params = useParams(); const { resolvedTheme } = useTheme(); + const { t } = useTranslation() const workspaceSlug = params.workspaceSlug as string; - const { data: customizedInsightsChartData } = useSWR( + const { data: priorityChartData, isLoading: priorityChartLoading } = useSWR( `customized-insights-chart-${workspaceSlug}-${selectedDuration}-${props.x_axis}-${props.y_axis}-${props.group_by}`, () => analyticsV2Service.getAdvanceAnalyticsCharts(workspaceSlug, 'custom-work-items', { date_filter: selectedDuration, @@ -33,8 +37,8 @@ const PriorityChart = observer((props: Props) => { ...props }), ) - const parsedData = useMemo(() => parseChartData(customizedInsightsChartData, props.x_axis, props.group_by, props.x_axis_date_grouping) - , [customizedInsightsChartData, props.x_axis, props.group_by, props.x_axis_date_grouping]) + const parsedData = useMemo(() => parseChartData(priorityChartData, props.x_axis, props.group_by, props.x_axis_date_grouping) + , [priorityChartData, props.x_axis, props.group_by, props.x_axis_date_grouping]) const chart_model = props.group_by ? EChartModels.STACKED : EChartModels.BASIC; @@ -114,32 +118,43 @@ const PriorityChart = observer((props: Props) => { const yAxisLabel = useMemo(() => ANALYTICS_V2_Y_AXIS_VALUES.find((item) => item.value === props.y_axis)?.label ?? props.y_axis, [props.y_axis]); const xAxisLabel = useMemo(() => props.x_axis === ChartXAxisProperty.PRIORITY ? "Priority" : props.x_axis, [props.x_axis]); + return (
- - + {priorityChartLoading ? : + parsedData.data && parsedData.data.length > 0 ? + <> + + + : + + }
) From 61ab58f57678166b32bcfe4e0a96ffe6b6436cdf Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Mon, 5 May 2025 15:33:27 +0530 Subject: [PATCH 38/69] added icons in filters --- packages/propel/src/charts/bar-chart/root.tsx | 2 +- .../analytics-section-wrapper.tsx | 2 +- .../analytics-v2/select/analytics-params.tsx | 13 +- .../analytics-v2/select/duration.tsx | 175 +++--------------- .../analytics-v2/select/project.tsx | 18 +- .../analytics-v2/select/select-x-axis.tsx | 6 +- .../analytics-v2/select/select-y-axis.tsx | 8 +- 7 files changed, 57 insertions(+), 167 deletions(-) diff --git a/packages/propel/src/charts/bar-chart/root.tsx b/packages/propel/src/charts/bar-chart/root.tsx index 889064ece74..a18dd042f23 100644 --- a/packages/propel/src/charts/bar-chart/root.tsx +++ b/packages/propel/src/charts/bar-chart/root.tsx @@ -79,7 +79,7 @@ export const BarChart = React.memo((props: T )), [activeLegend, stackKeys, bars] ); - console.log(xAxis.label, "test data") + return (
diff --git a/web/core/components/analytics-v2/analytics-section-wrapper.tsx b/web/core/components/analytics-v2/analytics-section-wrapper.tsx index a886dfed9cc..f88b8d39142 100644 --- a/web/core/components/analytics-v2/analytics-section-wrapper.tsx +++ b/web/core/components/analytics-v2/analytics-section-wrapper.tsx @@ -12,7 +12,7 @@ const AnalyticsSectionWrapper: React.FC = (props) => { const { title, children, className, subtitle, actions } = props return (
-
+
{title &&

{title}

{subtitle &&

• {subtitle}

} diff --git a/web/core/components/analytics-v2/select/analytics-params.tsx b/web/core/components/analytics-v2/select/analytics-params.tsx index d9cd22466b7..6fa8fee5154 100644 --- a/web/core/components/analytics-v2/select/analytics-params.tsx +++ b/web/core/components/analytics-v2/select/analytics-params.tsx @@ -2,12 +2,13 @@ import { useMemo } from "react"; import { observer } from "mobx-react"; import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form"; // plane imports -import { Download } from "lucide-react"; +import { Briefcase, Calendar, Download, Filter, SlidersHorizontal } from "lucide-react"; import { ANALYTICS_V2_X_AXIS_VALUES, ANALYTICS_V2_Y_AXIS_VALUES, ChartYAxisMetric } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { IAnalyticsV2Params } from "@plane/types"; import { Button, Row, setToast, TOAST_TYPE } from "@plane/ui"; // components +import { cn } from "@plane/utils"; import { AnalyticsV2Service } from "@/services/analytics-v2.service"; import { SelectXAxis } from "./select-x-axis"; import { SelectYAxis } from "./select-y-axis"; @@ -49,7 +50,7 @@ export const AnalyticsV2SelectParams: React.FC = observer((props) => { return (
= observer((props) => { onChange={(val) => { onChange(val); }} + label={
+ + {xAxisOptions.find((v) => v.value === value)?.label || "Add Property"} +
} options={xAxisOptions} /> )} @@ -87,6 +92,10 @@ export const AnalyticsV2SelectParams: React.FC = observer((props) => { onChange={(val) => { onChange(val); }} + label={
+ + {groupByOptions.find((v) => v.value === value)?.label || "Add Property"} +
} options={groupByOptions} placeholder="Group By" allowNoValue diff --git a/web/core/components/analytics-v2/select/duration.tsx b/web/core/components/analytics-v2/select/duration.tsx index 134972de46f..0a73ff077eb 100644 --- a/web/core/components/analytics-v2/select/duration.tsx +++ b/web/core/components/analytics-v2/select/duration.tsx @@ -1,19 +1,13 @@ // plane package imports -import React, { ReactNode, useRef, useState } from 'react' -import { usePopper } from 'react-popper' -import { Check, ChevronDown } from 'lucide-react' -import { Combobox } from '@headlessui/react' +import React, { ReactNode } from 'react' +import { Briefcase, Calendar } from 'lucide-react' import { ANALYTICS_V2_DURATION_FILTER_OPTIONS } from '@plane/constants' import { useTranslation } from '@plane/i18n' -import { ComboDropDown } from '@plane/ui' +import { CustomSearchSelect } from '@plane/ui' import { cn } from '@plane/utils' // plane web components // components -import { DropdownButton } from '@/components/dropdowns/buttons' -import { BUTTON_VARIANTS_WITH_TEXT } from '@/components/dropdowns/constants' import { TDropdownProps } from '@/components/dropdowns/types' -// hooks -import { useDropdown } from '@/hooks/use-dropdown' type Props = TDropdownProps & { value: string | null @@ -46,154 +40,29 @@ function DurationDropdown({ tabIndex, value }: Props) { - //states - const [isOpen, setIsOpen] = useState(false) - const [query, setQuery] = useState("") - //refs - const dropdownRef = useRef(null) - const inputRef = useRef(null) - //popper-js refs - const [referenceElement, setReferenceElement] = useState(null) - const [popperElement, setPopperElement] = useState(null) - // popper-js init - const { styles, attributes } = usePopper(referenceElement, popperElement, { - placement: placement ?? "bottom-start", - modifiers: [ - { - name: "preventOverflow", - options: { - padding: 12, - }, - }, - ], - }); - //store hooks const { t } = useTranslation() - const { handleOnClick, handleClose } = useDropdown({ - dropdownRef, - inputRef, - isOpen, - onClose, - query, - setIsOpen, - setQuery - }) - const filteredOptions = - query === "" ? ANALYTICS_V2_DURATION_FILTER_OPTIONS : ANALYTICS_V2_DURATION_FILTER_OPTIONS?.filter((o) => o?.name.toLowerCase().includes(query.toLowerCase())); - - const dropdownOnChange = (val: typeof ANALYTICS_V2_DURATION_FILTER_OPTIONS[number]["value"]) => { - onChange(val) - handleClose() - } - - const getDisplayName = (value: string | string[] | null, placeholder: string = "") => { - const option = filteredOptions?.find((o) => o?.value === value) - return option ? option?.name : placeholder; - - }; - - const comboButton = ( - <> - {button ? ( - - ) : ( - - )} - - ); + const options = ANALYTICS_V2_DURATION_FILTER_OPTIONS.map((option) => ({ + value: option.value, + query: option.name, + content: ( +
+ {option.name} +
+ ), + })); return ( - - {isOpen && - -
-
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( - filteredOptions.map((option) => { - if (!option) return; - return ( - - `w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${active ? "bg-custom-background-80" : "" - } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` - } - > - {({ selected }) => ( - <> - {option.name} - {selected && } - - )} - - ); - }) - ) : ( -

{t("no_matching_results")}

- ) - ) : ( -

{t("loading")}

- )} -
-
-
+ + + {value ? ANALYTICS_V2_DURATION_FILTER_OPTIONS.find(opt => opt.value === value)?.name : placeholder} +
} - + /> ) } diff --git a/web/core/components/analytics-v2/select/project.tsx b/web/core/components/analytics-v2/select/project.tsx index 6459eeb8f29..40bcc34c9f7 100644 --- a/web/core/components/analytics-v2/select/project.tsx +++ b/web/core/components/analytics-v2/select/project.tsx @@ -2,6 +2,7 @@ import { observer } from "mobx-react"; // hooks +import { Briefcase } from "lucide-react"; import { CustomSearchSelect } from "@plane/ui"; import { useProject } from "@/hooks/store"; // ui @@ -37,13 +38,16 @@ export const ProjectSelect: React.FC = observer((props) => { onChange={(val: string[]) => onChange(val)} options={options} label={ -
- {value && value.length > 0 - ? projectIds - ?.filter((p) => value.includes(p)) - .map((p) => getProjectById(p)?.name) - .join(", ") - : "All projects"} +
+ + {value && value.length > 3 ? + `3+ projects` + : value && value.length > 0 + ? projectIds + ?.filter((p) => value.includes(p)) + .map((p) => getProjectById(p)?.name) + .join(", ") + : "All projects"}
} multiple diff --git a/web/core/components/analytics-v2/select/select-x-axis.tsx b/web/core/components/analytics-v2/select/select-x-axis.tsx index a2163d7cda5..e9a371af462 100644 --- a/web/core/components/analytics-v2/select/select-x-axis.tsx +++ b/web/core/components/analytics-v2/select/select-x-axis.tsx @@ -1,5 +1,6 @@ "use client"; +import { Briefcase } from "lucide-react"; import { ChartXAxisProperty } from "@plane/constants"; import { IAnalyticsV2Params } from "@plane/types"; // ui @@ -13,14 +14,15 @@ type Props = { placeholder?: string; hiddenOptions?: ChartXAxisProperty[]; allowNoValue?: boolean; + label?: string | JSX.Element; }; export const SelectXAxis: React.FC = (props) => { - const { value, onChange, options, hiddenOptions, placeholder, allowNoValue } = props; + const { value, onChange, options, hiddenOptions, placeholder, allowNoValue, label } = props; return ( {options.find((v) => v.value === value)?.label || placeholder || "Add Property"}} + label={label} onChange={onChange} maxHeight="lg" > diff --git a/web/core/components/analytics-v2/select/select-y-axis.tsx b/web/core/components/analytics-v2/select/select-y-axis.tsx index d8f9a31bbee..c209f57aecf 100644 --- a/web/core/components/analytics-v2/select/select-y-axis.tsx +++ b/web/core/components/analytics-v2/select/select-y-axis.tsx @@ -3,6 +3,7 @@ import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // plane imports +import { Briefcase } from "lucide-react"; import { ChartYAxisMetric } from "@plane/constants"; import { CustomSelect } from "@plane/ui"; // hooks @@ -42,7 +43,12 @@ export const SelectYAxis: React.FC = observer(({ value, onChange, hiddenO return ( {options.find((v) => v.value === value)?.label ?? "Add Metric"}} + label={ +
+ + {options.find((v) => v.value === value)?.label ?? "Add Metric"} +
+ } onChange={onChange} maxHeight="lg" > From 3521fb133f33386b543212f35d6502ff63019b83 Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Mon, 5 May 2025 20:16:40 +0530 Subject: [PATCH 39/69] added custom colors in customised charts --- .../work-items/priority-chart.tsx | 24 ++++++--- .../analytics-v2/work-items/utils.ts | 50 +++++++++++++++++++ 2 files changed, 68 insertions(+), 6 deletions(-) create mode 100644 web/core/components/analytics-v2/work-items/utils.ts diff --git a/web/core/components/analytics-v2/work-items/priority-chart.tsx b/web/core/components/analytics-v2/work-items/priority-chart.tsx index f067b47faea..69436fa7bec 100644 --- a/web/core/components/analytics-v2/work-items/priority-chart.tsx +++ b/web/core/components/analytics-v2/work-items/priority-chart.tsx @@ -4,17 +4,20 @@ import { observer } from 'mobx-react' import { useParams } from 'next/navigation' import { useTheme } from 'next-themes' import useSWR from 'swr' -import { ANALYTICS_V2_Y_AXIS_VALUES, ChartXAxisDateGrouping, ChartXAxisProperty, ChartYAxisMetric, EChartModels } from '@plane/constants' +import { ANALYTICS_V2_X_AXIS_VALUES, ANALYTICS_V2_Y_AXIS_VALUES, ChartXAxisDateGrouping, ChartXAxisProperty, ChartYAxisMetric, EChartModels } from '@plane/constants' import { useTranslation } from '@plane/i18n' import { BarChart } from '@plane/propel/charts/bar-chart' import { IChartResponseV2 } from '@plane/types' import { TBarItem, TChartDatum } from '@plane/types/src/charts' import { CHART_COLOR_PALETTES, generateExtendedColors, parseChartData } from '@/components/chart/utils' +import { useLabel, useProjectState } from '@/hooks/store' import { useAnalyticsV2 } from '@/hooks/store/use-analytics-v2' +import { useWorkspaceIssueProperties } from '@/hooks/use-workspace-issue-properties' import { AnalyticsV2Service } from '@/services/analytics-v2.service' import AnalyticsV2EmptyState from '../empty-state' import { DataTable } from '../insight-table/data-table' import { ChartLoader } from '../loaders' +import { generateBarColor } from './utils' interface Props { x_axis: ChartXAxisProperty y_axis: ChartYAxisMetric @@ -24,11 +27,16 @@ interface Props { const analyticsV2Service = new AnalyticsV2Service() const PriorityChart = observer((props: Props) => { + const { x_axis, y_axis, group_by } = props; const { selectedDuration, selectedProjects } = useAnalyticsV2() const params = useParams(); const { resolvedTheme } = useTheme(); const { t } = useTranslation() + const { workspaceStates } = useProjectState() + const workspaceSlug = params.workspaceSlug as string; + useWorkspaceIssueProperties(workspaceSlug); + const { data: priorityChartData, isLoading: priorityChartLoading } = useSWR( `customized-insights-chart-${workspaceSlug}-${selectedDuration}-${props.x_axis}-${props.y_axis}-${props.group_by}`, () => analyticsV2Service.getAdvanceAnalyticsCharts(workspaceSlug, 'custom-work-items', { @@ -39,7 +47,6 @@ const PriorityChart = observer((props: Props) => { ) const parsedData = useMemo(() => parseChartData(priorityChartData, props.x_axis, props.group_by, props.x_axis_date_grouping) , [priorityChartData, props.x_axis, props.group_by, props.x_axis_date_grouping]) - const chart_model = props.group_by ? EChartModels.STACKED : EChartModels.BASIC; const bars: TBarItem[] = useMemo(() => { @@ -55,7 +62,13 @@ const PriorityChart = observer((props: Props) => { key: "count", label: "Count", stackId: "bar-one", - fill: "#049bdc", + fill: (payload) => + generateBarColor( + payload.key, + { x_axis, y_axis, group_by }, + baseColors, + workspaceStates + ), textClassName: "", showPercentage: false, showTopBorderRadius: () => true, @@ -63,7 +76,6 @@ const PriorityChart = observer((props: Props) => { }, ]; } else if (chart_model === EChartModels.STACKED && parsedData.schema) { - // get the extreme bars of a particular group, excluding the zero value bars const parsedExtremes: { [key: string]: { top: string | null; @@ -96,7 +108,7 @@ const PriorityChart = observer((props: Props) => { parsedBars = []; } return parsedBars; - }, [chart_model, parsedData.data, parsedData.schema, resolvedTheme]); + }, [chart_model, group_by, parsedData.data, parsedData.schema, resolvedTheme, workspaceStates, x_axis, y_axis]); const defaultColumns: ColumnDef[] = useMemo(() => [ { @@ -117,7 +129,7 @@ const PriorityChart = observer((props: Props) => { })), [parsedData.schema]); const yAxisLabel = useMemo(() => ANALYTICS_V2_Y_AXIS_VALUES.find((item) => item.value === props.y_axis)?.label ?? props.y_axis, [props.y_axis]); - const xAxisLabel = useMemo(() => props.x_axis === ChartXAxisProperty.PRIORITY ? "Priority" : props.x_axis, [props.x_axis]); + const xAxisLabel = useMemo(() => ANALYTICS_V2_X_AXIS_VALUES.find((item) => item.value === props.x_axis)?.label ?? props.x_axis, [props.x_axis]); return (
diff --git a/web/core/components/analytics-v2/work-items/utils.ts b/web/core/components/analytics-v2/work-items/utils.ts new file mode 100644 index 00000000000..f42c73f9f64 --- /dev/null +++ b/web/core/components/analytics-v2/work-items/utils.ts @@ -0,0 +1,50 @@ +import { ChartXAxisProperty, ChartYAxisMetric } from "@plane/constants"; +import { IState } from "@plane/types"; + +interface ParamsProps { + x_axis: ChartXAxisProperty + y_axis: ChartYAxisMetric + group_by?: ChartXAxisProperty +} + +export const generateBarColor = ( + value: string, + params: ParamsProps, + baseColors: string[], + workspaceStates?: IState[], +): string => { + + let color = baseColors[0] + // Priority + if (params.x_axis === ChartXAxisProperty.PRIORITY) { + color = + value === "urgent" + ? "#ef4444" + : value === "high" + ? "#f97316" + : value === "medium" + ? "#eab308" + : value === "low" + ? "#22c55e" + : "#ced4da"; + } + + // State + if (params.x_axis === ChartXAxisProperty.STATES) { + const state = workspaceStates?.find((s) => s.id === value) + if (state) { + color = state.color + } + } + + // Label + if (params.x_axis === ChartXAxisProperty.LABELS) { + const label = workspaceStates?.find((l) => l.id === value) + if (label) { + color = label.color + } + } + + + return color +}; \ No newline at end of file From 060df176aaa401c87bbeafe87b895c5f4a6f11ad Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Mon, 5 May 2025 20:24:46 +0530 Subject: [PATCH 40/69] cleaned up some code --- .../analytics-v2/select/analytics-params.tsx | 8 ++++---- .../analytics-v2/select/duration.tsx | 19 ++----------------- .../analytics-v2/select/select-x-axis.tsx | 5 +---- .../work-items/priority-chart.tsx | 2 +- 4 files changed, 8 insertions(+), 26 deletions(-) diff --git a/web/core/components/analytics-v2/select/analytics-params.tsx b/web/core/components/analytics-v2/select/analytics-params.tsx index 6fa8fee5154..36bf2844fb3 100644 --- a/web/core/components/analytics-v2/select/analytics-params.tsx +++ b/web/core/components/analytics-v2/select/analytics-params.tsx @@ -1,8 +1,8 @@ import { useMemo } from "react"; import { observer } from "mobx-react"; -import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form"; +import { Control, Controller, UseFormSetValue } from "react-hook-form"; // plane imports -import { Briefcase, Calendar, Download, Filter, SlidersHorizontal } from "lucide-react"; +import { Calendar, Download, SlidersHorizontal } from "lucide-react"; import { ANALYTICS_V2_X_AXIS_VALUES, ANALYTICS_V2_Y_AXIS_VALUES, ChartYAxisMetric } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { IAnalyticsV2Params } from "@plane/types"; @@ -15,7 +15,7 @@ import { SelectYAxis } from "./select-y-axis"; // hooks type Props = { - control: Control; + control: Control; setValue: UseFormSetValue; params: IAnalyticsV2Params; workspaceSlug: string; @@ -24,7 +24,7 @@ type Props = { const analyticsV2Service = new AnalyticsV2Service() export const AnalyticsV2SelectParams: React.FC = observer((props) => { - const { control, setValue, params, workspaceSlug } = props; + const { control, params, workspaceSlug } = props; const { t } = useTranslation(); const xAxisOptions = useMemo(() => ANALYTICS_V2_X_AXIS_VALUES.filter((option) => option.value !== params.group_by), [params.group_by]); const groupByOptions = useMemo(() => ANALYTICS_V2_X_AXIS_VALUES.filter((option) => option.value !== params.x_axis), [params.x_axis]); diff --git a/web/core/components/analytics-v2/select/duration.tsx b/web/core/components/analytics-v2/select/duration.tsx index 0a73ff077eb..6ad6a8d5244 100644 --- a/web/core/components/analytics-v2/select/duration.tsx +++ b/web/core/components/analytics-v2/select/duration.tsx @@ -1,10 +1,9 @@ // plane package imports import React, { ReactNode } from 'react' -import { Briefcase, Calendar } from 'lucide-react' +import { Calendar } from 'lucide-react' import { ANALYTICS_V2_DURATION_FILTER_OPTIONS } from '@plane/constants' import { useTranslation } from '@plane/i18n' import { CustomSearchSelect } from '@plane/ui' -import { cn } from '@plane/utils' // plane web components // components import { TDropdownProps } from '@/components/dropdowns/types' @@ -22,25 +21,11 @@ type Props = TDropdownProps & { } function DurationDropdown({ - buttonClassName, - buttonContainerClassName, - buttonVariant, - className, - disabled, - hideIcon, placeholder = "Duration", - onClose, - placement, onChange, - showTooltip = false, - dropdownArrow = false, - dropdownArrowClassName = "", - button, - renderByDefault = true, - tabIndex, value }: Props) { - const { t } = useTranslation() + useTranslation() const options = ANALYTICS_V2_DURATION_FILTER_OPTIONS.map((option) => ({ value: option.value, diff --git a/web/core/components/analytics-v2/select/select-x-axis.tsx b/web/core/components/analytics-v2/select/select-x-axis.tsx index e9a371af462..817992c442a 100644 --- a/web/core/components/analytics-v2/select/select-x-axis.tsx +++ b/web/core/components/analytics-v2/select/select-x-axis.tsx @@ -1,11 +1,8 @@ "use client"; -import { Briefcase } from "lucide-react"; import { ChartXAxisProperty } from "@plane/constants"; -import { IAnalyticsV2Params } from "@plane/types"; // ui import { CustomSelect } from "@plane/ui"; -import { cn } from "@plane/utils"; type Props = { value?: ChartXAxisProperty; @@ -18,7 +15,7 @@ type Props = { }; export const SelectXAxis: React.FC = (props) => { - const { value, onChange, options, hiddenOptions, placeholder, allowNoValue, label } = props; + const { value, onChange, options, hiddenOptions, allowNoValue, label } = props; return ( Date: Tue, 6 May 2025 14:14:45 +0530 Subject: [PATCH 41/69] added some responsiveness --- .../analytics-v2/overview/active-projects.tsx | 2 +- .../analytics-v2/overview/project-insights.tsx | 10 ++++------ web/core/components/analytics-v2/overview/root.tsx | 2 +- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/web/core/components/analytics-v2/overview/active-projects.tsx b/web/core/components/analytics-v2/overview/active-projects.tsx index 9798491122f..b2ffc81a56c 100644 --- a/web/core/components/analytics-v2/overview/active-projects.tsx +++ b/web/core/components/analytics-v2/overview/active-projects.tsx @@ -17,7 +17,7 @@ const ActiveProjects = observer(() => { fields: "total_work_items,total_completed_work_items" }) : null) return ( - +
{isProjectAnalyticsCountLoading && Array.from({ length: 5 }).map((_, index) => ( diff --git a/web/core/components/analytics-v2/overview/project-insights.tsx b/web/core/components/analytics-v2/overview/project-insights.tsx index 56c38a3e01a..a269b7b838d 100644 --- a/web/core/components/analytics-v2/overview/project-insights.tsx +++ b/web/core/components/analytics-v2/overview/project-insights.tsx @@ -1,9 +1,7 @@ -import { useMemo } from 'react' import { observer } from 'mobx-react' import dynamic from 'next/dynamic' import { useParams } from 'next/navigation' import useSWR from 'swr' -import { PROJECT_CREATED_AT_FILTER_OPTIONS } from '@plane/constants' import { useTranslation } from '@plane/i18n' import { TChartData } from '@plane/types' import { useAnalyticsV2 } from '@/hooks/store/use-analytics-v2' @@ -37,7 +35,7 @@ const ProjectInsights = observer(() => { return ( - + {isLoadingProjectInsight ? : projectInsightsData && projectInsightsData?.length == 0 ? { description={t('workspace_analytics.empty_state_v2.project_insights.description')} className='h-[200px]' /> : -
+
{projectInsightsData && { key: 'name', }} />} -
+
{t('workspace_analytics.summary_of_projects')}
{t('workspace_analytics.all_projects')}
diff --git a/web/core/components/analytics-v2/overview/root.tsx b/web/core/components/analytics-v2/overview/root.tsx index d44508bfbb6..1e05f26e5e3 100644 --- a/web/core/components/analytics-v2/overview/root.tsx +++ b/web/core/components/analytics-v2/overview/root.tsx @@ -10,7 +10,7 @@ const Overview: React.FC = () => (
-
+
From 93e7747b63fb67b5b54d98830f5e8f27d3e62fd6 Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Tue, 6 May 2025 19:21:07 +0530 Subject: [PATCH 42/69] updated translations --- .../i18n/src/locales/cs/translations.json | 80 +++++++---------- .../i18n/src/locales/de/translations.json | 39 +++++++-- .../i18n/src/locales/es/translations.json | 82 +++++++----------- .../i18n/src/locales/fr/translations.json | 82 +++++++----------- .../i18n/src/locales/id/translations.json | 82 +++++++----------- .../i18n/src/locales/it/translations.json | 82 +++++++----------- .../i18n/src/locales/ja/translations.json | 82 +++++++----------- .../i18n/src/locales/ko/translations.json | 82 +++++++----------- .../i18n/src/locales/pl/translations.json | 40 +++++++-- .../i18n/src/locales/pt-BR/translations.json | 81 +++++++----------- .../i18n/src/locales/ro/translations.json | 82 +++++++----------- .../i18n/src/locales/ru/translations.json | 80 +++++++---------- .../i18n/src/locales/sk/translations.json | 80 +++++++---------- .../i18n/src/locales/tr-TR/translations.json | 85 +++++++------------ .../i18n/src/locales/ua/translations.json | 39 +++++++-- .../i18n/src/locales/vi-VN/translations.json | 39 +++++++-- .../i18n/src/locales/zh-CN/translations.json | 82 +++++++----------- .../i18n/src/locales/zh-TW/translations.json | 82 +++++++----------- 18 files changed, 594 insertions(+), 707 deletions(-) diff --git a/packages/i18n/src/locales/cs/translations.json b/packages/i18n/src/locales/cs/translations.json index 51f55d90bd5..8da7b00e3e5 100644 --- a/packages/i18n/src/locales/cs/translations.json +++ b/packages/i18n/src/locales/cs/translations.json @@ -500,7 +500,6 @@ "export": "Exportovat", "member": "{count, plural, one{# člen} few{# členové} other{# členů}}", "new_password_must_be_different_from_old_password": "Nové heslo musí být odlišné od starého hesla", - "project_view": { "sort_by": { "created_at": "Vytvořeno dne", @@ -508,12 +507,10 @@ "name": "Název" } }, - "toast": { "success": "Úspěch!", "error": "Chyba!" }, - "links": { "toasts": { "created": { @@ -542,7 +539,6 @@ } } }, - "home": { "empty": { "quickstart_guide": "Váš průvodce rychlým startem", @@ -610,7 +606,6 @@ "title": "Domů", "star_us_on_github": "Ohodnoťte nás na GitHubu" }, - "link": { "modal": { "url": { @@ -624,7 +619,6 @@ } } }, - "common": { "all": "Vše", "states": "Stavy", @@ -872,20 +866,17 @@ "apply": "Použít", "applying": "Používání" }, - "chart": { "x_axis": "Osa X", "y_axis": "Osa Y", "metric": "Metrika" }, - "form": { "title": { "required": "Název je povinný", "max_length": "Název by měl být kratší než {length} znaků" } }, - "entity": { "grouping_title": "Seskupení {entity}", "priority": "Priorita {entity}", @@ -909,7 +900,6 @@ "failed": "Chyba při přidávání {entity}" } }, - "epic": { "all": "Všechny epiky", "label": "{count, plural, one {Epik} other {Epiky}}", @@ -927,7 +917,6 @@ "required": "Název epiku je povinný." } }, - "issue": { "label": "{count, plural, one {Pracovní položka} few {Pracovní položky} other {Pracovních položek}}", "all": "Všechny pracovní položky", @@ -1094,7 +1083,6 @@ }, "open_in_full_screen": "Otevřít pracovní položku na celou obrazovku" }, - "attachment": { "error": "Soubor nelze připojit. Zkuste to prosím znovu.", "only_one_file_allowed": "Je možné nahrát pouze jeden soubor najednou.", @@ -1102,7 +1090,6 @@ "drag_and_drop": "Přetáhněte soubor kamkoli pro nahrání", "delete": "Smazat přílohu" }, - "label": { "select": "Vybrat štítek", "create": { @@ -1112,7 +1099,6 @@ "type": "Zadejte pro vytvoření nového štítku" } }, - "sub_work_item": { "update": { "success": "Podřízená pracovní položka úspěšně aktualizována", @@ -1123,7 +1109,6 @@ "error": "Chyba při odebírání podřízené položky" } }, - "view": { "label": "{count, plural, one {Pohled} few {Pohledy} other {Pohledů}}", "create": { @@ -1133,7 +1118,6 @@ "label": "Aktualizovat pohled" } }, - "inbox_issue": { "status": { "pending": { @@ -1219,7 +1203,6 @@ } } }, - "workspace_creation": { "heading": "Vytvořte si pracovní prostor", "subheading": "Pro používání Plane musíte vytvořit nebo se připojit k pracovnímu prostoru.", @@ -1271,7 +1254,6 @@ } } }, - "workspace_dashboard": { "empty_state": { "general": { @@ -1287,7 +1269,6 @@ } } }, - "workspace_analytics": { "label": "Analytika", "page_label": "{workspace} - Analytika", @@ -1330,9 +1311,39 @@ } } } - } + }, + "empty_state_v2": { + "customized_insights": { + "description": "Pracovní položky přiřazené vám, rozdělené podle stavu, se zde zobrazí.", + "title": "Zatím žádná data" + }, + "created_vs_resolved": { + "description": "Pracovní položky vytvořené a vyřešené v průběhu času se zde zobrazí.", + "title": "Zatím žádná data" + }, + "project_insights": { + "title": "Zatím žádná data", + "description": "Pracovní položky přiřazené vám, rozdělené podle stavu, se zde zobrazí." + } + }, + "created_vs_resolved": "Vytvořeno vs Vyřešeno", + "customized_insights": "Přizpůsobené přehledy", + "backlog_work_items": "Pracovní položky v backlogu", + "active_projects": "Aktivní projekty", + "trend_on_charts": "Trend na grafech", + "all_projects": "Všechny projekty", + "summary_of_projects": "Souhrn projektů", + "project_insights": "Přehled projektu", + "started_work_items": "Zahájené pracovní položky", + "total_work_items": "Celkový počet pracovních položek", + "total_projects": "Celkový počet projektů", + "total_admins": "Celkový počet administrátorů", + "total_users": "Celkový počet uživatelů", + "total_intake": "Celkový příjem", + "un_started_work_items": "Nezahájené pracovní položky", + "total_guests": "Celkový počet hostů", + "completed_work_items": "Dokončené pracovní položky" }, - "workspace_projects": { "label": "{count, plural, one {Projekt} few {Projekty} other {Projektů}}", "create": { @@ -1407,7 +1418,6 @@ } } }, - "workspace_views": { "add_view": "Přidat pohled", "empty_state": { @@ -1442,7 +1452,6 @@ } } }, - "workspace_settings": { "label": "Nastavení pracovního prostoru", "page_label": "{workspace} - Obecná nastavení", @@ -1624,7 +1633,6 @@ } } }, - "profile": { "label": "Profil", "page_label": "Vaše práce", @@ -1687,7 +1695,6 @@ } } }, - "project_settings": { "general": { "enter_project_id": "Zadejte ID projektu", @@ -1833,7 +1840,6 @@ "auto_close_status": "Stav automatického uzavření" } }, - "empty_state": { "labels": { "title": "Zatím žádné štítky", @@ -1846,7 +1852,6 @@ } } }, - "project_cycles": { "add_cycle": "Přidat cyklus", "more_details": "Více detailů", @@ -1972,7 +1977,6 @@ } } }, - "project_issues": { "empty_state": { "no_issues": { @@ -2001,7 +2005,6 @@ } } }, - "project_module": { "add_module": "Přidat modul", "update_module": "Aktualizovat modul", @@ -2055,7 +2058,6 @@ } } }, - "project_views": { "empty_state": { "general": { @@ -2075,7 +2077,6 @@ } } }, - "project_page": { "empty_state": { "general": { @@ -2105,7 +2106,6 @@ } } }, - "command_k": { "empty_state": { "search": { @@ -2113,7 +2113,6 @@ } } }, - "issue_relation": { "empty_state": { "search": { @@ -2124,7 +2123,6 @@ } } }, - "issue_comment": { "empty_state": { "general": { @@ -2133,7 +2131,6 @@ } } }, - "notification": { "label": "Schránka", "page_label": "{workspace} - Schránka", @@ -2190,7 +2187,6 @@ "custom": "Vlastní" } }, - "active_cycle": { "empty_state": { "progress": { @@ -2210,7 +2206,6 @@ } } }, - "disabled_project": { "empty_state": { "inbox": { @@ -2273,7 +2268,6 @@ } } }, - "stickies": { "title": "Vaše poznámky", "placeholder": "kliknutím začněte psát", @@ -2331,7 +2325,6 @@ } } }, - "role_details": { "guest": { "title": "Host", @@ -2346,7 +2339,6 @@ "description": "Má všechna oprávnění v prostoru." } }, - "user_roles": { "product_or_project_manager": "Produktový/Projektový manažer", "development_or_engineering": "Vývoj/Inženýrství", @@ -2359,7 +2351,6 @@ "human_resources": "Lidské zdroje", "other": "Jiné" }, - "importer": { "github": { "title": "GitHub", @@ -2370,7 +2361,6 @@ "description": "Importujte položky a epiky z Jira." } }, - "exporter": { "csv": { "title": "CSV", @@ -2399,7 +2389,6 @@ "created": "Vytvořeno", "subscribed": "Odebíráno" }, - "themes": { "theme_options": { "system_preference": { @@ -2445,20 +2434,17 @@ "manual": "Ručně" } }, - "cycle": { "label": "{count, plural, one {Cyklus} few {Cykly} other {Cyklů}}", "no_cycle": "Žádný cyklus" }, - "module": { "label": "{count, plural, one {Modul} few {Moduly} other {Modulů}}", "no_module": "Žádný modul" }, - "description_versions": { "last_edited_by": "Naposledy upraveno uživatelem", "previously_edited_by": "Dříve upraveno uživatelem", "edited_by": "Upraveno uživatelem" } -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/de/translations.json b/packages/i18n/src/locales/de/translations.json index 28d64a8cc11..cf6e9cb4cc9 100644 --- a/packages/i18n/src/locales/de/translations.json +++ b/packages/i18n/src/locales/de/translations.json @@ -500,7 +500,6 @@ "export": "Exportieren", "member": "{count, plural, one{# Mitglied} few{# Mitglieder} other{# Mitglieder}}", "new_password_must_be_different_from_old_password": "Das neue Passwort muss von dem alten Passwort abweichen", - "project_view": { "sort_by": { "created_at": "Erstellt am", @@ -1312,7 +1311,38 @@ } } } - } + }, + "empty_state_v2": { + "customized_insights": { + "description": "Ihnen zugewiesene Arbeitselemente, aufgeschlüsselt nach Status, werden hier angezeigt.", + "title": "Noch keine Daten" + }, + "created_vs_resolved": { + "description": "Im Laufe der Zeit erstellte und gelöste Arbeitselemente werden hier angezeigt.", + "title": "Noch keine Daten" + }, + "project_insights": { + "title": "Noch keine Daten", + "description": "Ihnen zugewiesene Arbeitselemente, aufgeschlüsselt nach Status, werden hier angezeigt." + } + }, + "created_vs_resolved": "Erstellt vs Gelöst", + "customized_insights": "Individuelle Einblicke", + "backlog_work_items": "Backlog-Arbeitselemente", + "active_projects": "Aktive Projekte", + "trend_on_charts": "Trend in Diagrammen", + "all_projects": "Alle Projekte", + "summary_of_projects": "Projektübersicht", + "project_insights": "Projekteinblicke", + "started_work_items": "Begonnene Arbeitselemente", + "total_work_items": "Gesamte Arbeitselemente", + "total_projects": "Gesamtprojekte", + "total_admins": "Gesamtanzahl der Admins", + "total_users": "Gesamtanzahl der Benutzer", + "total_intake": "Gesamteinnahmen", + "un_started_work_items": "Nicht begonnene Arbeitselemente", + "total_guests": "Gesamtanzahl der Gäste", + "completed_work_items": "Abgeschlossene Arbeitselemente" }, "workspace_projects": { "label": "{count, plural, one {Projekt} few {Projekte} other {Projekte}}", @@ -2403,20 +2433,17 @@ "manual": "Manuell" } }, - "cycle": { "label": "{count, plural, one {Zyklus} few {Zyklen} other {Zyklen}}", "no_cycle": "Kein Zyklus" }, - "module": { "label": "{count, plural, one {Modul} few {Module} other {Module}}", "no_module": "Kein Modul" }, - "description_versions": { "last_edited_by": "Zuletzt bearbeitet von", "previously_edited_by": "Zuvor bearbeitet von", "edited_by": "Bearbeitet von" } -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/es/translations.json b/packages/i18n/src/locales/es/translations.json index 18147cf4e1d..50eec23ff26 100644 --- a/packages/i18n/src/locales/es/translations.json +++ b/packages/i18n/src/locales/es/translations.json @@ -18,7 +18,6 @@ "pro": "Pro", "upgrade": "Mejorar" }, - "auth": { "common": { "email": { @@ -168,7 +167,6 @@ } } }, - "submit": "Enviar", "cancel": "Cancelar", "loading": "Cargando", @@ -506,7 +504,6 @@ "new_password_must_be_different_from_old_password": "La nueva contraseña debe ser diferente a la contraseña anterior", "edited": "Modificado", "bot": "Bot", - "project_view": { "sort_by": { "created_at": "Creado el", @@ -514,12 +511,10 @@ "name": "Nombre" } }, - "toast": { "success": "¡Éxito!", "error": "¡Error!" }, - "links": { "toasts": { "created": { @@ -548,7 +543,6 @@ } } }, - "home": { "empty": { "quickstart_guide": "Guía de inicio rápido", @@ -616,7 +610,6 @@ "title": "Inicio", "star_us_on_github": "Danos una estrella en GitHub" }, - "link": { "modal": { "url": { @@ -630,7 +623,6 @@ } } }, - "common": { "all": "Todo", "states": "Estados", @@ -877,20 +869,17 @@ "apply": "Aplicar", "applying": "Aplicando" }, - "chart": { "x_axis": "Eje X", "y_axis": "Eje Y", "metric": "Métrica" }, - "form": { "title": { "required": "El título es obligatorio", "max_length": "El título debe tener menos de {length} caracteres" } }, - "entity": { "grouping_title": "Agrupación de {entity}", "priority": "Prioridad de {entity}", @@ -914,7 +903,6 @@ "failed": "Error al agregar {entity}" } }, - "epic": { "all": "Todos los Epics", "label": "{count, plural, one {Epic} other {Epics}}", @@ -932,7 +920,6 @@ "required": "El título del epic es obligatorio." } }, - "issue": { "label": "{count, plural, one {Elemento de trabajo} other {Elementos de trabajo}}", "all": "Todos los elementos de trabajo", @@ -1099,7 +1086,6 @@ }, "open_in_full_screen": "Abrir elemento de trabajo en pantalla completa" }, - "attachment": { "error": "No se pudo adjuntar el archivo. Intenta subirlo de nuevo.", "only_one_file_allowed": "Solo se puede subir un archivo a la vez.", @@ -1107,7 +1093,6 @@ "drag_and_drop": "Arrastra y suelta en cualquier lugar para subir", "delete": "Eliminar archivo adjunto" }, - "label": { "select": "Seleccionar etiqueta", "create": { @@ -1117,7 +1102,6 @@ "type": "Escribe para agregar una nueva etiqueta" } }, - "sub_work_item": { "update": { "success": "Sub-elemento actualizado correctamente", @@ -1128,7 +1112,6 @@ "error": "Error al eliminar el sub-elemento" } }, - "view": { "label": "{count, plural, one {Vista} other {Vistas}}", "create": { @@ -1138,7 +1121,6 @@ "label": "Actualizar vista" } }, - "inbox_issue": { "status": { "pending": { @@ -1224,7 +1206,6 @@ } } }, - "workspace_creation": { "heading": "Crea tu espacio de trabajo", "subheading": "Para comenzar a usar Plane, necesitas crear o unirte a un espacio de trabajo.", @@ -1276,7 +1257,6 @@ } } }, - "workspace_dashboard": { "empty_state": { "general": { @@ -1292,7 +1272,6 @@ } } }, - "workspace_analytics": { "label": "Análisis", "page_label": "{workspace} - Análisis", @@ -1335,9 +1314,39 @@ } } } - } + }, + "empty_state_v2": { + "customized_insights": { + "description": "Los elementos de trabajo asignados a ti, desglosados por estado, aparecerán aquí.", + "title": "Aún no hay datos" + }, + "created_vs_resolved": { + "description": "Los elementos de trabajo creados y resueltos con el tiempo aparecerán aquí.", + "title": "Aún no hay datos" + }, + "project_insights": { + "title": "Aún no hay datos", + "description": "Los elementos de trabajo asignados a ti, desglosados por estado, aparecerán aquí." + } + }, + "created_vs_resolved": "Creado vs Resuelto", + "customized_insights": "Información personalizada", + "backlog_work_items": "Elementos de trabajo en backlog", + "active_projects": "Proyectos activos", + "trend_on_charts": "Tendencia en gráficos", + "all_projects": "Todos los proyectos", + "summary_of_projects": "Resumen de proyectos", + "project_insights": "Información del proyecto", + "started_work_items": "Elementos de trabajo iniciados", + "total_work_items": "Total de elementos de trabajo", + "total_projects": "Total de proyectos", + "total_admins": "Total de administradores", + "total_users": "Total de usuarios", + "total_intake": "Ingreso total", + "un_started_work_items": "Elementos de trabajo no iniciados", + "total_guests": "Total de invitados", + "completed_work_items": "Elementos de trabajo completados" }, - "workspace_projects": { "label": "{count, plural, one {Proyecto} other {Proyectos}}", "create": { @@ -1411,7 +1420,6 @@ } } }, - "workspace_views": { "add_view": "Agregar vista", "empty_state": { @@ -1446,7 +1454,6 @@ } } }, - "workspace_settings": { "label": "Configuración del espacio de trabajo", "page_label": "{workspace} - Configuración general", @@ -1628,7 +1635,6 @@ } } }, - "profile": { "label": "Perfil", "page_label": "Tu trabajo", @@ -1691,7 +1697,6 @@ } } }, - "project_settings": { "general": { "enter_project_id": "Ingresa el ID del proyecto", @@ -1837,7 +1842,6 @@ "auto_close_status": "Estado de cierre automático" } }, - "empty_state": { "labels": { "title": "Aún no hay etiquetas", @@ -1850,7 +1854,6 @@ } } }, - "project_cycles": { "add_cycle": "Agregar ciclo", "more_details": "Más detalles", @@ -1976,7 +1979,6 @@ } } }, - "project_issues": { "empty_state": { "no_issues": { @@ -2005,7 +2007,6 @@ } } }, - "project_module": { "add_module": "Agregar Módulo", "update_module": "Actualizar Módulo", @@ -2059,7 +2060,6 @@ } } }, - "project_views": { "empty_state": { "general": { @@ -2079,7 +2079,6 @@ } } }, - "project_page": { "empty_state": { "general": { @@ -2109,7 +2108,6 @@ } } }, - "command_k": { "empty_state": { "search": { @@ -2117,7 +2115,6 @@ } } }, - "issue_relation": { "empty_state": { "search": { @@ -2128,7 +2125,6 @@ } } }, - "issue_comment": { "empty_state": { "general": { @@ -2137,7 +2133,6 @@ } } }, - "notification": { "label": "Bandeja de entrada", "page_label": "{workspace} - Bandeja de entrada", @@ -2194,7 +2189,6 @@ "custom": "Personalizado" } }, - "active_cycle": { "empty_state": { "progress": { @@ -2214,7 +2208,6 @@ } } }, - "disabled_project": { "empty_state": { "inbox": { @@ -2277,7 +2270,6 @@ } } }, - "stickies": { "title": "Tus notas adhesivas", "placeholder": "haz clic para escribir aquí", @@ -2335,7 +2327,6 @@ } } }, - "role_details": { "guest": { "title": "Invitado", @@ -2350,7 +2341,6 @@ "description": "Todos los permisos establecidos como verdaderos dentro del espacio de trabajo." } }, - "user_roles": { "product_or_project_manager": "Gerente de Producto / Proyecto", "development_or_engineering": "Desarrollo / Ingeniería", @@ -2363,7 +2353,6 @@ "human_resources": "Recursos Humanos", "other": "Otro" }, - "importer": { "github": { "title": "GitHub", @@ -2374,7 +2363,6 @@ "description": "Importa elementos de trabajo y epics desde proyectos y epics de Jira." } }, - "exporter": { "csv": { "title": "CSV", @@ -2403,7 +2391,6 @@ "created": "Creados", "subscribed": "Suscritos" }, - "themes": { "theme_options": { "system_preference": { @@ -2449,20 +2436,17 @@ "manual": "Manual" } }, - "cycle": { "label": "{count, plural, one {Ciclo} other {Ciclos}}", "no_cycle": "Sin ciclo" }, - "module": { "label": "{count, plural, one {Módulo} other {Módulos}}", "no_module": "Sin módulo" }, - "description_versions": { "last_edited_by": "Última edición por", "previously_edited_by": "Editado anteriormente por", "edited_by": "Editado por" } -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/fr/translations.json b/packages/i18n/src/locales/fr/translations.json index 9d7fa10f8bd..931cd376bad 100644 --- a/packages/i18n/src/locales/fr/translations.json +++ b/packages/i18n/src/locales/fr/translations.json @@ -18,7 +18,6 @@ "pro": "Pro", "upgrade": "Mettre à niveau" }, - "auth": { "common": { "email": { @@ -168,7 +167,6 @@ } } }, - "submit": "Soumettre", "cancel": "Annuler", "loading": "Chargement", @@ -504,7 +502,6 @@ "new_password_must_be_different_from_old_password": "Le nouveau mot de passe doit être différent du mot de passe précédent", "edited": "Modifié", "bot": "Bot", - "project_view": { "sort_by": { "created_at": "Créé le", @@ -512,12 +509,10 @@ "name": "Nom" } }, - "toast": { "success": "Succès !", "error": "Erreur !" }, - "links": { "toasts": { "created": { @@ -546,7 +541,6 @@ } } }, - "home": { "empty": { "quickstart_guide": "Guide de démarrage rapide", @@ -614,7 +608,6 @@ "title": "Accueil", "star_us_on_github": "Donnez-nous une étoile sur GitHub" }, - "link": { "modal": { "url": { @@ -628,7 +621,6 @@ } } }, - "common": { "all": "Tout", "states": "États", @@ -875,20 +867,17 @@ "apply": "Appliquer", "applying": "Application" }, - "chart": { "x_axis": "Axe X", "y_axis": "Axe Y", "metric": "Métrique" }, - "form": { "title": { "required": "Le titre est requis", "max_length": "Le titre doit contenir moins de {length} caractères" } }, - "entity": { "grouping_title": "Regroupement {entity}", "priority": "Priorité {entity}", @@ -912,7 +901,6 @@ "failed": "Erreur lors de l'ajout de {entity}" } }, - "epic": { "all": "Tous les Epics", "label": "{count, plural, one {Epic} other {Epics}}", @@ -930,7 +918,6 @@ "required": "Le titre de l'Epic est requis." } }, - "issue": { "label": "{count, plural, one {Élément de travail} other {Éléments de travail}}", "all": "Tous les éléments de travail", @@ -1097,7 +1084,6 @@ }, "open_in_full_screen": "Ouvrir l'élément de travail en plein écran" }, - "attachment": { "error": "Le fichier n'a pas pu être joint. Essayez de le télécharger à nouveau.", "only_one_file_allowed": "Un seul fichier peut être téléchargé à la fois.", @@ -1105,7 +1091,6 @@ "drag_and_drop": "Glissez-déposez n'importe où pour télécharger", "delete": "Supprimer la pièce jointe" }, - "label": { "select": "Sélectionner une étiquette", "create": { @@ -1115,7 +1100,6 @@ "type": "Tapez pour ajouter une nouvelle étiquette" } }, - "sub_work_item": { "update": { "success": "Sous-élément de travail mis à jour avec succès", @@ -1126,7 +1110,6 @@ "error": "Erreur lors de la suppression du sous-élément de travail" } }, - "view": { "label": "{count, plural, one {Vue} other {Vues}}", "create": { @@ -1136,7 +1119,6 @@ "label": "Mettre à jour la vue" } }, - "inbox_issue": { "status": { "pending": { @@ -1222,7 +1204,6 @@ } } }, - "workspace_creation": { "heading": "Créez votre espace de travail", "subheading": "Pour commencer à utiliser Plane, vous devez créer ou rejoindre un espace de travail.", @@ -1274,7 +1255,6 @@ } } }, - "workspace_dashboard": { "empty_state": { "general": { @@ -1290,7 +1270,6 @@ } } }, - "workspace_analytics": { "label": "Analytique", "page_label": "{workspace} - Analytique", @@ -1333,9 +1312,39 @@ } } } - } + }, + "empty_state_v2": { + "customized_insights": { + "description": "Les éléments de travail qui vous sont assignés, répartis par état, s'afficheront ici.", + "title": "Pas encore de données" + }, + "created_vs_resolved": { + "description": "Les éléments de travail créés et résolus au fil du temps s'afficheront ici.", + "title": "Pas encore de données" + }, + "project_insights": { + "title": "Pas encore de données", + "description": "Les éléments de travail qui vous sont assignés, répartis par état, s'afficheront ici." + } + }, + "created_vs_resolved": "Créé vs Résolu", + "customized_insights": "Informations personnalisées", + "backlog_work_items": "Éléments de travail en backlog", + "active_projects": "Projets actifs", + "trend_on_charts": "Tendance sur les graphiques", + "all_projects": "Tous les projets", + "summary_of_projects": "Résumé des projets", + "project_insights": "Aperçus du projet", + "started_work_items": "Éléments de travail commencés", + "total_work_items": "Total des éléments de travail", + "total_projects": "Total des projets", + "total_admins": "Total des administrateurs", + "total_users": "Nombre total d'utilisateurs", + "total_intake": "Revenu total", + "un_started_work_items": "Éléments de travail non commencés", + "total_guests": "Nombre total d'invités", + "completed_work_items": "Éléments de travail terminés" }, - "workspace_projects": { "label": "{count, plural, one {Projet} other {Projets}}", "create": { @@ -1409,7 +1418,6 @@ } } }, - "workspace_views": { "add_view": "Ajouter une vue", "empty_state": { @@ -1444,7 +1452,6 @@ } } }, - "workspace_settings": { "label": "Paramètres de l'espace de travail", "page_label": "{workspace} - Paramètres généraux", @@ -1626,7 +1633,6 @@ } } }, - "profile": { "label": "Profil", "page_label": "Votre travail", @@ -1689,7 +1695,6 @@ } } }, - "project_settings": { "general": { "enter_project_id": "Saisissez l'ID du projet", @@ -1835,7 +1840,6 @@ "auto_close_status": "Statut de fermeture automatique" } }, - "empty_state": { "labels": { "title": "Pas encore d'étiquettes", @@ -1848,7 +1852,6 @@ } } }, - "project_cycles": { "add_cycle": "Ajouter un cycle", "more_details": "Plus de détails", @@ -1974,7 +1977,6 @@ } } }, - "project_issues": { "empty_state": { "no_issues": { @@ -2003,7 +2005,6 @@ } } }, - "project_module": { "add_module": "Ajouter un module", "update_module": "Mettre à jour le module", @@ -2057,7 +2058,6 @@ } } }, - "project_views": { "empty_state": { "general": { @@ -2077,7 +2077,6 @@ } } }, - "project_page": { "empty_state": { "general": { @@ -2107,7 +2106,6 @@ } } }, - "command_k": { "empty_state": { "search": { @@ -2115,7 +2113,6 @@ } } }, - "issue_relation": { "empty_state": { "search": { @@ -2126,7 +2123,6 @@ } } }, - "issue_comment": { "empty_state": { "general": { @@ -2135,7 +2131,6 @@ } } }, - "notification": { "label": "Boîte de réception", "page_label": "{workspace} - Boîte de réception", @@ -2192,7 +2187,6 @@ "custom": "Personnalisé" } }, - "active_cycle": { "empty_state": { "progress": { @@ -2212,7 +2206,6 @@ } } }, - "disabled_project": { "empty_state": { "inbox": { @@ -2275,7 +2268,6 @@ } } }, - "stickies": { "title": "Vos notes adhésives", "placeholder": "cliquez pour écrire ici", @@ -2333,7 +2325,6 @@ } } }, - "role_details": { "guest": { "title": "Invité", @@ -2348,7 +2339,6 @@ "description": "Toutes les permissions sont activées dans l'espace de travail." } }, - "user_roles": { "product_or_project_manager": "Chef de produit / Chef de projet", "development_or_engineering": "Développement / Ingénierie", @@ -2361,7 +2351,6 @@ "human_resources": "Ressources Humaines", "other": "Autre" }, - "importer": { "github": { "title": "GitHub", @@ -2372,7 +2361,6 @@ "description": "Importez des éléments de travail et des epics depuis les projets et epics Jira." } }, - "exporter": { "csv": { "title": "CSV", @@ -2401,7 +2389,6 @@ "created": "Créés", "subscribed": "Suivis" }, - "themes": { "theme_options": { "system_preference": { @@ -2447,20 +2434,17 @@ "manual": "Manuel" } }, - "cycle": { "label": "{count, plural, one {Cycle} other {Cycles}}", "no_cycle": "Pas de cycle" }, - "module": { "label": "{count, plural, one {Module} other {Modules}}", "no_module": "Pas de module" }, - "description_versions": { "last_edited_by": "Dernière modification par", "previously_edited_by": "Précédemment modifié par", "edited_by": "Modifié par" } -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/id/translations.json b/packages/i18n/src/locales/id/translations.json index 5a01933455f..9ea6b613e7b 100644 --- a/packages/i18n/src/locales/id/translations.json +++ b/packages/i18n/src/locales/id/translations.json @@ -18,7 +18,6 @@ "pro": "Pro", "upgrade": "Upgrade" }, - "auth": { "common": { "email": { @@ -168,7 +167,6 @@ } } }, - "submit": "Kirim", "cancel": "Batal", "loading": "Memuat", @@ -502,7 +500,6 @@ "export": "Ekspor", "member": "{count, plural, one{# anggota} other{# anggota}}", "new_password_must_be_different_from_old_password": "Kata sandi baru harus berbeda dari kata sandi lama", - "project_view": { "sort_by": { "created_at": "Dibuat pada", @@ -510,12 +507,10 @@ "name": "Nama" } }, - "toast": { "success": "Sukses!", "error": "Kesalahan!" }, - "links": { "toasts": { "created": { @@ -544,7 +539,6 @@ } } }, - "home": { "empty": { "quickstart_guide": "Panduan pemula Anda", @@ -612,7 +606,6 @@ "title": "Beranda", "star_us_on_github": "Bintang kami di GitHub" }, - "link": { "modal": { "url": { @@ -626,7 +619,6 @@ } } }, - "common": { "all": "Semua", "states": "Negara-negara", @@ -874,20 +866,17 @@ "apply": "Terapkan", "applying": "Terapkan" }, - "chart": { "x_axis": "Sumbu-X", "y_axis": "Sumbu-Y", "metric": "Metrik" }, - "form": { "title": { "required": "Judul wajib diisi", "max_length": "Judul harus kurang dari {length} karakter" } }, - "entity": { "grouping_title": "Pengelompokan {entity}", "priority": "Prioritas {entity}", @@ -911,7 +900,6 @@ "failed": "Terjadi kesalahan saat menambahkan {entity}" } }, - "epic": { "all": "Semua Epik", "label": "{count, plural, one {Epik} other {Epik}}", @@ -929,7 +917,6 @@ "required": "Judul epik wajib diisi." } }, - "issue": { "label": "{count, plural, one {Item Kerja} other {Item Kerja}}", "all": "Semua Item Kerja", @@ -1096,7 +1083,6 @@ }, "open_in_full_screen": "Buka item kerja dalam layar penuh" }, - "attachment": { "error": "File tidak dapat dilampirkan. Coba unggah lagi.", "only_one_file_allowed": "Hanya satu file yang dapat diunggah pada satu waktu.", @@ -1104,7 +1090,6 @@ "drag_and_drop": "Seret dan jatuhkan di mana saja untuk mengunggah", "delete": "Hapus lampiran" }, - "label": { "select": "Pilih label", "create": { @@ -1114,7 +1099,6 @@ "type": "Ketik untuk menambah label baru" } }, - "sub_work_item": { "update": { "success": "Sub-item kerja berhasil diperbarui", @@ -1125,7 +1109,6 @@ "error": "Kesalahan saat menghapus sub-item kerja" } }, - "view": { "label": "{count, plural, one {Tampilan} other {Tampilan}}", "create": { @@ -1135,7 +1118,6 @@ "label": "Perbarui Tampilan" } }, - "inbox_issue": { "status": { "pending": { @@ -1221,7 +1203,6 @@ } } }, - "workspace_creation": { "heading": "Buat ruang kerja Anda", "subheading": "Untuk mulai menggunakan Plane, Anda perlu membuat atau bergabung dengan ruang kerja.", @@ -1273,7 +1254,6 @@ } } }, - "workspace_dashboard": { "empty_state": { "general": { @@ -1289,7 +1269,6 @@ } } }, - "workspace_analytics": { "label": "Analitik", "page_label": "{workspace} - Analitik", @@ -1332,9 +1311,39 @@ } } } - } + }, + "empty_state_v2": { + "customized_insights": { + "description": "Item pekerjaan yang ditugaskan kepada Anda, dipecah berdasarkan status, akan muncul di sini.", + "title": "Belum ada data" + }, + "created_vs_resolved": { + "description": "Item pekerjaan yang dibuat dan diselesaikan dari waktu ke waktu akan muncul di sini.", + "title": "Belum ada data" + }, + "project_insights": { + "title": "Belum ada data", + "description": "Item pekerjaan yang ditugaskan kepada Anda, dipecah berdasarkan status, akan muncul di sini." + } + }, + "created_vs_resolved": "Dibuat vs Diselesaikan", + "customized_insights": "Wawasan yang Disesuaikan", + "backlog_work_items": "Item pekerjaan backlog", + "active_projects": "Proyek Aktif", + "trend_on_charts": "Tren pada grafik", + "all_projects": "Semua Proyek", + "summary_of_projects": "Ringkasan Proyek", + "project_insights": "Wawasan Proyek", + "started_work_items": "Item pekerjaan yang telah dimulai", + "total_work_items": "Total item pekerjaan", + "total_projects": "Total Proyek", + "total_admins": "Total Admin", + "total_users": "Total Pengguna", + "total_intake": "Total Pemasukan", + "un_started_work_items": "Item pekerjaan yang belum dimulai", + "total_guests": "Total Tamu", + "completed_work_items": "Item pekerjaan yang telah selesai" }, - "workspace_projects": { "label": "{count, plural, one {Proyek} other {Proyek}}", "create": { @@ -1409,7 +1418,6 @@ } } }, - "workspace_views": { "add_view": "Tambah tampilan", "empty_state": { @@ -1444,7 +1452,6 @@ } } }, - "workspace_settings": { "label": "Pengaturan ruang kerja", "page_label": "{workspace} - Pengaturan Umum", @@ -1626,7 +1633,6 @@ } } }, - "profile": { "label": "Profil", "page_label": "Pekerjaan Anda", @@ -1689,7 +1695,6 @@ } } }, - "project_settings": { "general": { "enter_project_id": "Masukkan ID proyek", @@ -1835,7 +1840,6 @@ "auto_close_status": "Status penutupan otomatis" } }, - "empty_state": { "labels": { "title": "Belum ada label", @@ -1848,7 +1852,6 @@ } } }, - "project_cycles": { "add_cycle": "Tambah siklus", "more_details": "Detail lebih lanjut", @@ -1968,7 +1971,6 @@ } } }, - "project_issues": { "empty_state": { "no_issues": { @@ -1997,7 +1999,6 @@ } } }, - "project_module": { "add_module": "Tambah Modul", "update_module": "Perbarui Modul", @@ -2051,7 +2052,6 @@ } } }, - "project_views": { "empty_state": { "general": { @@ -2071,7 +2071,6 @@ } } }, - "project_page": { "empty_state": { "general": { @@ -2101,7 +2100,6 @@ } } }, - "command_k": { "empty_state": { "search": { @@ -2109,7 +2107,6 @@ } } }, - "issue_relation": { "empty_state": { "search": { @@ -2120,7 +2117,6 @@ } } }, - "issue_comment": { "empty_state": { "general": { @@ -2129,7 +2125,6 @@ } } }, - "notification": { "label": "Kotak Masuk", "page_label": "{workspace} - Kotak Masuk", @@ -2186,7 +2181,6 @@ "custom": "Kustom" } }, - "active_cycle": { "empty_state": { "progress": { @@ -2206,7 +2200,6 @@ } } }, - "disabled_project": { "empty_state": { "inbox": { @@ -2269,7 +2262,6 @@ } } }, - "stickies": { "title": "Catatan tempel Anda", "placeholder": "klik untuk mengetik di sini", @@ -2327,7 +2319,6 @@ } } }, - "role_details": { "guest": { "title": "Tamu", @@ -2342,7 +2333,6 @@ "description": "Semua izin diatur ke true dalam ruang kerja." } }, - "user_roles": { "product_or_project_manager": "Manajer Produk / Proyek", "development_or_engineering": "Pengembangan / Rekayasa", @@ -2355,7 +2345,6 @@ "human_resources": "Sumber Daya Manusia", "other": "Lainnya" }, - "importer": { "github": { "title": "Github", @@ -2366,7 +2355,6 @@ "description": "Impor item kerja dan epik dari proyek dan epik Jira." } }, - "exporter": { "csv": { "title": "CSV", @@ -2395,7 +2383,6 @@ "created": "Dibuat", "subscribed": "Disubscribe" }, - "themes": { "theme_options": { "system_preference": { @@ -2441,20 +2428,17 @@ "manual": "Manual" } }, - "cycle": { "label": "{count, plural, one {Siklus} other {Siklus}}", "no_cycle": "Tidak ada siklus" }, - "module": { "label": "{count, plural, one {Modul} other {Modul}}", "no_module": "Tidak ada modul" }, - "description_versions": { "last_edited_by": "Terakhir disunting oleh", "previously_edited_by": "Sebelumnya disunting oleh", "edited_by": "Disunting oleh" } -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/it/translations.json b/packages/i18n/src/locales/it/translations.json index 8a341f173eb..ba4e729252a 100644 --- a/packages/i18n/src/locales/it/translations.json +++ b/packages/i18n/src/locales/it/translations.json @@ -18,7 +18,6 @@ "pro": "Pro", "upgrade": "Aggiorna" }, - "auth": { "common": { "email": { @@ -501,10 +500,8 @@ "export": "Esporta", "member": "{count, plural, one {# membro} other {# membri}}", "new_password_must_be_different_from_old_password": "La nuova password deve essere diversa dalla password precedente", - "edited": "Modificato", "bot": "Bot", - "project_view": { "sort_by": { "created_at": "Creato il", @@ -512,12 +509,10 @@ "name": "Nome" } }, - "toast": { "success": "Successo!", "error": "Errore!" }, - "links": { "toasts": { "created": { @@ -546,7 +541,6 @@ } } }, - "home": { "empty": { "quickstart_guide": "La tua guida rapida", @@ -614,7 +608,6 @@ "title": "Home", "star_us_on_github": "Metti una stella su GitHub" }, - "link": { "modal": { "url": { @@ -628,7 +621,6 @@ } } }, - "common": { "all": "Tutti", "states": "Stati", @@ -873,20 +865,17 @@ "apply": "Applica", "applying": "Applicazione" }, - "chart": { "x_axis": "Asse X", "y_axis": "Asse Y", "metric": "Metrica" }, - "form": { "title": { "required": "Il titolo è obbligatorio", "max_length": "Il titolo deve contenere meno di {length} caratteri" } }, - "entity": { "grouping_title": "Raggruppamento di {entity}", "priority": "Priorità di {entity}", @@ -910,7 +899,6 @@ "failed": "Errore nell'aggiunta di {entity}" } }, - "epic": { "all": "Tutti gli Epic", "label": "{count, plural, one {Epic} other {Epic}}", @@ -928,7 +916,6 @@ "required": "Il titolo dell'Epic è obbligatorio." } }, - "issue": { "label": "{count, plural, one {Elemento di lavoro} other {Elementi di lavoro}}", "all": "Tutti gli elementi di lavoro", @@ -1095,7 +1082,6 @@ }, "open_in_full_screen": "Apri l'elemento di lavoro a schermo intero" }, - "attachment": { "error": "Impossibile allegare il file. Riprova a caricarlo.", "only_one_file_allowed": "È possibile caricare un solo file alla volta.", @@ -1103,7 +1089,6 @@ "drag_and_drop": "Trascina e rilascia ovunque per caricare", "delete": "Elimina allegato" }, - "label": { "select": "Seleziona etichetta", "create": { @@ -1113,7 +1098,6 @@ "type": "Digita per aggiungere una nuova etichetta" } }, - "sub_work_item": { "update": { "success": "Sotto-elemento di lavoro aggiornato con successo", @@ -1124,7 +1108,6 @@ "error": "Errore nella rimozione del sotto-elemento di lavoro" } }, - "view": { "label": "{count, plural, one {Visualizzazione} other {Visualizzazioni}}", "create": { @@ -1134,7 +1117,6 @@ "label": "Aggiorna visualizzazione" } }, - "inbox_issue": { "status": { "pending": { @@ -1220,7 +1202,6 @@ } } }, - "workspace_creation": { "heading": "Crea il tuo spazio di lavoro", "subheading": "Per iniziare a usare Plane, devi creare o unirti a uno spazio di lavoro.", @@ -1272,7 +1253,6 @@ } } }, - "workspace_dashboard": { "empty_state": { "general": { @@ -1288,7 +1268,6 @@ } } }, - "workspace_analytics": { "label": "Analisi", "page_label": "{workspace} - Analisi", @@ -1331,9 +1310,39 @@ } } } - } + }, + "empty_state_v2": { + "customized_insights": { + "description": "Gli elementi di lavoro assegnati a te, suddivisi per stato, verranno visualizzati qui.", + "title": "Nessun dato disponibile" + }, + "created_vs_resolved": { + "description": "Gli elementi di lavoro creati e risolti nel tempo verranno visualizzati qui.", + "title": "Nessun dato disponibile" + }, + "project_insights": { + "title": "Nessun dato disponibile", + "description": "Gli elementi di lavoro assegnati a te, suddivisi per stato, verranno visualizzati qui." + } + }, + "created_vs_resolved": "Creato vs Risolto", + "customized_insights": "Approfondimenti personalizzati", + "backlog_work_items": "Elementi di lavoro nel backlog", + "active_projects": "Progetti attivi", + "trend_on_charts": "Tendenza nei grafici", + "all_projects": "Tutti i progetti", + "summary_of_projects": "Riepilogo dei progetti", + "project_insights": "Approfondimenti sul progetto", + "started_work_items": "Elementi di lavoro iniziati", + "total_work_items": "Totale elementi di lavoro", + "total_projects": "Progetti totali", + "total_admins": "Totale amministratori", + "total_users": "Totale utenti", + "total_intake": "Entrate totali", + "un_started_work_items": "Elementi di lavoro non avviati", + "total_guests": "Totale ospiti", + "completed_work_items": "Elementi di lavoro completati" }, - "workspace_projects": { "label": "{count, plural, one {Progetto} other {Progetti}}", "create": { @@ -1408,7 +1417,6 @@ } } }, - "workspace_views": { "add_view": "Aggiungi visualizzazione", "empty_state": { @@ -1443,7 +1451,6 @@ } } }, - "workspace_settings": { "label": "Impostazioni dello spazio di lavoro", "page_label": "{workspace} - Impostazioni generali", @@ -1625,7 +1632,6 @@ } } }, - "profile": { "label": "Profilo", "page_label": "Il tuo lavoro", @@ -1688,7 +1694,6 @@ } } }, - "project_settings": { "general": { "enter_project_id": "Inserisci l'ID del progetto", @@ -1834,7 +1839,6 @@ "auto_close_status": "Stato di chiusura automatica" } }, - "empty_state": { "labels": { "title": "Nessuna etichetta ancora", @@ -1847,7 +1851,6 @@ } } }, - "project_cycles": { "add_cycle": "Aggiungi ciclo", "more_details": "Altri dettagli", @@ -1973,7 +1976,6 @@ } } }, - "project_issues": { "empty_state": { "no_issues": { @@ -2002,7 +2004,6 @@ } } }, - "project_module": { "add_module": "Aggiungi Modulo", "update_module": "Aggiorna Modulo", @@ -2056,7 +2057,6 @@ } } }, - "project_views": { "empty_state": { "general": { @@ -2076,7 +2076,6 @@ } } }, - "project_page": { "empty_state": { "general": { @@ -2106,7 +2105,6 @@ } } }, - "command_k": { "empty_state": { "search": { @@ -2114,7 +2112,6 @@ } } }, - "issue_relation": { "empty_state": { "search": { @@ -2125,7 +2122,6 @@ } } }, - "issue_comment": { "empty_state": { "general": { @@ -2134,7 +2130,6 @@ } } }, - "notification": { "label": "Notifiche", "page_label": "{workspace} - Notifiche", @@ -2191,7 +2186,6 @@ "custom": "Personalizzato" } }, - "active_cycle": { "empty_state": { "progress": { @@ -2211,7 +2205,6 @@ } } }, - "disabled_project": { "empty_state": { "inbox": { @@ -2274,7 +2267,6 @@ } } }, - "stickies": { "title": "I tuoi stickies", "placeholder": "clicca per scrivere qui", @@ -2332,7 +2324,6 @@ } } }, - "role_details": { "guest": { "title": "Ospite", @@ -2347,7 +2338,6 @@ "description": "Tutti i permessi impostati su true all'interno dello spazio di lavoro." } }, - "user_roles": { "product_or_project_manager": "Product / Project Manager", "development_or_engineering": "Sviluppo / Ingegneria", @@ -2360,7 +2350,6 @@ "human_resources": "Risorse umane", "other": "Altro" }, - "importer": { "github": { "title": "Github", @@ -2371,7 +2360,6 @@ "description": "Importa elementi di lavoro ed epic dai progetti e dagli epic di Jira." } }, - "exporter": { "csv": { "title": "CSV", @@ -2400,7 +2388,6 @@ "created": "Creati", "subscribed": "Iscritti" }, - "themes": { "theme_options": { "system_preference": { @@ -2446,20 +2433,17 @@ "manual": "Manuale" } }, - "cycle": { "label": "{count, plural, one {Ciclo} other {Cicli}}", "no_cycle": "Nessun ciclo" }, - "module": { "label": "{count, plural, one {Modulo} other {Moduli}}", "no_module": "Nessun modulo" }, - "description_versions": { "last_edited_by": "Ultima modifica di", "previously_edited_by": "Precedentemente modificato da", "edited_by": "Modificato da" } -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/ja/translations.json b/packages/i18n/src/locales/ja/translations.json index 24b312be914..9fd234b2eea 100644 --- a/packages/i18n/src/locales/ja/translations.json +++ b/packages/i18n/src/locales/ja/translations.json @@ -18,7 +18,6 @@ "pro": "プロ", "upgrade": "アップグレード" }, - "auth": { "common": { "email": { @@ -168,7 +167,6 @@ } } }, - "submit": "送信", "cancel": "キャンセル", "loading": "読み込み中", @@ -504,7 +502,6 @@ "new_password_must_be_different_from_old_password": "新しいパスワードは古いパスワードと異なる必要があります", "edited": "編集済み", "bot": "ボット", - "project_view": { "sort_by": { "created_at": "作成日時", @@ -512,12 +509,10 @@ "name": "名前" } }, - "toast": { "success": "成功!", "error": "エラー!" }, - "links": { "toasts": { "created": { @@ -546,7 +541,6 @@ } } }, - "home": { "empty": { "quickstart_guide": "クイックスタートガイド", @@ -614,7 +608,6 @@ "title": "ホーム", "star_us_on_github": "GitHubでスターをつける" }, - "link": { "modal": { "url": { @@ -628,7 +621,6 @@ } } }, - "common": { "all": "すべて", "states": "ステータス", @@ -875,20 +867,17 @@ "apply": "適用", "applying": "適用中" }, - "chart": { "x_axis": "エックス アクシス", "y_axis": "ワイ アクシス", "metric": "メトリック" }, - "form": { "title": { "required": "タイトルは必須です", "max_length": "タイトルは{length}文字未満である必要があります" } }, - "entity": { "grouping_title": "{entity}のグループ化", "priority": "{entity}の優先度", @@ -912,7 +901,6 @@ "failed": "{entity}の追加中にエラーが発生しました" } }, - "epic": { "all": "すべてのエピック", "label": "{count, plural, one {エピック} other {エピック}}", @@ -930,7 +918,6 @@ "required": "エピックのタイトルは必須です。" } }, - "issue": { "label": "{count, plural, one {作業項目} other {作業項目}}", "all": "すべての作業項目", @@ -1097,7 +1084,6 @@ }, "open_in_full_screen": "作業項目をフルスクリーンで開く" }, - "attachment": { "error": "ファイルを添付できませんでした。もう一度アップロードしてください。", "only_one_file_allowed": "一度にアップロードできるファイルは1つだけです。", @@ -1105,7 +1091,6 @@ "drag_and_drop": "どこにでもドラッグ&ドロップでアップロード", "delete": "添付ファイルを削除" }, - "label": { "select": "ラベルを選択", "create": { @@ -1115,7 +1100,6 @@ "type": "新しいラベルを追加するには入力してください" } }, - "sub_work_item": { "update": { "success": "サブ作業項目を更新しました", @@ -1126,7 +1110,6 @@ "error": "サブ作業項目の削除中にエラーが発生しました" } }, - "view": { "label": "{count, plural, one {ビュー} other {ビュー}}", "create": { @@ -1136,7 +1119,6 @@ "label": "ビューを更新" } }, - "inbox_issue": { "status": { "pending": { @@ -1222,7 +1204,6 @@ } } }, - "workspace_creation": { "heading": "ワークスペースを作成", "subheading": "Planeを使用するには、ワークスペースを作成するか参加する必要があります。", @@ -1274,7 +1255,6 @@ } } }, - "workspace_dashboard": { "empty_state": { "general": { @@ -1290,7 +1270,6 @@ } } }, - "workspace_analytics": { "label": "アナリティクス", "page_label": "{workspace} - アナリティクス", @@ -1333,9 +1312,39 @@ } } } - } + }, + "empty_state_v2": { + "customized_insights": { + "description": "あなたに割り当てられた作業項目は、ステータスごとに分類されてここに表示されます。", + "title": "まだデータがありません" + }, + "created_vs_resolved": { + "description": "時間の経過とともに作成および解決された作業項目がここに表示されます。", + "title": "まだデータがありません" + }, + "project_insights": { + "title": "まだデータがありません", + "description": "あなたに割り当てられた作業項目は、ステータスごとに分類されてここに表示されます。" + } + }, + "created_vs_resolved": "作成 vs 解決", + "customized_insights": "カスタマイズされたインサイト", + "backlog_work_items": "バックログの作業項目", + "active_projects": "アクティブなプロジェクト", + "trend_on_charts": "グラフの傾向", + "all_projects": "すべてのプロジェクト", + "summary_of_projects": "プロジェクトの概要", + "project_insights": "プロジェクトのインサイト", + "started_work_items": "開始された作業項目", + "total_work_items": "作業項目の合計", + "total_projects": "プロジェクト合計", + "total_admins": "管理者の合計", + "total_users": "ユーザー総数", + "total_intake": "総収入", + "un_started_work_items": "未開始の作業項目", + "total_guests": "ゲストの合計", + "completed_work_items": "完了した作業項目" }, - "workspace_projects": { "label": "{count, plural, one {プロジェクト} other {プロジェクト}}", "create": { @@ -1409,7 +1418,6 @@ } } }, - "workspace_views": { "add_view": "ビューを追加", "empty_state": { @@ -1444,7 +1452,6 @@ } } }, - "workspace_settings": { "label": "ワークスペース設定", "page_label": "{workspace} - 一般設定", @@ -1626,7 +1633,6 @@ } } }, - "profile": { "label": "プロフィール", "page_label": "あなたの作業", @@ -1689,7 +1695,6 @@ } } }, - "project_settings": { "general": { "enter_project_id": "プロジェクトIDを入力", @@ -1835,7 +1840,6 @@ "auto_close_status": "自動クローズステータス" } }, - "empty_state": { "labels": { "title": "ラベルがまだありません", @@ -1848,7 +1852,6 @@ } } }, - "project_cycles": { "add_cycle": "サイクルを追加", "more_details": "詳細情報", @@ -1974,7 +1977,6 @@ } } }, - "project_issues": { "empty_state": { "no_issues": { @@ -2003,7 +2005,6 @@ } } }, - "project_module": { "add_module": "モジュールを追加", "update_module": "モジュールを更新", @@ -2057,7 +2058,6 @@ } } }, - "project_views": { "empty_state": { "general": { @@ -2077,7 +2077,6 @@ } } }, - "project_page": { "empty_state": { "general": { @@ -2107,7 +2106,6 @@ } } }, - "command_k": { "empty_state": { "search": { @@ -2115,7 +2113,6 @@ } } }, - "issue_relation": { "empty_state": { "search": { @@ -2126,7 +2123,6 @@ } } }, - "issue_comment": { "empty_state": { "general": { @@ -2135,7 +2131,6 @@ } } }, - "notification": { "label": "受信トレイ", "page_label": "{workspace} - 受信トレイ", @@ -2192,7 +2187,6 @@ "custom": "カスタム" } }, - "active_cycle": { "empty_state": { "progress": { @@ -2212,7 +2206,6 @@ } } }, - "disabled_project": { "empty_state": { "inbox": { @@ -2275,7 +2268,6 @@ } } }, - "stickies": { "title": "あなたの付箋", "placeholder": "ここをクリックして入力", @@ -2333,7 +2325,6 @@ } } }, - "role_details": { "guest": { "title": "ゲスト", @@ -2348,7 +2339,6 @@ "description": "ワークスペース内のすべての権限が有効。" } }, - "user_roles": { "product_or_project_manager": "プロダクト/プロジェクトマネージャー", "development_or_engineering": "開発/エンジニアリング", @@ -2361,7 +2351,6 @@ "human_resources": "人事", "other": "その他" }, - "importer": { "github": { "title": "GitHub", @@ -2372,7 +2361,6 @@ "description": "Jiraプロジェクトとエピックから作業項目とエピックをインポートします。" } }, - "exporter": { "csv": { "title": "CSV", @@ -2401,7 +2389,6 @@ "created": "作成済み", "subscribed": "購読中" }, - "themes": { "theme_options": { "system_preference": { @@ -2447,20 +2434,17 @@ "manual": "手動" } }, - "cycle": { "label": "{count, plural, one {サイクル} other {サイクル}}", "no_cycle": "サイクルなし" }, - "module": { "label": "{count, plural, one {モジュール} other {モジュール}}", "no_module": "モジュールなし" }, - "description_versions": { "last_edited_by": "最終編集者", "previously_edited_by": "以前の編集者", "edited_by": "編集者" } -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/ko/translations.json b/packages/i18n/src/locales/ko/translations.json index cbfe169bed8..7b8cbad2a8a 100644 --- a/packages/i18n/src/locales/ko/translations.json +++ b/packages/i18n/src/locales/ko/translations.json @@ -18,7 +18,6 @@ "pro": "프로", "upgrade": "업그레이드" }, - "auth": { "common": { "email": { @@ -168,7 +167,6 @@ } } }, - "submit": "제출", "cancel": "취소", "loading": "로딩 중", @@ -504,7 +502,6 @@ "new_password_must_be_different_from_old_password": "새 비밀번호는 이전 비밀번호와 다르게 설정해야 합니다", "edited": "수정됨", "bot": "봇", - "project_view": { "sort_by": { "created_at": "생성일", @@ -512,12 +509,10 @@ "name": "이름" } }, - "toast": { "success": "성공!", "error": "오류!" }, - "links": { "toasts": { "created": { @@ -546,7 +541,6 @@ } } }, - "home": { "empty": { "quickstart_guide": "빠른 시작 가이드", @@ -614,7 +608,6 @@ "title": "홈", "star_us_on_github": "GitHub에서 별표" }, - "link": { "modal": { "url": { @@ -628,7 +621,6 @@ } } }, - "common": { "all": "모두", "states": "상태", @@ -876,20 +868,17 @@ "apply": "적용", "applying": "적용 중" }, - "chart": { "x_axis": "X축", "y_axis": "Y축", "metric": "메트릭" }, - "form": { "title": { "required": "제목이 필요합니다", "max_length": "제목은 {length}자 미만이어야 합니다" } }, - "entity": { "grouping_title": "{entity} 그룹화", "priority": "{entity} 우선순위", @@ -913,7 +902,6 @@ "failed": "{entity} 추가 중 오류 발생" } }, - "epic": { "all": "모든 에픽", "label": "{count, plural, one {에픽} other {에픽}}", @@ -931,7 +919,6 @@ "required": "에픽 제목이 필요합니다." } }, - "issue": { "label": "{count, plural, one {작업 항목} other {작업 항목}}", "all": "모든 작업 항목", @@ -1098,7 +1085,6 @@ }, "open_in_full_screen": "작업 항목을 전체 화면으로 열기" }, - "attachment": { "error": "파일을 첨부할 수 없습니다. 다시 업로드하세요.", "only_one_file_allowed": "한 번에 하나의 파일만 업로드할 수 있습니다.", @@ -1106,7 +1092,6 @@ "drag_and_drop": "업로드하려면 아무 곳에나 드래그 앤 드롭하세요", "delete": "첨부 파일 삭제" }, - "label": { "select": "레이블 선택", "create": { @@ -1116,7 +1101,6 @@ "type": "새 레이블을 추가하려면 입력하세요" } }, - "sub_work_item": { "update": { "success": "하위 작업 항목이 성공적으로 업데이트되었습니다", @@ -1127,7 +1111,6 @@ "error": "하위 작업 항목 제거 중 오류 발생" } }, - "view": { "label": "{count, plural, one {뷰} other {뷰}}", "create": { @@ -1137,7 +1120,6 @@ "label": "뷰 업데이트" } }, - "inbox_issue": { "status": { "pending": { @@ -1223,7 +1205,6 @@ } } }, - "workspace_creation": { "heading": "작업 공간 생성", "subheading": "Plane을 사용하려면 작업 공간을 생성하거나 참여해야 합니다.", @@ -1275,7 +1256,6 @@ } } }, - "workspace_dashboard": { "empty_state": { "general": { @@ -1291,7 +1271,6 @@ } } }, - "workspace_analytics": { "label": "분석", "page_label": "{workspace} - 분석", @@ -1334,9 +1313,39 @@ } } } - } + }, + "empty_state_v2": { + "customized_insights": { + "description": "귀하에게 할당된 작업 항목이 상태별로 나누어 여기에 표시됩니다.", + "title": "아직 데이터가 없습니다" + }, + "created_vs_resolved": { + "description": "시간이 지나면서 생성되고 해결된 작업 항목이 여기에 표시됩니다.", + "title": "아직 데이터가 없습니다" + }, + "project_insights": { + "title": "아직 데이터가 없습니다", + "description": "귀하에게 할당된 작업 항목이 상태별로 나누어 여기에 표시됩니다." + } + }, + "created_vs_resolved": "생성됨 vs 해결됨", + "customized_insights": "맞춤형 인사이트", + "backlog_work_items": "백로그 작업 항목", + "active_projects": "활성 프로젝트", + "trend_on_charts": "차트의 추세", + "all_projects": "모든 프로젝트", + "summary_of_projects": "프로젝트 요약", + "project_insights": "프로젝트 인사이트", + "started_work_items": "시작된 작업 항목", + "total_work_items": "총 작업 항목", + "total_projects": "총 프로젝트 수", + "total_admins": "총 관리자 수", + "total_users": "총 사용자 수", + "total_intake": "총 수입", + "un_started_work_items": "시작되지 않은 작업 항목", + "total_guests": "총 게스트 수", + "completed_work_items": "완료된 작업 항목" }, - "workspace_projects": { "label": "{count, plural, one {프로젝트} other {프로젝트}}", "create": { @@ -1411,7 +1420,6 @@ } } }, - "workspace_views": { "add_view": "뷰 추가", "empty_state": { @@ -1446,7 +1454,6 @@ } } }, - "workspace_settings": { "label": "작업 공간 설정", "page_label": "{workspace} - 일반 설정", @@ -1628,7 +1635,6 @@ } } }, - "profile": { "label": "프로필", "page_label": "나의 작업", @@ -1691,7 +1697,6 @@ } } }, - "project_settings": { "general": { "enter_project_id": "프로젝트 ID 입력", @@ -1837,7 +1842,6 @@ "auto_close_status": "자동 닫기 상태" } }, - "empty_state": { "labels": { "title": "레이블 없음", @@ -1850,7 +1854,6 @@ } } }, - "project_cycles": { "add_cycle": "주기 추가", "more_details": "자세히 보기", @@ -1976,7 +1979,6 @@ } } }, - "project_issues": { "empty_state": { "no_issues": { @@ -2005,7 +2007,6 @@ } } }, - "project_module": { "add_module": "모듈 추가", "update_module": "모듈 업데이트", @@ -2059,7 +2060,6 @@ } } }, - "project_views": { "empty_state": { "general": { @@ -2079,7 +2079,6 @@ } } }, - "project_page": { "empty_state": { "general": { @@ -2109,7 +2108,6 @@ } } }, - "command_k": { "empty_state": { "search": { @@ -2117,7 +2115,6 @@ } } }, - "issue_relation": { "empty_state": { "search": { @@ -2128,7 +2125,6 @@ } } }, - "issue_comment": { "empty_state": { "general": { @@ -2137,7 +2133,6 @@ } } }, - "notification": { "label": "받은 편지함", "page_label": "{workspace} - 받은 편지함", @@ -2194,7 +2189,6 @@ "custom": "사용자 정의" } }, - "active_cycle": { "empty_state": { "progress": { @@ -2214,7 +2208,6 @@ } } }, - "disabled_project": { "empty_state": { "inbox": { @@ -2277,7 +2270,6 @@ } } }, - "stickies": { "title": "나의 스티키", "placeholder": "여기에 입력하려면 클릭하세요", @@ -2335,7 +2327,6 @@ } } }, - "role_details": { "guest": { "title": "게스트", @@ -2350,7 +2341,6 @@ "description": "작업 공간 내에서 모든 권한이 true로 설정됨." } }, - "user_roles": { "product_or_project_manager": "제품 / 프로젝트 관리자", "development_or_engineering": "개발 / 엔지니어링", @@ -2363,7 +2353,6 @@ "human_resources": "인사 / 자원", "other": "기타" }, - "importer": { "github": { "title": "Github", @@ -2374,7 +2363,6 @@ "description": "Jira 프로젝트 및 에픽에서 작업 항목과 에픽을 가져옵니다." } }, - "exporter": { "csv": { "title": "CSV", @@ -2403,7 +2391,6 @@ "created": "생성됨", "subscribed": "구독됨" }, - "themes": { "theme_options": { "system_preference": { @@ -2449,20 +2436,17 @@ "manual": "수동" } }, - "cycle": { "label": "{count, plural, one {주기} other {주기}}", "no_cycle": "주기 없음" }, - "module": { "label": "{count, plural, one {모듈} other {모듈}}", "no_module": "모듈 없음" }, - "description_versions": { "last_edited_by": "마지막 편집자", "previously_edited_by": "이전 편집자", "edited_by": "편집자" } -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/pl/translations.json b/packages/i18n/src/locales/pl/translations.json index a47732020a1..d96bae7d536 100644 --- a/packages/i18n/src/locales/pl/translations.json +++ b/packages/i18n/src/locales/pl/translations.json @@ -500,10 +500,8 @@ "export": "Eksportuj", "member": "{count, plural, one{# członek} few{# członkowie} other{# członków}}", "new_password_must_be_different_from_old_password": "Nowe hasło musi być innym niż stare hasło", - "edited": "Edytowano", "bot": "Bot", - "project_view": { "sort_by": { "created_at": "Utworzono dnia", @@ -1315,7 +1313,38 @@ } } } - } + }, + "empty_state_v2": { + "customized_insights": { + "description": "Przypisane do Ciebie elementy pracy, podzielone według stanu, pojawią się tutaj.", + "title": "Brak danych" + }, + "created_vs_resolved": { + "description": "Elementy pracy utworzone i rozwiązane w czasie pojawią się tutaj.", + "title": "Brak danych" + }, + "project_insights": { + "title": "Brak danych", + "description": "Przypisane do Ciebie elementy pracy, podzielone według stanu, pojawią się tutaj." + } + }, + "created_vs_resolved": "Utworzone vs Rozwiązane", + "customized_insights": "Dostosowane informacje", + "backlog_work_items": "Elementy pracy w backlogu", + "active_projects": "Aktywne projekty", + "trend_on_charts": "Trend na wykresach", + "all_projects": "Wszystkie projekty", + "summary_of_projects": "Podsumowanie projektów", + "project_insights": "Wgląd w projekt", + "started_work_items": "Rozpoczęte elementy pracy", + "total_work_items": "Łączna liczba elementów pracy", + "total_projects": "Łączna liczba projektów", + "total_admins": "Łączna liczba administratorów", + "total_users": "Łączna liczba użytkowników", + "total_intake": "Całkowity dochód", + "un_started_work_items": "Nierozpoczęte elementy pracy", + "total_guests": "Łączna liczba gości", + "completed_work_items": "Ukończone elementy pracy" }, "workspace_projects": { "label": "{count, plural, one {Projekt} few {Projekty} other {Projektów}}", @@ -2406,20 +2435,17 @@ "manual": "Ręcznie" } }, - "cycle": { "label": "{count, plural, one {Cykl} few {Cykle} other {Cyklów}}", "no_cycle": "Brak cyklu" }, - "module": { "label": "{count, plural, one {Moduł} few {Moduły} other {Modułów}}", "no_module": "Brak modułu" }, - "description_versions": { "last_edited_by": "Ostatnio edytowane przez", "previously_edited_by": "Wcześniej edytowane przez", "edited_by": "Edytowane przez" } -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/pt-BR/translations.json b/packages/i18n/src/locales/pt-BR/translations.json index a5d7fe50e7c..a116133448a 100644 --- a/packages/i18n/src/locales/pt-BR/translations.json +++ b/packages/i18n/src/locales/pt-BR/translations.json @@ -18,7 +18,6 @@ "pro": "Pro", "upgrade": "Upgrade" }, - "auth": { "common": { "email": { @@ -168,7 +167,6 @@ } } }, - "submit": "Enviar", "cancel": "Cancelar", "loading": "Carregando", @@ -504,7 +502,6 @@ "new_password_must_be_different_from_old_password": "Nova senha deve ser diferente da senha antiga", "edited": "editado", "bot": "robô", - "project_view": { "sort_by": { "created_at": "Criado em", @@ -512,12 +509,10 @@ "name": "Nome" } }, - "toast": { "success": "Sucesso!", "error": "Erro!" }, - "links": { "toasts": { "created": { @@ -546,7 +541,6 @@ } } }, - "home": { "empty": { "quickstart_guide": "Seu guia de início rápido", @@ -614,7 +608,6 @@ "title": "Página inicial", "star_us_on_github": "Nos dê uma estrela no GitHub" }, - "link": { "modal": { "url": { @@ -628,7 +621,6 @@ } } }, - "common": { "all": "Todos", "states": "Estados", @@ -876,20 +868,17 @@ "apply": "Aplicar", "applying": "Aplicando" }, - "chart": { "x_axis": "Eixo X", "y_axis": "Eixo Y", "metric": "Métrica" }, - "form": { "title": { "required": "Título é obrigatório", "max_length": "O título deve ter menos de {length} caracteres" } }, - "entity": { "grouping_title": "Agrupamento de {entity}", "priority": "Prioridade de {entity}", @@ -913,7 +902,6 @@ "failed": "Erro ao adicionar {entity}" } }, - "epic": { "all": "Todos os Épicos", "label": "{count, plural, one {Épico} other {Épicos}}", @@ -931,7 +919,6 @@ "required": "O título do épico é obrigatório." } }, - "issue": { "label": "{count, plural, one {Item de trabalho} other {Itens de trabalho}}", "all": "Todos os Itens de trabalho", @@ -1098,7 +1085,6 @@ }, "open_in_full_screen": "Abrir item de trabalho em tela cheia" }, - "attachment": { "error": "Não foi possível anexar o arquivo. Tente enviar novamente.", "only_one_file_allowed": "Apenas um arquivo pode ser enviado por vez.", @@ -1106,7 +1092,6 @@ "drag_and_drop": "Arraste e solte em qualquer lugar para enviar", "delete": "Excluir anexo" }, - "label": { "select": "Selecionar etiqueta", "create": { @@ -1116,7 +1101,6 @@ "type": "Digite para adicionar uma nova etiqueta" } }, - "sub_work_item": { "update": { "success": "Sub-item de trabalho atualizado com sucesso", @@ -1127,7 +1111,6 @@ "error": "Erro ao remover sub-item de trabalho" } }, - "view": { "label": "{count, plural, one {Visualização} other {Visualizações}}", "create": { @@ -1137,7 +1120,6 @@ "label": "Atualizar Visualização" } }, - "inbox_issue": { "status": { "pending": { @@ -1223,7 +1205,6 @@ } } }, - "workspace_creation": { "heading": "Crie seu espaço de trabalho", "subheading": "Para começar a usar o Plane, você precisa criar ou entrar em um espaço de trabalho.", @@ -1275,7 +1256,6 @@ } } }, - "workspace_dashboard": { "empty_state": { "general": { @@ -1291,7 +1271,6 @@ } } }, - "workspace_analytics": { "label": "Análises", "page_label": "{workspace} - Análises", @@ -1334,9 +1313,39 @@ } } } - } + }, + "empty_state_v2": { + "customized_insights": { + "description": "Os itens de trabalho atribuídos a você, divididos por estado, aparecerão aqui.", + "title": "Ainda não há dados" + }, + "created_vs_resolved": { + "description": "Os itens de trabalho criados e resolvidos ao longo do tempo aparecerão aqui.", + "title": "Ainda não há dados" + }, + "project_insights": { + "title": "Ainda não há dados", + "description": "Os itens de trabalho atribuídos a você, divididos por estado, aparecerão aqui." + } + }, + "created_vs_resolved": "Criado vs Resolvido", + "customized_insights": "Insights personalizados", + "backlog_work_items": "Itens de trabalho no backlog", + "active_projects": "Projetos ativos", + "trend_on_charts": "Tendência nos gráficos", + "all_projects": "Todos os projetos", + "summary_of_projects": "Resumo dos projetos", + "project_insights": "Insights do projeto", + "started_work_items": "Itens de trabalho iniciados", + "total_work_items": "Total de itens de trabalho", + "total_projects": "Total de projetos", + "total_admins": "Total de administradores", + "total_users": "Total de usuários", + "total_intake": "Receita total", + "un_started_work_items": "Itens de trabalho não iniciados", + "total_guests": "Total de convidados", + "completed_work_items": "Itens de trabalho concluídos" }, - "workspace_projects": { "label": "{count, plural, one {Projeto} other {Projetos}}", "create": { @@ -1411,7 +1420,6 @@ } } }, - "workspace_views": { "add_view": "Adicionar visualização", "empty_state": { @@ -1446,7 +1454,6 @@ } } }, - "workspace_settings": { "label": "Configurações do espaço de trabalho", "page_label": "{workspace} - Configurações gerais", @@ -1628,7 +1635,6 @@ } } }, - "profile": { "label": "Perfil", "page_label": "Seu trabalho", @@ -1691,7 +1697,6 @@ } } }, - "project_settings": { "general": { "enter_project_id": "Inserir ID do projeto", @@ -1837,7 +1842,6 @@ "auto_close_status": "Status de fechamento automático" } }, - "empty_state": { "labels": { "title": "Nenhuma etiqueta ainda", @@ -1850,7 +1854,6 @@ } } }, - "project_cycles": { "add_cycle": "Adicionar ciclo", "more_details": "Mais detalhes", @@ -1970,7 +1973,6 @@ } } }, - "project_issues": { "empty_state": { "no_issues": { @@ -1999,7 +2001,6 @@ } } }, - "project_module": { "add_module": "Adicionar Módulo", "update_module": "Atualizar Módulo", @@ -2053,7 +2054,6 @@ } } }, - "project_views": { "empty_state": { "general": { @@ -2073,7 +2073,6 @@ } } }, - "project_page": { "empty_state": { "general": { @@ -2103,7 +2102,6 @@ } } }, - "command_k": { "empty_state": { "search": { @@ -2111,7 +2109,6 @@ } } }, - "issue_relation": { "empty_state": { "search": { @@ -2122,7 +2119,6 @@ } } }, - "issue_comment": { "empty_state": { "general": { @@ -2131,7 +2127,6 @@ } } }, - "notification": { "label": "Caixa de entrada", "page_label": "{workspace} - Caixa de entrada", @@ -2188,7 +2183,6 @@ "custom": "Personalizado" } }, - "active_cycle": { "empty_state": { "progress": { @@ -2270,7 +2264,6 @@ } } }, - "stickies": { "title": "Suas anotações", "placeholder": "clique para digitar aqui", @@ -2328,7 +2321,6 @@ } } }, - "role_details": { "guest": { "title": "Convidado", @@ -2343,7 +2335,6 @@ "description": "Todas as permissões definidas como verdadeiras dentro do espaço de trabalho." } }, - "user_roles": { "product_or_project_manager": "Gerente de Produto / Projeto", "development_or_engineering": "Desenvolvimento / Engenharia", @@ -2356,7 +2347,6 @@ "human_resources": "Recursos Humanos", "other": "Outro" }, - "importer": { "github": { "title": "Github", @@ -2367,7 +2357,6 @@ "description": "Importe itens de trabalho e épicos de projetos e épicos do Jira." } }, - "exporter": { "csv": { "title": "CSV", @@ -2396,7 +2385,6 @@ "created": "Criado", "subscribed": "Inscrito" }, - "themes": { "theme_options": { "system_preference": { @@ -2442,20 +2430,17 @@ "manual": "Manual" } }, - "cycle": { "label": "{count, plural, one {Ciclo} other {Ciclos}}", "no_cycle": "Nenhum ciclo" }, - "module": { "label": "{count, plural, one {Módulo} other {Módulos}}", "no_module": "Nenhum módulo" }, - "description_versions": { "last_edited_by": "Última edição por", "previously_edited_by": "Anteriormente editado por", "edited_by": "Editado por" } -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/ro/translations.json b/packages/i18n/src/locales/ro/translations.json index fd91024105f..67d81e3a581 100644 --- a/packages/i18n/src/locales/ro/translations.json +++ b/packages/i18n/src/locales/ro/translations.json @@ -18,7 +18,6 @@ "pro": "Pro", "upgrade": "Treci la versiunea superioară" }, - "auth": { "common": { "email": { @@ -168,7 +167,6 @@ } } }, - "submit": "Trimite", "cancel": "Anulează", "loading": "Se încarcă", @@ -502,7 +500,6 @@ "export": "Exportă", "member": "{count, plural, one{# membru} other{# membri}}", "new_password_must_be_different_from_old_password": "Parola nouă trebuie să fie diferită de parola veche", - "project_view": { "sort_by": { "created_at": "Creat la", @@ -510,12 +507,10 @@ "name": "Nume" } }, - "toast": { "success": "Succes!", "error": "Eroare!" }, - "links": { "toasts": { "created": { @@ -544,7 +539,6 @@ } } }, - "home": { "empty": { "quickstart_guide": "Ghid de pornire rapidă", @@ -612,7 +606,6 @@ "title": "Acasă", "star_us_on_github": "Dă-ne o stea pe GitHub" }, - "link": { "modal": { "url": { @@ -626,7 +619,6 @@ } } }, - "common": { "all": "Toate", "states": "Stări", @@ -874,20 +866,17 @@ "apply": "Aplică", "applying": "Aplicând" }, - "chart": { "x_axis": "axa-X", "y_axis": "axa-Y", "metric": "Indicator" }, - "form": { "title": { "required": "Titlul este obligatoriu", "max_length": "Titlul trebuie să conțină mai puțin de {length} caractere" } }, - "entity": { "grouping_title": "Grupare {entity}", "priority": "Prioritate {entity}", @@ -911,7 +900,6 @@ "failed": "Eroare la adăugarea {entity}" } }, - "epic": { "all": "Toate Sarcinile majore", "label": "{count, plural, one {Sarcină majoră} other {Sarcini majore}}", @@ -929,7 +917,6 @@ "required": "Titlul sarcinii majore este obligatoriu." } }, - "issue": { "label": "{count, plural, one {Activitate} other {Activități}}", "all": "Toate activitățile", @@ -1096,7 +1083,6 @@ }, "open_in_full_screen": "Deschide activitatea pe tot ecranul" }, - "attachment": { "error": "Fișierul nu a putut fi atașat. Încearcă să încarci din nou.", "only_one_file_allowed": "Se poate încărca doar un fișier o dată.", @@ -1104,7 +1090,6 @@ "drag_and_drop": "Trage și plasează oriunde pentru a încărca", "delete": "Șterge atașamentul" }, - "label": { "select": "Selectează eticheta", "create": { @@ -1114,7 +1099,6 @@ "type": "Tastează pentru a adăuga o etichetă nouă" } }, - "sub_work_item": { "update": { "success": "Sub-activitatea a fost actualizată cu succes", @@ -1125,7 +1109,6 @@ "error": "Eroare la eliminarea sub-activității" } }, - "view": { "label": "{count, plural, one {Perspectivă} other {Perspective}}", "create": { @@ -1135,7 +1118,6 @@ "label": "Actualizează perspectiva" } }, - "inbox_issue": { "status": { "pending": { @@ -1221,7 +1203,6 @@ } } }, - "workspace_creation": { "heading": "Creează spațiul tău de lucru", "subheading": "Pentru a începe să folosești Plane, trebuie să creezi sau să te alături unui spațiu de lucru.", @@ -1273,7 +1254,6 @@ } } }, - "workspace_dashboard": { "empty_state": { "general": { @@ -1289,7 +1269,6 @@ } } }, - "workspace_analytics": { "label": "Statistici", "page_label": "{workspace} - Statistici", @@ -1332,9 +1311,39 @@ } } } - } + }, + "empty_state_v2": { + "customized_insights": { + "description": "Elementele de lucru atribuite ție, împărțite pe stări, vor apărea aici.", + "title": "Nu există date încă" + }, + "created_vs_resolved": { + "description": "Elementele de lucru create și rezolvate în timp vor apărea aici.", + "title": "Nu există date încă" + }, + "project_insights": { + "title": "Nu există date încă", + "description": "Elementele de lucru atribuite ție, împărțite pe stări, vor apărea aici." + } + }, + "created_vs_resolved": "Creat vs Rezolvat", + "customized_insights": "Perspective personalizate", + "backlog_work_items": "Elemente de lucru din backlog", + "active_projects": "Proiecte active", + "trend_on_charts": "Tendință în grafice", + "all_projects": "Toate proiectele", + "summary_of_projects": "Sumarul proiectelor", + "project_insights": "Informații despre proiect", + "started_work_items": "Elemente de lucru începute", + "total_work_items": "Totalul elementelor de lucru", + "total_projects": "Total proiecte", + "total_admins": "Total administratori", + "total_users": "Total utilizatori", + "total_intake": "Venit total", + "un_started_work_items": "Elemente de lucru neîncepute", + "total_guests": "Total invitați", + "completed_work_items": "Elemente de lucru finalizate" }, - "workspace_projects": { "label": "{count, plural, one {Proiect} other {Proiecte}}", "create": { @@ -1409,7 +1418,6 @@ } } }, - "workspace_views": { "add_view": "Adaugă perspectivă", "empty_state": { @@ -1444,7 +1452,6 @@ } } }, - "workspace_settings": { "label": "Setări spațiu de lucru", "page_label": "{workspace} - Setări generale", @@ -1626,7 +1633,6 @@ } } }, - "profile": { "label": "Profil", "page_label": "Munca ta", @@ -1689,7 +1695,6 @@ } } }, - "project_settings": { "general": { "enter_project_id": "Introdu ID-ul proiectului", @@ -1835,7 +1840,6 @@ "auto_close_status": "Stare închidere automată" } }, - "empty_state": { "labels": { "title": "Nicio etichetă încă", @@ -1848,7 +1852,6 @@ } } }, - "project_cycles": { "add_cycle": "Adaugă ciclu", "more_details": "Mai multe detalii", @@ -1968,7 +1971,6 @@ } } }, - "project_issues": { "empty_state": { "no_issues": { @@ -1997,7 +1999,6 @@ } } }, - "project_module": { "add_module": "Adaugă Modul", "update_module": "Actualizează Modul", @@ -2051,7 +2052,6 @@ } } }, - "project_views": { "empty_state": { "general": { @@ -2071,7 +2071,6 @@ } } }, - "project_page": { "empty_state": { "general": { @@ -2101,7 +2100,6 @@ } } }, - "command_k": { "empty_state": { "search": { @@ -2109,7 +2107,6 @@ } } }, - "issue_relation": { "empty_state": { "search": { @@ -2120,7 +2117,6 @@ } } }, - "issue_comment": { "empty_state": { "general": { @@ -2129,7 +2125,6 @@ } } }, - "notification": { "label": "Căsuță de mesaje", "page_label": "{workspace} - Căsuță de mesaje", @@ -2186,7 +2181,6 @@ "custom": "Personalizat" } }, - "active_cycle": { "empty_state": { "progress": { @@ -2206,7 +2200,6 @@ } } }, - "disabled_project": { "empty_state": { "inbox": { @@ -2269,7 +2262,6 @@ } } }, - "stickies": { "title": "Notițele tale", "placeholder": "click pentru a scrie aici", @@ -2327,7 +2319,6 @@ } } }, - "role_details": { "guest": { "title": "Invitat", @@ -2342,7 +2333,6 @@ "description": "Toate permisiunile setate pe adevărat în cadrul workspace-ului." } }, - "user_roles": { "product_or_project_manager": "Manager de produs / proiect", "development_or_engineering": "Dezvoltare / Inginerie", @@ -2355,7 +2345,6 @@ "human_resources": "Resurse umane", "other": "Altceva" }, - "importer": { "github": { "title": "Github", @@ -2366,7 +2355,6 @@ "description": "Importă activități și episoade din proiectele și episoadele Jira." } }, - "exporter": { "csv": { "title": "CSV", @@ -2395,7 +2383,6 @@ "created": "Create", "subscribed": "Urmărite" }, - "themes": { "theme_options": { "system_preference": { @@ -2441,20 +2428,17 @@ "manual": "Manual" } }, - "cycle": { "label": "{count, plural, one {Ciclu} other {Cicluri}}", "no_cycle": "Niciun ciclu" }, - "module": { "label": "{count, plural, one {Modul} other {Module}}", "no_module": "Niciun modul" }, - "description_versions": { "last_edited_by": "Ultima editare de către", "previously_edited_by": "Editat anterior de către", "edited_by": "Editat de" } -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/ru/translations.json b/packages/i18n/src/locales/ru/translations.json index 7c52c5e910f..07feddd24f2 100644 --- a/packages/i18n/src/locales/ru/translations.json +++ b/packages/i18n/src/locales/ru/translations.json @@ -502,7 +502,6 @@ "new_password_must_be_different_from_old_password": "Новое пароль должен отличаться от старого пароля", "edited": "Редактировано", "bot": "Бот", - "project_view": { "sort_by": { "created_at": "Дата создания", @@ -510,12 +509,10 @@ "name": "Имя" } }, - "toast": { "success": "Успех!", "error": "Ошибка!" }, - "links": { "toasts": { "created": { @@ -544,7 +541,6 @@ } } }, - "home": { "empty": { "quickstart_guide": "Руководство по началу работы", @@ -612,7 +608,6 @@ "title": "Главная", "star_us_on_github": "Оцените нас на GitHub" }, - "link": { "modal": { "url": { @@ -626,7 +621,6 @@ } } }, - "common": { "all": "Все", "states": "Статусы", @@ -874,20 +868,17 @@ "apply": "Применить", "applying": "Применение" }, - "chart": { "x_axis": "Ось X", "y_axis": "Ось Y", "metric": "Метрика" }, - "form": { "title": { "required": "Название обязательно", "max_length": "Название должно быть короче {length} символов" } }, - "entity": { "grouping_title": "Группировка {entity}", "priority": "Приоритет {entity}", @@ -911,7 +902,6 @@ "failed": "Ошибка добавления {entity}" } }, - "epic": { "all": "Все эпики", "label": "{count, plural, one {Эпик} other {Эпики}}", @@ -929,7 +919,6 @@ "required": "Название эпика обязательно" } }, - "issue": { "label": "{count, plural, one {Рабочий элемент} other {Рабочие элементы}}", "all": "Все рабочие элементы", @@ -1096,7 +1085,6 @@ }, "open_in_full_screen": "Открыть рабочий элемент в полном экране" }, - "attachment": { "error": "Ошибка прикрепления файла", "only_one_file_allowed": "Можно загрузить только один файл", @@ -1104,7 +1092,6 @@ "drag_and_drop": "Перетащите файл для загрузки", "delete": "Удалить вложение" }, - "label": { "select": "Выбрать метку", "create": { @@ -1114,7 +1101,6 @@ "type": "Введите новую метку" } }, - "sub_work_item": { "update": { "success": "Подэлемент успешно обновлен", @@ -1125,7 +1111,6 @@ "error": "Ошибка удаления подэлемента" } }, - "view": { "label": "{count, plural, one {Представление} other {Представления}}", "create": { @@ -1135,7 +1120,6 @@ "label": "Обновить представление" } }, - "inbox_issue": { "status": { "pending": { @@ -1221,7 +1205,6 @@ } } }, - "workspace_creation": { "heading": "Создайте рабочее пространство", "subheading": "Чтобы начать использовать Plane, создайте или присоединитесь к рабочему пространству.", @@ -1273,7 +1256,6 @@ } } }, - "workspace_dashboard": { "empty_state": { "general": { @@ -1289,7 +1271,6 @@ } } }, - "workspace_analytics": { "label": "Аналитика", "page_label": "{workspace} - Аналитика", @@ -1332,9 +1313,39 @@ } } } - } + }, + "empty_state_v2": { + "customized_insights": { + "description": "Назначенные вам рабочие элементы, разбитые по статусам, появятся здесь.", + "title": "Данных пока нет" + }, + "created_vs_resolved": { + "description": "Созданные и решённые со временем рабочие элементы появятся здесь.", + "title": "Данных пока нет" + }, + "project_insights": { + "title": "Данных пока нет", + "description": "Назначенные вам рабочие элементы, разбитые по статусам, появятся здесь." + } + }, + "created_vs_resolved": "Создано vs Решено", + "customized_insights": "Индивидуальные аналитические данные", + "backlog_work_items": "Элементы работы в бэклоге", + "active_projects": "Активные проекты", + "trend_on_charts": "Тренд на графиках", + "all_projects": "Все проекты", + "summary_of_projects": "Сводка по проектам", + "project_insights": "Аналитика проекта", + "started_work_items": "Начатые рабочие элементы", + "total_work_items": "Общее количество рабочих элементов", + "total_projects": "Всего проектов", + "total_admins": "Всего администраторов", + "total_users": "Всего пользователей", + "total_intake": "Общий доход", + "un_started_work_items": "Не начатые рабочие элементы", + "total_guests": "Всего гостей", + "completed_work_items": "Завершённые рабочие элементы" }, - "workspace_projects": { "label": "{count, plural, one {Проект} other {Проекты}}", "create": { @@ -1409,7 +1420,6 @@ } } }, - "workspace_views": { "add_view": "Добавить представление", "empty_state": { @@ -1444,7 +1454,6 @@ } } }, - "workspace_settings": { "label": "Настройки пространства", "page_label": "{workspace} - Основные настройки", @@ -1626,7 +1635,6 @@ } } }, - "profile": { "label": "Профиль", "page_label": "Ваша работа", @@ -1689,7 +1697,6 @@ } } }, - "project_settings": { "general": { "enter_project_id": "Введите ID проекта", @@ -1835,7 +1842,6 @@ "auto_close_status": "Статус автоматического закрытия" } }, - "empty_state": { "labels": { "title": "Нет меток", @@ -1848,7 +1854,6 @@ } } }, - "project_cycles": { "add_cycle": "Добавить цикл", "more_details": "Подробнее", @@ -1974,7 +1979,6 @@ } } }, - "project_issues": { "empty_state": { "no_issues": { @@ -2003,7 +2007,6 @@ } } }, - "project_module": { "add_module": "Добавить модуль", "update_module": "Обновить модуль", @@ -2057,7 +2060,6 @@ } } }, - "project_views": { "empty_state": { "general": { @@ -2077,7 +2079,6 @@ } } }, - "project_page": { "empty_state": { "general": { @@ -2107,7 +2108,6 @@ } } }, - "command_k": { "empty_state": { "search": { @@ -2115,7 +2115,6 @@ } } }, - "issue_relation": { "empty_state": { "search": { @@ -2126,7 +2125,6 @@ } } }, - "issue_comment": { "empty_state": { "general": { @@ -2135,7 +2133,6 @@ } } }, - "notification": { "label": "Входящие", "page_label": "{workspace} - Входящие", @@ -2192,7 +2189,6 @@ "custom": "Другое" } }, - "active_cycle": { "empty_state": { "progress": { @@ -2212,7 +2208,6 @@ } } }, - "disabled_project": { "empty_state": { "inbox": { @@ -2275,7 +2270,6 @@ } } }, - "stickies": { "title": "Ваши стикеры", "placeholder": "нажмите, чтобы написать", @@ -2333,7 +2327,6 @@ } } }, - "role_details": { "guest": { "title": "Гость", @@ -2348,7 +2341,6 @@ "description": "Полные права доступа в рамках рабочего пространства." } }, - "user_roles": { "product_or_project_manager": "Продукт / Проект менеджер", "development_or_engineering": "Разработка / Инжиниринг", @@ -2361,7 +2353,6 @@ "human_resources": "HR / Кадры", "other": "Другое" }, - "importer": { "github": { "title": "GitHub", @@ -2372,7 +2363,6 @@ "description": "Импорт рабочих элементов и эпиков из проектов Jira." } }, - "exporter": { "csv": { "title": "CSV", @@ -2401,7 +2391,6 @@ "created": "Созданные", "subscribed": "Подписанные" }, - "themes": { "theme_options": { "system_preference": { @@ -2447,20 +2436,17 @@ "manual": "Вручную" } }, - "cycle": { "label": "{count, plural, one {Цикл} other {Циклы}}", "no_cycle": "Нет цикла" }, - "module": { "label": "{count, plural, one {Модуль} other {Модули}}", "no_module": "Нет модуля" }, - "description_versions": { "last_edited_by": "Последнее редактирование", "previously_edited_by": "Ранее отредактировано", "edited_by": "Отредактировано" } -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/sk/translations.json b/packages/i18n/src/locales/sk/translations.json index 808753b830c..b6c68df2a5e 100644 --- a/packages/i18n/src/locales/sk/translations.json +++ b/packages/i18n/src/locales/sk/translations.json @@ -502,7 +502,6 @@ "new_password_must_be_different_from_old_password": "Nové heslo musí byť odlišné od starého hesla", "edited": "Upravené", "bot": "Bot", - "project_view": { "sort_by": { "created_at": "Vytvorené dňa", @@ -510,12 +509,10 @@ "name": "Názov" } }, - "toast": { "success": "Úspech!", "error": "Chyba!" }, - "links": { "toasts": { "created": { @@ -544,7 +541,6 @@ } } }, - "home": { "empty": { "quickstart_guide": "Váš sprievodca rýchlym štartom", @@ -612,7 +608,6 @@ "title": "Domov", "star_us_on_github": "Ohodnoťte nás na GitHube" }, - "link": { "modal": { "url": { @@ -626,7 +621,6 @@ } } }, - "common": { "all": "Všetko", "states": "Stavy", @@ -874,20 +868,17 @@ "apply": "Použiť", "applying": "Používanie" }, - "chart": { "x_axis": "Os X", "y_axis": "Os Y", "metric": "Metrika" }, - "form": { "title": { "required": "Názov je povinný", "max_length": "Názov by mal byť kratší ako {length} znakov" } }, - "entity": { "grouping_title": "Zoskupenie {entity}", "priority": "Priorita {entity}", @@ -911,7 +902,6 @@ "failed": "Chyba pri pridávaní {entity}" } }, - "epic": { "all": "Všetky epiky", "label": "{count, plural, one {Epika} few {Epiky} other {Epík}}", @@ -929,7 +919,6 @@ "required": "Názov epiky je povinný." } }, - "issue": { "label": "{count, plural, one {Pracovná položka} few {Pracovné položky} other {Pracovných položiek}}", "all": "Všetky pracovné položky", @@ -1096,7 +1085,6 @@ }, "open_in_full_screen": "Otvoriť pracovnú položku na celú obrazovku" }, - "attachment": { "error": "Súbor sa nedá pripojiť. Skúste to prosím znova.", "only_one_file_allowed": "Je možné nahrať iba jeden súbor naraz.", @@ -1104,7 +1092,6 @@ "drag_and_drop": "Pretiahnite súbor kamkoľvek pre nahratie", "delete": "Zmazať prílohu" }, - "label": { "select": "Vybrať štítok", "create": { @@ -1114,7 +1101,6 @@ "type": "Zadajte pre vytvorenie nového štítka" } }, - "sub_work_item": { "update": { "success": "Podriadená pracovná položka bola úspešne aktualizovaná", @@ -1125,7 +1111,6 @@ "error": "Chyba pri odstraňovaní podriadenej položky" } }, - "view": { "label": "{count, plural, one {Pohľad} few {Pohľady} other {Pohľadov}}", "create": { @@ -1135,7 +1120,6 @@ "label": "Aktualizovať pohľad" } }, - "inbox_issue": { "status": { "pending": { @@ -1221,7 +1205,6 @@ } } }, - "workspace_creation": { "heading": "Vytvorte si pracovný priestor", "subheading": "Na používanie Plane musíte vytvoriť alebo sa pripojiť k pracovnému priestoru.", @@ -1273,7 +1256,6 @@ } } }, - "workspace_dashboard": { "empty_state": { "general": { @@ -1289,7 +1271,6 @@ } } }, - "workspace_analytics": { "label": "Analytika", "page_label": "{workspace} - Analytika", @@ -1332,9 +1313,39 @@ } } } - } + }, + "empty_state_v2": { + "customized_insights": { + "description": "Pracovné položky priradené vám, rozdelené podľa stavu, sa zobrazia tu.", + "title": "Zatiaľ žiadne údaje" + }, + "created_vs_resolved": { + "description": "Pracovné položky vytvorené a vyriešené v priebehu času sa zobrazia tu.", + "title": "Zatiaľ žiadne údaje" + }, + "project_insights": { + "title": "Zatiaľ žiadne údaje", + "description": "Pracovné položky priradené vám, rozdelené podľa stavu, sa zobrazia tu." + } + }, + "created_vs_resolved": "Vytvorené vs Vyriešené", + "customized_insights": "Prispôsobené prehľady", + "backlog_work_items": "Pracovné položky v backlogu", + "active_projects": "Aktívne projekty", + "trend_on_charts": "Trend na grafoch", + "all_projects": "Všetky projekty", + "summary_of_projects": "Súhrn projektov", + "project_insights": "Prehľad projektu", + "started_work_items": "Spustené pracovné položky", + "total_work_items": "Celkový počet pracovných položiek", + "total_projects": "Celkový počet projektov", + "total_admins": "Celkový počet administrátorov", + "total_users": "Celkový počet používateľov", + "total_intake": "Celkový príjem", + "un_started_work_items": "Nespustené pracovné položky", + "total_guests": "Celkový počet hostí", + "completed_work_items": "Dokončené pracovné položky" }, - "workspace_projects": { "label": "{count, plural, one {Projekt} few {Projekty} other {Projektov}}", "create": { @@ -1409,7 +1420,6 @@ } } }, - "workspace_views": { "add_view": "Pridať pohľad", "empty_state": { @@ -1444,7 +1454,6 @@ } } }, - "workspace_settings": { "label": "Nastavenia pracovného priestoru", "page_label": "{workspace} - Všeobecné nastavenia", @@ -1625,7 +1634,6 @@ } } }, - "profile": { "label": "Profil", "page_label": "Vaša práca", @@ -1688,7 +1696,6 @@ } } }, - "project_settings": { "general": { "enter_project_id": "Zadajte ID projektu", @@ -1834,7 +1841,6 @@ "auto_close_status": "Stav pre automatické uzatvorenie" } }, - "empty_state": { "labels": { "title": "Žiadne štítky", @@ -1847,7 +1853,6 @@ } } }, - "project_cycles": { "add_cycle": "Pridať cyklus", "more_details": "Viac detailov", @@ -1973,7 +1978,6 @@ } } }, - "project_issues": { "empty_state": { "no_issues": { @@ -2002,7 +2006,6 @@ } } }, - "project_module": { "add_module": "Pridať modul", "update_module": "Aktualizovať modul", @@ -2056,7 +2059,6 @@ } } }, - "project_views": { "empty_state": { "general": { @@ -2076,7 +2078,6 @@ } } }, - "project_page": { "empty_state": { "general": { @@ -2106,7 +2107,6 @@ } } }, - "command_k": { "empty_state": { "search": { @@ -2114,7 +2114,6 @@ } } }, - "issue_relation": { "empty_state": { "search": { @@ -2125,7 +2124,6 @@ } } }, - "issue_comment": { "empty_state": { "general": { @@ -2134,7 +2132,6 @@ } } }, - "notification": { "label": "Schránka", "page_label": "{workspace} - Schránka", @@ -2191,7 +2188,6 @@ "custom": "Vlastné" } }, - "active_cycle": { "empty_state": { "progress": { @@ -2211,7 +2207,6 @@ } } }, - "disabled_project": { "empty_state": { "inbox": { @@ -2274,7 +2269,6 @@ } } }, - "stickies": { "title": "Vaše poznámky", "placeholder": "kliknutím začnite písať", @@ -2332,7 +2326,6 @@ } } }, - "role_details": { "guest": { "title": "Hosť", @@ -2347,7 +2340,6 @@ "description": "Má všetky oprávnenia v priestore." } }, - "user_roles": { "product_or_project_manager": "Produktový/Projektový manažér", "development_or_engineering": "Vývoj/Inžinierstvo", @@ -2360,7 +2352,6 @@ "human_resources": "Ľudské zdroje", "other": "Iné" }, - "importer": { "github": { "title": "GitHub", @@ -2371,7 +2362,6 @@ "description": "Importujte položky a epiky z Jira." } }, - "exporter": { "csv": { "title": "CSV", @@ -2400,7 +2390,6 @@ "created": "Vytvorené", "subscribed": "Odobierané" }, - "themes": { "theme_options": { "system_preference": { @@ -2446,20 +2435,17 @@ "manual": "Manuálne" } }, - "cycle": { "label": "{count, plural, one {Cyklus} few {Cykly} other {Cyklov}}", "no_cycle": "Žiadny cyklus" }, - "module": { "label": "{count, plural, one {Modul} few {Moduly} other {Modulov}}", "no_module": "Žiadny modul" }, - "description_versions": { "last_edited_by": "Naposledy upravené používateľom", "previously_edited_by": "Predtým upravené používateľom", "edited_by": "Upravené používateľom" } -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/tr-TR/translations.json b/packages/i18n/src/locales/tr-TR/translations.json index 098bd1f0462..08d16a07656 100644 --- a/packages/i18n/src/locales/tr-TR/translations.json +++ b/packages/i18n/src/locales/tr-TR/translations.json @@ -18,7 +18,6 @@ "pro": "Pro", "upgrade": "Apgreyd" }, - "auth": { "common": { "email": { @@ -168,7 +167,6 @@ } } }, - "submit": "Gönder", "cancel": "İptal", "loading": "Yükleniyor", @@ -504,7 +502,6 @@ "new_password_must_be_different_from_old_password": "Yeni şifre eski şifreden farklı olmalı", "edited": "düzenlendi", "bot": "Bot", - "project_view": { "sort_by": { "created_at": "Oluşturulma tarihi", @@ -512,12 +509,10 @@ "name": "Ad" } }, - "toast": { "success": "Başarılı!", "error": "Hata!" }, - "links": { "toasts": { "created": { @@ -546,7 +541,6 @@ } } }, - "home": { "empty": { "quickstart_guide": "Hızlı başlangıç rehberiniz", @@ -614,7 +608,6 @@ "title": "Ana Sayfa", "star_us_on_github": "Bizi GitHub'da yıldızlayın" }, - "link": { "modal": { "url": { @@ -628,7 +621,6 @@ } } }, - "common": { "all": "Tümü", "states": "Durumlar", @@ -877,20 +869,17 @@ "apply": "Uygula", "applying": "Uygulanıyor" }, - "chart": { "x_axis": "X ekseni", "y_axis": "Y ekseni", "metric": "Metrik" }, - "form": { "title": { "required": "Başlık gereklidir", "max_length": "Başlık {length} karakterden az olmalı" } }, - "entity": { "grouping_title": "{entity} Gruplandırma", "priority": "{entity} Önceliği", @@ -914,7 +903,6 @@ "failed": "{entity} eklenirken hata oluştu" } }, - "epic": { "all": "Tüm Epikler", "label": "{count, plural, one {Epik} other {Epikler}}", @@ -932,7 +920,6 @@ "required": "Epik başlığı gereklidir." } }, - "issue": { "label": "{count, plural, one {İş öğesi} other {İş öğeleri}}", "all": "Tüm İş Öğeleri", @@ -1099,7 +1086,6 @@ }, "open_in_full_screen": "İş öğesini tam ekranda aç" }, - "attachment": { "error": "Dosya eklenemedi. Tekrar yüklemeyi deneyin.", "only_one_file_allowed": "Aynı anda yalnızca bir dosya yüklenebilir.", @@ -1107,7 +1093,6 @@ "drag_and_drop": "Yüklemek için herhangi bir yere sürükleyip bırakın", "delete": "Eki sil" }, - "label": { "select": "Etiket seç", "create": { @@ -1117,7 +1102,6 @@ "type": "Yeni etiket eklemek için yazın" } }, - "sub_work_item": { "update": { "success": "Alt iş öğesi başarıyla güncellendi", @@ -1128,7 +1112,6 @@ "error": "Alt iş öğesi kaldırılırken hata oluştu" } }, - "view": { "label": "{count, plural, one {Görünüm} other {Görünümler}}", "create": { @@ -1138,7 +1121,6 @@ "label": "Görünümü Güncelle" } }, - "inbox_issue": { "status": { "pending": { @@ -1224,7 +1206,6 @@ } } }, - "workspace_creation": { "heading": "Çalışma Alanınızı Oluşturun", "subheading": "Plane'i kullanmaya başlamak için bir çalışma alanı oluşturmalı veya katılmalısınız.", @@ -1276,7 +1257,6 @@ } } }, - "workspace_dashboard": { "empty_state": { "general": { @@ -1292,7 +1272,6 @@ } } }, - "workspace_analytics": { "label": "Analitik", "page_label": "{workspace} - Analitik", @@ -1335,9 +1314,39 @@ } } } - } + }, + "empty_state_v2": { + "customized_insights": { + "description": "Size atanan iş öğeleri, duruma göre ayrılarak burada gösterilecektir.", + "title": "Henüz veri yok" + }, + "created_vs_resolved": { + "description": "Zaman içinde oluşturulan ve çözümlenen iş öğeleri burada gösterilecektir.", + "title": "Henüz veri yok" + }, + "project_insights": { + "title": "Henüz veri yok", + "description": "Size atanan iş öğeleri, duruma göre ayrılarak burada gösterilecektir." + } + }, + "created_vs_resolved": "Oluşturulan vs Çözülen", + "customized_insights": "Özelleştirilmiş İçgörüler", + "backlog_work_items": "Backlog iş öğeleri", + "active_projects": "Aktif Projeler", + "trend_on_charts": "Grafiklerdeki eğilim", + "all_projects": "Tüm Projeler", + "summary_of_projects": "Projelerin Özeti", + "project_insights": "Proje İçgörüleri", + "started_work_items": "Başlatılan iş öğeleri", + "total_work_items": "Toplam iş öğesi", + "total_projects": "Toplam Proje", + "total_admins": "Toplam Yönetici", + "total_users": "Toplam Kullanıcı", + "total_intake": "Toplam Gelir", + "un_started_work_items": "Başlanmamış iş öğeleri", + "total_guests": "Toplam Misafir", + "completed_work_items": "Tamamlanmış iş öğeleri" }, - "workspace_projects": { "label": "{count, plural, one {Proje} other {Projeler}}", "create": { @@ -1412,7 +1421,6 @@ } } }, - "workspace_views": { "add_view": "Görünüm ekle", "empty_state": { @@ -1447,7 +1455,6 @@ } } }, - "workspace_settings": { "label": "Çalışma Alanı Ayarları", "page_label": "{workspace} - Genel ayarlar", @@ -1629,7 +1636,6 @@ } } }, - "profile": { "label": "Profil", "page_label": "Sizin İşleriniz", @@ -1692,7 +1698,6 @@ } } }, - "project_settings": { "general": { "enter_project_id": "Proje ID girin", @@ -1815,7 +1820,6 @@ "auto_close_status": "Otomatik kapatma durumu" } }, - "empty_state": { "labels": { "title": "Henüz etiket yok", @@ -1828,7 +1832,6 @@ } } }, - "project_cycles": { "add_cycle": "Döngü ekle", "more_details": "Daha fazla detay", @@ -1954,7 +1957,6 @@ } } }, - "project_issues": { "empty_state": { "no_issues": { @@ -1983,7 +1985,6 @@ } } }, - "project_module": { "add_module": "Modül Ekle", "update_module": "Modülü Güncelle", @@ -2037,7 +2038,6 @@ } } }, - "project_views": { "empty_state": { "general": { @@ -2057,7 +2057,6 @@ } } }, - "project_page": { "empty_state": { "general": { @@ -2087,7 +2086,6 @@ } } }, - "command_k": { "empty_state": { "search": { @@ -2095,7 +2093,6 @@ } } }, - "issue_relation": { "empty_state": { "search": { @@ -2106,7 +2103,6 @@ } } }, - "issue_comment": { "empty_state": { "general": { @@ -2115,7 +2111,6 @@ } } }, - "notification": { "label": "Bildirimler", "page_label": "{workspace} - Bildirimler", @@ -2172,7 +2167,6 @@ "custom": "Özel" } }, - "active_cycle": { "empty_state": { "progress": { @@ -2192,7 +2186,6 @@ } } }, - "disabled_project": { "empty_state": { "inbox": { @@ -2232,7 +2225,6 @@ } } }, - "workspace_draft_issues": { "draft_an_issue": "Taslak iş öğesi oluştur", "empty_state": { @@ -2256,7 +2248,6 @@ } } }, - "stickies": { "title": "Yapışkan Notlarınız", "placeholder": "buraya yazmak için tıkla", @@ -2314,7 +2305,6 @@ } } }, - "role_details": { "guest": { "title": "Misafir", @@ -2329,7 +2319,6 @@ "description": "Çalışma alanı içinde tüm izinler aktif." } }, - "user_roles": { "product_or_project_manager": "Ürün / Proje Yöneticisi", "development_or_engineering": "Geliştirme / Mühendislik", @@ -2342,7 +2331,6 @@ "human_resources": "İnsan Kaynakları", "other": "Diğer" }, - "importer": { "github": { "title": "Github", @@ -2353,7 +2341,6 @@ "description": "Jira projelerinden ve epiklerinden iş öğelerini içe aktarın." } }, - "exporter": { "csv": { "title": "CSV", @@ -2376,14 +2363,12 @@ "short_description": "JSON olarak aktar" } }, - "default_global_view": { "all_issues": "Tüm iş öğeleri", "assigned": "Atanan", "created": "Oluşturulan", "subscribed": "Abone olunan" }, - "themes": { "theme_options": { "system_preference": { @@ -2406,7 +2391,6 @@ } } }, - "project_modules": { "status": { "backlog": "Bekleme Listesi", @@ -2430,20 +2414,17 @@ "manual": "Manuel" } }, - "cycle": { "label": "{count, plural, one {Döngü} other {Döngüler}}", "no_cycle": "Döngü yok" }, - "module": { "label": "{count, plural, one {Modül} other {Modüller}}", "no_module": "Modül yok" }, - "description_versions": { "last_edited_by": "Son düzenleyen", "previously_edited_by": "Önceki düzenleyen", "edited_by": "Tarafından düzenlendi" } -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/ua/translations.json b/packages/i18n/src/locales/ua/translations.json index e7b0d6cd6d3..3d6357ba71f 100644 --- a/packages/i18n/src/locales/ua/translations.json +++ b/packages/i18n/src/locales/ua/translations.json @@ -502,7 +502,6 @@ "new_password_must_be_different_from_old_password": "Новий пароль повинен бути відмінним від старого пароля", "edited": "Редагувано", "bot": "Бот", - "project_view": { "sort_by": { "created_at": "Створено", @@ -1314,7 +1313,38 @@ } } } - } + }, + "empty_state_v2": { + "customized_insights": { + "description": "Призначені вам робочі елементи, розбиті за станом, з’являться тут.", + "title": "Ще немає даних" + }, + "created_vs_resolved": { + "description": "Створені та вирішені з часом робочі елементи з’являться тут.", + "title": "Ще немає даних" + }, + "project_insights": { + "title": "Ще немає даних", + "description": "Призначені вам робочі елементи, розбиті за станом, з’являться тут." + } + }, + "created_vs_resolved": "Створено vs Вирішено", + "customized_insights": "Персоналізовані аналітичні дані", + "backlog_work_items": "Робочі елементи у беклозі", + "active_projects": "Активні проєкти", + "trend_on_charts": "Тенденція на графіках", + "all_projects": "Усі проєкти", + "summary_of_projects": "Зведення проєктів", + "project_insights": "Аналітика проєкту", + "started_work_items": "Розпочаті робочі елементи", + "total_work_items": "Усього робочих елементів", + "total_projects": "Усього проєктів", + "total_admins": "Усього адміністраторів", + "total_users": "Усього користувачів", + "total_intake": "Загальний дохід", + "un_started_work_items": "Нерозпочаті робочі елементи", + "total_guests": "Усього гостей", + "completed_work_items": "Завершені робочі елементи" }, "workspace_projects": { "label": "{count, plural, one {Проєкт} few {Проєкти} other {Проєктів}}", @@ -2405,20 +2435,17 @@ "manual": "Вручну" } }, - "cycle": { "label": "{count, plural, one {Цикл} few {Цикли} other {Циклів}}", "no_cycle": "Немає циклу" }, - "module": { "label": "{count, plural, one {Модуль} few {Модулі} other {Модулів}}", "no_module": "Немає модуля" }, - "description_versions": { "last_edited_by": "Останнє редагування", "previously_edited_by": "Раніше відредаговано", "edited_by": "Відредаговано" } -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/vi-VN/translations.json b/packages/i18n/src/locales/vi-VN/translations.json index 9c3a0e5dcc6..6cecb51e816 100644 --- a/packages/i18n/src/locales/vi-VN/translations.json +++ b/packages/i18n/src/locales/vi-VN/translations.json @@ -502,7 +502,6 @@ "new_password_must_be_different_from_old_password": "Mật khẩu mới phải khác mật khẩu cũ", "edited": "đã chỉnh sửa", "bot": "bot", - "project_view": { "sort_by": { "created_at": "Thời gian tạo", @@ -1313,7 +1312,38 @@ } } } - } + }, + "empty_state_v2": { + "customized_insights": { + "description": "Các hạng mục công việc được giao cho bạn, phân loại theo trạng thái, sẽ hiển thị tại đây.", + "title": "Chưa có dữ liệu" + }, + "created_vs_resolved": { + "description": "Các hạng mục công việc được tạo và giải quyết theo thời gian sẽ hiển thị tại đây.", + "title": "Chưa có dữ liệu" + }, + "project_insights": { + "title": "Chưa có dữ liệu", + "description": "Các hạng mục công việc được giao cho bạn, phân loại theo trạng thái, sẽ hiển thị tại đây." + } + }, + "created_vs_resolved": "Đã tạo vs Đã giải quyết", + "customized_insights": "Thông tin chi tiết tùy chỉnh", + "backlog_work_items": "Các hạng mục công việc tồn đọng", + "active_projects": "Dự án đang hoạt động", + "trend_on_charts": "Xu hướng trên biểu đồ", + "all_projects": "Tất cả dự án", + "summary_of_projects": "Tóm tắt dự án", + "project_insights": "Thông tin chi tiết dự án", + "started_work_items": "Hạng mục công việc đã bắt đầu", + "total_work_items": "Tổng số hạng mục công việc", + "total_projects": "Tổng số dự án", + "total_admins": "Tổng số quản trị viên", + "total_users": "Tổng số người dùng", + "total_intake": "Tổng thu", + "un_started_work_items": "Hạng mục công việc chưa bắt đầu", + "total_guests": "Tổng số khách", + "completed_work_items": "Hạng mục công việc đã hoàn thành" }, "workspace_projects": { "label": "{count, plural, one {dự án} other {dự án}}", @@ -2403,20 +2433,17 @@ "manual": "Thủ công" } }, - "cycle": { "label": "{count, plural, one {chu kỳ} other {chu kỳ}}", "no_cycle": "Không có chu kỳ" }, - "module": { "label": "{count, plural, one {mô-đun} other {mô-đun}}", "no_module": "Không có mô-đun" }, - "description_versions": { "last_edited_by": "Chỉnh sửa lần cuối bởi", "previously_edited_by": "Trước đây được chỉnh sửa bởi", "edited_by": "Được chỉnh sửa bởi" } -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/zh-CN/translations.json b/packages/i18n/src/locales/zh-CN/translations.json index 2a5d6d43f92..51bac4c7512 100644 --- a/packages/i18n/src/locales/zh-CN/translations.json +++ b/packages/i18n/src/locales/zh-CN/translations.json @@ -18,7 +18,6 @@ "pro": "专业版", "upgrade": "升级" }, - "auth": { "common": { "email": { @@ -168,7 +167,6 @@ } } }, - "submit": "提交", "cancel": "取消", "loading": "加载中", @@ -504,7 +502,6 @@ "new_password_must_be_different_from_old_password": "新密码必须不同于旧密码", "edited": "已编辑", "bot": "机器人", - "project_view": { "sort_by": { "created_at": "创建时间", @@ -512,12 +509,10 @@ "name": "名称" } }, - "toast": { "success": "成功!", "error": "错误!" }, - "links": { "toasts": { "created": { @@ -546,7 +541,6 @@ } } }, - "home": { "empty": { "quickstart_guide": "快速入门指南", @@ -614,7 +608,6 @@ "title": "首页", "star_us_on_github": "在GitHub上为我们加星" }, - "link": { "modal": { "url": { @@ -628,7 +621,6 @@ } } }, - "common": { "all": "全部", "states": "状态", @@ -875,20 +867,17 @@ "apply": "应用", "applying": "应用中" }, - "chart": { "x_axis": "X轴", "y_axis": "Y轴", "metric": "指标" }, - "form": { "title": { "required": "标题为必填项", "max_length": "标题应少于 {length} 个字符" } }, - "entity": { "grouping_title": "{entity}分组", "priority": "{entity}优先级", @@ -912,7 +901,6 @@ "failed": "添加{entity}时出错" } }, - "epic": { "all": "所有史诗", "label": "{count, plural, one {史诗} other {史诗}}", @@ -930,7 +918,6 @@ "required": "史诗标题为必填项" } }, - "issue": { "label": "{count, plural, one {工作项} other {工作项}}", "all": "所有工作项", @@ -1097,7 +1084,6 @@ }, "open_in_full_screen": "在全屏中打开工作项" }, - "attachment": { "error": "无法附加文件。请重新上传。", "only_one_file_allowed": "一次只能上传一个文件。", @@ -1105,7 +1091,6 @@ "drag_and_drop": "拖放到任意位置以上传", "delete": "删除附件" }, - "label": { "select": "选择标签", "create": { @@ -1115,7 +1100,6 @@ "type": "输入以添加新标签" } }, - "sub_work_item": { "update": { "success": "子工作项更新成功", @@ -1126,7 +1110,6 @@ "error": "移除子工作项时出错" } }, - "view": { "label": "{count, plural, one {视图} other {视图}}", "create": { @@ -1136,7 +1119,6 @@ "label": "更新视图" } }, - "inbox_issue": { "status": { "pending": { @@ -1222,7 +1204,6 @@ } } }, - "workspace_creation": { "heading": "创建您的工作区", "subheading": "要开始使用 Plane,您需要创建或加入一个工作区。", @@ -1274,7 +1255,6 @@ } } }, - "workspace_dashboard": { "empty_state": { "general": { @@ -1290,7 +1270,6 @@ } } }, - "workspace_analytics": { "label": "分析", "page_label": "{workspace} - 分析", @@ -1333,9 +1312,39 @@ } } } - } + }, + "empty_state_v2": { + "customized_insights": { + "description": "分配给您的工作项将按状态分类显示在此处。", + "title": "暂无数据" + }, + "created_vs_resolved": { + "description": "随着时间推移创建和解决的工作项将显示在此处。", + "title": "暂无数据" + }, + "project_insights": { + "title": "暂无数据", + "description": "分配给您的工作项将按状态分类显示在此处。" + } + }, + "created_vs_resolved": "已创建 vs 已解决", + "customized_insights": "自定义洞察", + "backlog_work_items": "待办工作项", + "active_projects": "活跃项目", + "trend_on_charts": "图表趋势", + "all_projects": "所有项目", + "summary_of_projects": "项目概览", + "project_insights": "项目洞察", + "started_work_items": "已开始的工作项", + "total_work_items": "工作项总数", + "total_projects": "项目总数", + "total_admins": "管理员总数", + "total_users": "用户总数", + "total_intake": "总收入", + "un_started_work_items": "未开始的工作项", + "total_guests": "访客总数", + "completed_work_items": "已完成的工作项" }, - "workspace_projects": { "label": "{count, plural, one {项目} other {项目}}", "create": { @@ -1409,7 +1418,6 @@ } } }, - "workspace_views": { "add_view": "添加视图", "empty_state": { @@ -1444,7 +1452,6 @@ } } }, - "workspace_settings": { "label": "工作区设置", "page_label": "{workspace} - 常规设置", @@ -1626,7 +1633,6 @@ } } }, - "profile": { "label": "个人资料", "page_label": "您的工作", @@ -1689,7 +1695,6 @@ } } }, - "project_settings": { "general": { "enter_project_id": "输入项目 ID", @@ -1816,7 +1821,6 @@ "auto_close_status": "自动关闭状态" } }, - "empty_state": { "labels": { "title": "尚无标签", @@ -1829,7 +1833,6 @@ } } }, - "project_cycles": { "add_cycle": "添加周期", "more_details": "更多详情", @@ -1955,7 +1958,6 @@ } } }, - "project_issues": { "empty_state": { "no_issues": { @@ -1984,7 +1986,6 @@ } } }, - "project_module": { "add_module": "添加模块", "update_module": "更新模块", @@ -2038,7 +2039,6 @@ } } }, - "project_views": { "empty_state": { "general": { @@ -2058,7 +2058,6 @@ } } }, - "project_page": { "empty_state": { "general": { @@ -2088,7 +2087,6 @@ } } }, - "command_k": { "empty_state": { "search": { @@ -2096,7 +2094,6 @@ } } }, - "issue_relation": { "empty_state": { "search": { @@ -2107,7 +2104,6 @@ } } }, - "issue_comment": { "empty_state": { "general": { @@ -2116,7 +2112,6 @@ } } }, - "notification": { "label": "收件箱", "page_label": "{workspace} - 收件箱", @@ -2173,7 +2168,6 @@ "custom": "自定义" } }, - "active_cycle": { "empty_state": { "progress": { @@ -2193,7 +2187,6 @@ } } }, - "disabled_project": { "empty_state": { "inbox": { @@ -2256,7 +2249,6 @@ } } }, - "stickies": { "title": "您的便签", "placeholder": "点击此处输入", @@ -2314,7 +2306,6 @@ } } }, - "role_details": { "guest": { "title": "访客", @@ -2329,7 +2320,6 @@ "description": "在工作区内所有权限均设置为允许。" } }, - "user_roles": { "product_or_project_manager": "产品/项目经理", "development_or_engineering": "开发/工程", @@ -2342,7 +2332,6 @@ "human_resources": "人力资源", "other": "其他" }, - "importer": { "github": { "title": "GitHub", @@ -2353,7 +2342,6 @@ "description": "从 Jira 项目和史诗导入工作项和史诗。" } }, - "exporter": { "csv": { "title": "CSV", @@ -2382,7 +2370,6 @@ "created": "已创建", "subscribed": "已订阅" }, - "themes": { "theme_options": { "system_preference": { @@ -2428,20 +2415,17 @@ "manual": "手动" } }, - "cycle": { "label": "{count, plural, one {周期} other {周期}}", "no_cycle": "无周期" }, - "module": { "label": "{count, plural, one {模块} other {模块}}", "no_module": "无模块" }, - "description_versions": { "last_edited_by": "最后编辑者", "previously_edited_by": "之前编辑者", "edited_by": "编辑者" } -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/zh-TW/translations.json b/packages/i18n/src/locales/zh-TW/translations.json index 9ccc176a5a5..c27f678c84b 100644 --- a/packages/i18n/src/locales/zh-TW/translations.json +++ b/packages/i18n/src/locales/zh-TW/translations.json @@ -18,7 +18,6 @@ "pro": "專業版", "upgrade": "升級" }, - "auth": { "common": { "email": { @@ -168,7 +167,6 @@ } } }, - "submit": "送出", "cancel": "取消", "loading": "載入中", @@ -504,7 +502,6 @@ "new_password_must_be_different_from_old_password": "新密碼必須與舊密碼不同", "edited": "已編輯", "bot": "機器人", - "project_view": { "sort_by": { "created_at": "建立時間", @@ -512,12 +509,10 @@ "name": "名稱" } }, - "toast": { "success": "成功!", "error": "錯誤!" }, - "links": { "toasts": { "created": { @@ -546,7 +541,6 @@ } } }, - "home": { "empty": { "quickstart_guide": "您的快速入門指南", @@ -614,7 +608,6 @@ "title": "首頁", "star_us_on_github": "在 GitHub 上給我們星星" }, - "link": { "modal": { "url": { @@ -628,7 +621,6 @@ } } }, - "common": { "all": "全部", "states": "狀態", @@ -876,20 +868,17 @@ "apply": "應用", "applying": "應用中" }, - "chart": { "x_axis": "X 軸", "y_axis": "Y 軸", "metric": "指標" }, - "form": { "title": { "required": "標題為必填", "max_length": "標題不應超過 {length} 個字元" } }, - "entity": { "grouping_title": "{entity} 分組", "priority": "{entity} 優先順序", @@ -913,7 +902,6 @@ "failed": "新增 {entity} 時發生錯誤" } }, - "epic": { "all": "所有 Epic", "label": "{count, plural, one {Epic} other {Epic}}", @@ -931,7 +919,6 @@ "required": "Epic 標題為必填。" } }, - "issue": { "label": "{count, plural, one {工作事項} other {工作事項}}", "all": "所有工作事項", @@ -1098,7 +1085,6 @@ }, "open_in_full_screen": "以全螢幕開啟工作事項" }, - "attachment": { "error": "無法附加檔案。請重新上傳。", "only_one_file_allowed": "一次只能上傳一個檔案。", @@ -1106,7 +1092,6 @@ "drag_and_drop": "拖曳到任何位置以上傳", "delete": "刪除附件" }, - "label": { "select": "選擇標籤", "create": { @@ -1116,7 +1101,6 @@ "type": "輸入以新增標籤" } }, - "sub_work_item": { "update": { "success": "子工作事項更新成功", @@ -1127,7 +1111,6 @@ "error": "移除子工作事項時發生錯誤" } }, - "view": { "label": "{count, plural, one {檢視} other {檢視}}", "create": { @@ -1137,7 +1120,6 @@ "label": "更新檢視" } }, - "inbox_issue": { "status": { "pending": { @@ -1223,7 +1205,6 @@ } } }, - "workspace_creation": { "heading": "建立您的工作區", "subheading": "若要開始使用 Plane,您需要建立或加入工作區。", @@ -1275,7 +1256,6 @@ } } }, - "workspace_dashboard": { "empty_state": { "general": { @@ -1291,7 +1271,6 @@ } } }, - "workspace_analytics": { "label": "分析", "page_label": "{workspace} - 分析", @@ -1334,9 +1313,39 @@ } } } - } + }, + "empty_state_v2": { + "customized_insights": { + "description": "指派給您的工作項目將依狀態分類顯示在此處。", + "title": "尚無資料" + }, + "created_vs_resolved": { + "description": "隨著時間推移所建立與解決的工作項目將顯示在此處。", + "title": "尚無資料" + }, + "project_insights": { + "title": "尚無資料", + "description": "指派給您的工作項目將依狀態分類顯示在此處。" + } + }, + "created_vs_resolved": "已建立 vs 已解決", + "customized_insights": "自訂化洞察", + "backlog_work_items": "待辦工作項目", + "active_projects": "啟用中的專案", + "trend_on_charts": "圖表趨勢", + "all_projects": "所有專案", + "summary_of_projects": "專案摘要", + "project_insights": "專案洞察", + "started_work_items": "已開始的工作項目", + "total_work_items": "工作項目總數", + "total_projects": "專案總數", + "total_admins": "管理員總數", + "total_users": "使用者總數", + "total_intake": "總收入", + "un_started_work_items": "未開始的工作項目", + "total_guests": "訪客總數", + "completed_work_items": "已完成的工作項目" }, - "workspace_projects": { "label": "{count, plural, one {專案} other {專案}}", "create": { @@ -1411,7 +1420,6 @@ } } }, - "workspace_views": { "add_view": "新增檢視", "empty_state": { @@ -1446,7 +1454,6 @@ } } }, - "workspace_settings": { "label": "工作區設定", "page_label": "{workspace} - 一般設定", @@ -1628,7 +1635,6 @@ } } }, - "profile": { "label": "個人資料", "page_label": "您的工作", @@ -1691,7 +1697,6 @@ } } }, - "project_settings": { "general": { "enter_project_id": "輸入專案 ID", @@ -1837,7 +1842,6 @@ "auto_close_status": "自動關閉狀態" } }, - "empty_state": { "labels": { "title": "尚無標籤", @@ -1850,7 +1854,6 @@ } } }, - "project_cycles": { "add_cycle": "新增週期", "more_details": "更多詳細資訊", @@ -1976,7 +1979,6 @@ } } }, - "project_issues": { "empty_state": { "no_issues": { @@ -2005,7 +2007,6 @@ } } }, - "project_module": { "add_module": "新增模組", "update_module": "更新模組", @@ -2059,7 +2060,6 @@ } } }, - "project_views": { "empty_state": { "general": { @@ -2079,7 +2079,6 @@ } } }, - "project_page": { "empty_state": { "general": { @@ -2109,7 +2108,6 @@ } } }, - "command_k": { "empty_state": { "search": { @@ -2117,7 +2115,6 @@ } } }, - "issue_relation": { "empty_state": { "search": { @@ -2128,7 +2125,6 @@ } } }, - "issue_comment": { "empty_state": { "general": { @@ -2137,7 +2133,6 @@ } } }, - "notification": { "label": "收件匣", "page_label": "{workspace} - 收件匣", @@ -2194,7 +2189,6 @@ "custom": "自訂" } }, - "active_cycle": { "empty_state": { "progress": { @@ -2214,7 +2208,6 @@ } } }, - "disabled_project": { "empty_state": { "inbox": { @@ -2277,7 +2270,6 @@ } } }, - "stickies": { "title": "您的便利貼", "placeholder": "點選此處輸入", @@ -2335,7 +2327,6 @@ } } }, - "role_details": { "guest": { "title": "訪客", @@ -2350,7 +2341,6 @@ "description": "工作區內的所有權限都設為允許。" } }, - "user_roles": { "product_or_project_manager": "產品/專案經理", "development_or_engineering": "開發/工程", @@ -2363,7 +2353,6 @@ "human_resources": "人力資源", "other": "其他" }, - "importer": { "github": { "title": "GitHub", @@ -2374,7 +2363,6 @@ "description": "從 Jira 專案和 Epic 匯入工作事項和 Epic。" } }, - "exporter": { "csv": { "title": "CSV", @@ -2403,7 +2391,6 @@ "created": "已建立", "subscribed": "已訂閱" }, - "themes": { "theme_options": { "system_preference": { @@ -2449,20 +2436,17 @@ "manual": "手動" } }, - "cycle": { "label": "{count, plural, one {週期} other {週期}}", "no_cycle": "無週期" }, - "module": { "label": "{count, plural, one {模組} other {模組}}", "no_module": "無模組" }, - "description_versions": { "last_edited_by": "最後編輯者", "previously_edited_by": "先前編輯者", "edited_by": "編輯者" } -} +} \ No newline at end of file From 03316b0bf53ce5b65f5db284d7feb09f840deb64 Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Tue, 6 May 2025 23:40:41 +0530 Subject: [PATCH 43/69] updated serrchbar for the table --- .../analytics-v2/insight-table/data-table.tsx | 69 +++++++++++++++---- 1 file changed, 57 insertions(+), 12 deletions(-) diff --git a/web/core/components/analytics-v2/insight-table/data-table.tsx b/web/core/components/analytics-v2/insight-table/data-table.tsx index 7ebbde6920f..9064a1c7157 100644 --- a/web/core/components/analytics-v2/insight-table/data-table.tsx +++ b/web/core/components/analytics-v2/insight-table/data-table.tsx @@ -16,7 +16,7 @@ import { useReactTable, } from "@tanstack/react-table" -import { Search } from "lucide-react" +import { Search, X } from "lucide-react" import { useTranslation } from "@plane/i18n" import { Table, @@ -27,6 +27,7 @@ import { TableRow, } from "@plane/propel/table" import { Input } from "@plane/ui" +import { cn } from "@plane/utils" import AnalyticsV2EmptyState from "../empty-state" interface DataTableProps { @@ -48,6 +49,8 @@ export function DataTable({ ) const [sorting, setSorting] = React.useState([]) const { t } = useTranslation() + const inputRef = React.useRef(null) + const [isSearchOpen, setIsSearchOpen] = React.useState(false) const table = useReactTable({ data, @@ -73,17 +76,59 @@ export function DataTable({ return (
- {table.getHeaderGroups()?.[0]?.headers?.[0]?.id &&
- { - table.getColumn(table.getHeaderGroups()?.[0].headers[0].id)?.setFilterValue(event.target.value) - }} - className="w-30 border-none" - /> - -
} + +
+ {table.getHeaderGroups()?.[0]?.headers?.[0]?.id &&
+ {searchPlaceholder} +
} + {!isSearchOpen && ( + + )} +
+ + table.getColumn(table.getHeaderGroups()?.[0].headers[0].id)?.setFilterValue(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + setIsSearchOpen(true) + } + }} + /> + {isSearchOpen && ( + + )} +
+
+
From 717d14a8ba5af3c0f97172fb26a5069b533f54f5 Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Wed, 7 May 2025 00:54:31 +0530 Subject: [PATCH 44/69] added work item modal in project analytics --- .../issues/(list)/mobile-header.tsx | 4 +- .../analytics-v2/insight-table/data-table.tsx | 1 - .../analytics-v2/total-insights.tsx | 2 +- .../analytics-v2/work-items/modal/content.tsx | 42 ++++++++++++ .../analytics-v2/work-items/modal/header.tsx | 37 +++++++++++ .../analytics-v2/work-items/modal/index.tsx | 65 +++++++++++++++++++ web/core/components/issues/filters.tsx | 3 +- 7 files changed, 149 insertions(+), 5 deletions(-) create mode 100644 web/core/components/analytics-v2/work-items/modal/content.tsx create mode 100644 web/core/components/analytics-v2/work-items/modal/header.tsx create mode 100644 web/core/components/analytics-v2/work-items/modal/index.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/mobile-header.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/mobile-header.tsx index 6b86cd88d95..a9777ce23d5 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/mobile-header.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/mobile-header.tsx @@ -20,7 +20,7 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption // ui import { CustomMenu } from "@plane/ui"; // components -import { ProjectAnalyticsModal } from "@/components/analytics"; +import { WorkItemsModal } from "@/components/analytics-v2/work-items/modal"; import { DisplayFiltersSelection, FilterSelection, @@ -105,7 +105,7 @@ export const ProjectIssuesMobileHeader = observer(() => { return ( <> - setAnalyticsModal(false)} projectDetails={currentProjectDetails ?? undefined} diff --git a/web/core/components/analytics-v2/insight-table/data-table.tsx b/web/core/components/analytics-v2/insight-table/data-table.tsx index 9064a1c7157..5c89a0d4004 100644 --- a/web/core/components/analytics-v2/insight-table/data-table.tsx +++ b/web/core/components/analytics-v2/insight-table/data-table.tsx @@ -76,7 +76,6 @@ export function DataTable({ return (
-
{table.getHeaderGroups()?.[0]?.headers?.[0]?.id &&
{searchPlaceholder} diff --git a/web/core/components/analytics-v2/total-insights.tsx b/web/core/components/analytics-v2/total-insights.tsx index d937bccb0d5..8bac87d94d7 100644 --- a/web/core/components/analytics-v2/total-insights.tsx +++ b/web/core/components/analytics-v2/total-insights.tsx @@ -30,7 +30,7 @@ const TotalInsights: React.FC<{ analyticsType: TAnalyticsTabsV2Base }> = observe })) return ( -
{insightsFields[analyticsType].map((item: string) => ( diff --git a/web/core/components/analytics-v2/work-items/modal/content.tsx b/web/core/components/analytics-v2/work-items/modal/content.tsx new file mode 100644 index 00000000000..edc2271ee0f --- /dev/null +++ b/web/core/components/analytics-v2/work-items/modal/content.tsx @@ -0,0 +1,42 @@ +import React, { useEffect, useState } from "react"; +import { observer } from "mobx-react"; +import { Tab } from "@headlessui/react"; +// plane package imports +import { IProject } from "@plane/types"; +// components +import { Spinner } from "@plane/ui"; +import { useAnalyticsV2 } from "@/hooks/store"; +import TotalInsights from "../../total-insights"; +import CreatedVsResolved from "../created-vs-resolved"; +import CustomizedInsights from "../customized-insights"; +import WorkItemsInsightTable from "../workitems-insight-table"; + +type Props = { + fullScreen: boolean; + projectDetails: IProject | undefined; +}; + +export const WorkItemsModalMainContent: React.FC = observer((props) => { + const { projectDetails } = props; + const { updateSelectedProjects } = useAnalyticsV2() + const [isProjectConfigured, setIsProjectConfigured] = useState(false) + + useEffect(() => { + if (!projectDetails?.id) return; + updateSelectedProjects([projectDetails?.id ?? '']) + setIsProjectConfigured(true) + }, [projectDetails?.id, updateSelectedProjects]) + + if (!isProjectConfigured) return
+ + return ( + +
+ + + + +
+
+ ); +}); diff --git a/web/core/components/analytics-v2/work-items/modal/header.tsx b/web/core/components/analytics-v2/work-items/modal/header.tsx new file mode 100644 index 00000000000..e57b4ef97a9 --- /dev/null +++ b/web/core/components/analytics-v2/work-items/modal/header.tsx @@ -0,0 +1,37 @@ +import { observer } from "mobx-react"; + +// icons +import { Expand, Shrink, X } from "lucide-react"; + +type Props = { + fullScreen: boolean; + handleClose: () => void; + setFullScreen: React.Dispatch>; + title: string; +}; + +export const WorkItemsModalHeader: React.FC = observer((props) => { + const { fullScreen, handleClose, setFullScreen, title } = props; + + return ( +
+

Analytics for {title}

+
+ + +
+
+ ); +}); diff --git a/web/core/components/analytics-v2/work-items/modal/index.tsx b/web/core/components/analytics-v2/work-items/modal/index.tsx new file mode 100644 index 00000000000..624f930dcc9 --- /dev/null +++ b/web/core/components/analytics-v2/work-items/modal/index.tsx @@ -0,0 +1,65 @@ +import React, { useState } from "react"; +import { observer } from "mobx-react"; +import { Dialog, Transition } from "@headlessui/react"; +import { IProject } from "@plane/types"; + +import { WorkItemsModalMainContent } from "./content"; +import { WorkItemsModalHeader } from "./header"; + + +type Props = { + isOpen: boolean; + onClose: () => void; + projectDetails?: IProject | undefined; +}; + +export const WorkItemsModal: React.FC = observer((props) => { + const { isOpen, onClose, projectDetails } = props; + + const [fullScreen, setFullScreen] = useState(false); + + const handleClose = () => { + onClose(); + }; + + return ( + + + +
+ +
+
+ + +
+
+
+
+
+
+
+ ); +}); diff --git a/web/core/components/issues/filters.tsx b/web/core/components/issues/filters.tsx index 346c960b769..a50afe50ebc 100644 --- a/web/core/components/issues/filters.tsx +++ b/web/core/components/issues/filters.tsx @@ -18,6 +18,7 @@ import { useLabel, useProjectState, useMember, useIssues } from "@/hooks/store"; // plane web types import { TProject } from "@/plane-web/types"; import { ProjectAnalyticsModal } from "../analytics"; +import { WorkItemsModal } from "../analytics-v2/work-items/modal"; type Props = { currentProjectDetails: TProject | undefined; @@ -97,7 +98,7 @@ const HeaderFilters = observer((props: Props) => { return ( <> - setAnalyticsModal(false)} projectDetails={currentProjectDetails ?? undefined} From 8010c76f1a9377cca93b8d2f40a84ea8c00cc6b0 Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Wed, 7 May 2025 15:53:42 +0530 Subject: [PATCH 45/69] fixed some of the layput issues in the peek view --- .../analytics-v2/analytics-section-wrapper.tsx | 5 +++-- .../components/analytics-v2/select/analytics-params.tsx | 9 +++++---- web/core/components/analytics-v2/total-insights.tsx | 9 ++++++--- .../analytics-v2/work-items/customized-insights.tsx | 4 +++- .../components/analytics-v2/work-items/modal/content.tsx | 6 +++--- 5 files changed, 20 insertions(+), 13 deletions(-) diff --git a/web/core/components/analytics-v2/analytics-section-wrapper.tsx b/web/core/components/analytics-v2/analytics-section-wrapper.tsx index f88b8d39142..6b4dfa6a770 100644 --- a/web/core/components/analytics-v2/analytics-section-wrapper.tsx +++ b/web/core/components/analytics-v2/analytics-section-wrapper.tsx @@ -6,13 +6,14 @@ type Props = { className?: string subtitle?: string | null, actions?: React.ReactNode + headerClassName?: string } const AnalyticsSectionWrapper: React.FC = (props) => { - const { title, children, className, subtitle, actions } = props + const { title, children, className, subtitle, actions, headerClassName } = props return (
-
+
{title &&

{title}

{subtitle &&

• {subtitle}

} diff --git a/web/core/components/analytics-v2/select/analytics-params.tsx b/web/core/components/analytics-v2/select/analytics-params.tsx index 36bf2844fb3..f3a472eedb1 100644 --- a/web/core/components/analytics-v2/select/analytics-params.tsx +++ b/web/core/components/analytics-v2/select/analytics-params.tsx @@ -19,12 +19,13 @@ type Props = { setValue: UseFormSetValue; params: IAnalyticsV2Params; workspaceSlug: string; + classNames?: string; }; const analyticsV2Service = new AnalyticsV2Service() export const AnalyticsV2SelectParams: React.FC = observer((props) => { - const { control, params, workspaceSlug } = props; + const { control, params, workspaceSlug, classNames } = props; const { t } = useTranslation(); const xAxisOptions = useMemo(() => ANALYTICS_V2_X_AXIS_VALUES.filter((option) => option.value !== params.group_by), [params.group_by]); const groupByOptions = useMemo(() => ANALYTICS_V2_X_AXIS_VALUES.filter((option) => option.value !== params.x_axis), [params.x_axis]); @@ -48,8 +49,8 @@ export const AnalyticsV2SelectParams: React.FC = observer((props) => { ); }; return ( -
- +
= observer((props) => { /> )} /> - +
diff --git a/web/core/components/analytics-v2/total-insights.tsx b/web/core/components/analytics-v2/total-insights.tsx index 8bac87d94d7..cd9355063a4 100644 --- a/web/core/components/analytics-v2/total-insights.tsx +++ b/web/core/components/analytics-v2/total-insights.tsx @@ -17,7 +17,7 @@ import InsightCard from './insight-card'; const analyticsV2Service = new AnalyticsV2Service(); -const TotalInsights: React.FC<{ analyticsType: TAnalyticsTabsV2Base }> = observer(({ analyticsType }) => { +const TotalInsights: React.FC<{ analyticsType: TAnalyticsTabsV2Base, peekView?: boolean }> = observer(({ analyticsType, peekView }) => { const params = useParams(); const workspaceSlug = params.workspaceSlug as string; const { t } = useTranslation() @@ -28,10 +28,13 @@ const TotalInsights: React.FC<{ analyticsType: TAnalyticsTabsV2Base }> = observe date_filter: selectedDuration, ...(selectedProjects ? { project_ids: selectedProjects } : {}) })) - return (
{insightsFields[analyticsType].map((item: string) => ( { +const CustomizedInsights = observer(({ peekView }: { peekView?: boolean }) => { const { t } = useTranslation() const { workspaceSlug } = useParams(); const { control, watch, setValue } = useForm({ @@ -30,6 +31,7 @@ const CustomizedInsights = observer(() => { return ( = observer((props) => { - const { projectDetails } = props; + const { projectDetails, fullScreen } = props; const { updateSelectedProjects } = useAnalyticsV2() const [isProjectConfigured, setIsProjectConfigured] = useState(false) @@ -32,9 +32,9 @@ export const WorkItemsModalMainContent: React.FC = observer((props) => { return (
- + - +
From bdc9eb0c95bbbd33697de7b0d1dda4bcb1560430 Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Wed, 7 May 2025 16:34:24 +0530 Subject: [PATCH 46/69] chore: updated the base function for viewsets --- apiserver/plane/app/views/analytic/advance.py | 72 ++++++------------- apiserver/plane/utils/date_utils.py | 15 ++-- 2 files changed, 34 insertions(+), 53 deletions(-) diff --git a/apiserver/plane/app/views/analytic/advance.py b/apiserver/plane/app/views/analytic/advance.py index 8dca649a081..31342f6a788 100644 --- a/apiserver/plane/app/views/analytic/advance.py +++ b/apiserver/plane/app/views/analytic/advance.py @@ -19,23 +19,25 @@ ) from plane.utils.build_chart import build_analytics_chart from datetime import timedelta -from django.db.models.functions import TruncDay from plane.bgtasks.analytic_plot_export import export_analytics_to_csv_email from plane.utils.date_utils import ( get_analytics_filters, ) -class AdvanceAnalyticsEndpoint(BaseAPIView): - def initialize_workspace(self, slug): +class AdvanceAnalyticsBaseView(BaseAPIView): + def initialize_workspace(self, slug, type): self._workspace_slug = slug self.filters = get_analytics_filters( slug=slug, + type=type, user=self.request.user, date_filter=self.request.GET.get("date_filter", None), project_ids=self.request.GET.get("project_ids", None), ) + +class AdvanceAnalyticsEndpoint(AdvanceAnalyticsBaseView): def get_filtered_counts(self, queryset): def get_filtered_count(): if self.filters["analytics_date_range"]: @@ -124,7 +126,7 @@ def get_work_items_stats(self): base_queryset.filter(state__group="backlog") ), "un_started_work_items": self.get_filtered_counts( - base_queryset.filter(state__group="un-started") + base_queryset.filter(state__group="unstarted") ), "completed_work_items": self.get_filtered_counts( base_queryset.filter(state__group="completed") @@ -133,7 +135,7 @@ def get_work_items_stats(self): @allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE") def get(self, request, slug): - self.initialize_workspace(slug) + self.initialize_workspace(slug, type="analytics") tab = request.GET.get("tab", "overview") if tab == "overview": @@ -151,16 +153,7 @@ def get(self, request, slug): return Response({"message": "Invalid tab"}, status=status.HTTP_400_BAD_REQUEST) -class AdvanceAnalyticsStatsEndpoint(BaseAPIView): - def initialize_workspace(self, slug): - self._workspace_slug = slug - self.filters = get_analytics_filters( - slug=slug, - user=self.request.user, - date_filter=self.request.GET.get("date_filter", None), - project_ids=self.request.GET.get("project_ids", None), - ) - +class AdvanceAnalyticsStatsEndpoint(AdvanceAnalyticsBaseView): def get_project_issues_stats(self): # Get the base queryset with workspace and project filters base_queryset = Issue.issue_objects.filter(**self.filters["base_filters"]) @@ -178,7 +171,7 @@ def get_project_issues_stats(self): cancelled_work_items=Count("id", filter=Q(state__group="cancelled")), completed_work_items=Count("id", filter=Q(state__group="completed")), backlog_work_items=Count("id", filter=Q(state__group="backlog")), - un_started_work_items=Count("id", filter=Q(state__group="un-started")), + un_started_work_items=Count("id", filter=Q(state__group="unstarted")), started_work_items=Count("id", filter=Q(state__group="started")), ) .order_by("project_id") @@ -186,7 +179,7 @@ def get_project_issues_stats(self): @allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE") def get(self, request, slug): - self.initialize_workspace(slug) + self.initialize_workspace(slug, type="chart") type = request.GET.get("type", "work-items") if type == "work-items": @@ -198,16 +191,7 @@ def get(self, request, slug): return Response({"message": "Invalid type"}, status=status.HTTP_400_BAD_REQUEST) -class AdvanceAnalyticsChartEndpoint(BaseAPIView): - def initialize_workspace(self, slug): - self._workspace_slug = slug - self.filters = get_analytics_filters( - slug=slug, - user=self.request.user, - date_filter=self.request.GET.get("date_filter", None), - project_ids=self.request.GET.get("project_ids", None), - ) - +class AdvanceAnalyticsChartEndpoint(AdvanceAnalyticsBaseView): def project_chart(self): # Get the base queryset with workspace and project filters base_queryset = Issue.issue_objects.filter(**self.filters["base_filters"]) @@ -275,20 +259,19 @@ def work_item_completion_chart(self): created_at__date__gte=start_date, created_at__date__lte=end_date ) - # Get daily stats + # Get daily stats with optimized query daily_stats = ( - queryset.annotate( - date=TruncDay("created_at"), - created_count=Count("id", filter=Q(created_at__isnull=False)), + queryset.values("created_at__date") + .annotate( + created_count=Count("id"), completed_count=Count("id", filter=Q(completed_at__isnull=False)), ) - .values("date", "created_count", "completed_count") - .order_by("date") + .order_by("created_at__date") ) - # Create a dictionary of existing stats + # Create a dictionary of existing stats with summed counts stats_dict = { - stat["date"].strftime("%Y-%m-%d"): { + stat["created_at__date"].strftime("%Y-%m-%d"): { "created_count": stat["created_count"], "completed_count": stat["completed_count"], } @@ -321,7 +304,7 @@ def work_item_completion_chart(self): @allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE") def get(self, request, slug): - self.initialize_workspace(slug) + self.initialize_workspace(slug, type="chart") type = request.GET.get("type", "projects") group_by = request.GET.get("group_by", None) x_axis = request.GET.get("x_axis", "PRIORITY") @@ -360,19 +343,10 @@ def get(self, request, slug): return Response({"message": "Invalid type"}, status=status.HTTP_400_BAD_REQUEST) -class AdvanceAnalyticsExportEndpoint(BaseAPIView): - def initialize_workspace(self, slug): - self._workspace_slug = slug - self.filters = get_analytics_filters( - slug=slug, - user=self.request.user, - date_filter=self.request.GET.get("date_filter", None), - project_ids=self.request.GET.get("project_ids", None), - ) - +class AdvanceAnalyticsExportEndpoint(AdvanceAnalyticsBaseView): @allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE") - def get(self, request, slug): - self.initialize_workspace(slug) + def post(self, request, slug): + self.initialize_workspace(slug, type="chart") data = ( Issue.issue_objects.filter(**self.filters["base_filters"]) .values("project_id", "project__name") @@ -380,7 +354,7 @@ def get(self, request, slug): cancelled_work_items=Count("id", filter=Q(state__group="cancelled")), completed_work_items=Count("id", filter=Q(state__group="completed")), backlog_work_items=Count("id", filter=Q(state__group="backlog")), - un_started_work_items=Count("id", filter=Q(state__group="un-started")), + un_started_work_items=Count("id", filter=Q(state__group="unstarted")), started_work_items=Count("id", filter=Q(state__group="started")), ) .order_by("project_id") diff --git a/apiserver/plane/utils/date_utils.py b/apiserver/plane/utils/date_utils.py index dfb6639ac86..0c1da6b7f5b 100644 --- a/apiserver/plane/utils/date_utils.py +++ b/apiserver/plane/utils/date_utils.py @@ -120,13 +120,14 @@ def get_chart_period_range(date_filter=None): return period_ranges.get(date_filter, period_ranges["last_7_days"]) -def get_analytics_filters(slug, user, date_filter=None, project_ids=None): +def get_analytics_filters(slug, user, type, date_filter=None, project_ids=None): """ Get combined project and date filters for analytics endpoints Args: slug: The workspace slug user: The current user + type: The type of filter ("analytics" or "chart") date_filter: Optional date filter string project_ids: Optional list of project IDs @@ -160,9 +161,15 @@ def get_analytics_filters(slug, user, date_filter=None, project_ids=None): base_filters["project_id__in"] = project_ids project_filters["id__in"] = project_ids - # Get date range filters - analytics_date_range = get_analytics_date_range(date_filter) - chart_period_range = get_chart_period_range(date_filter) + # Initialize date range variables + analytics_date_range = None + chart_period_range = None + + # Get date range filters based on type + if type == "analytics": + analytics_date_range = get_analytics_date_range(date_filter) + elif type == "chart": + chart_period_range = get_chart_period_range(date_filter) return { "base_filters": base_filters, From 6a775d3a28788d86fd087d4163627a1efe8c95c4 Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Wed, 7 May 2025 17:53:18 +0530 Subject: [PATCH 47/69] synced tab to url --- .../(projects)/analytics-v2/page.tsx | 13 +++++++++---- web/ce/components/analytics-v2/tabs.ts | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/web/app/[workspaceSlug]/(projects)/analytics-v2/page.tsx b/web/app/[workspaceSlug]/(projects)/analytics-v2/page.tsx index 6aeed71a539..5e89d17b60e 100644 --- a/web/app/[workspaceSlug]/(projects)/analytics-v2/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/analytics-v2/page.tsx @@ -2,8 +2,7 @@ import { useMemo } from "react"; import { observer } from "mobx-react"; - -import { useSearchParams } from "next/navigation"; +import { useRouter, useSearchParams } from "next/navigation"; // plane package imports import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; @@ -18,6 +17,8 @@ import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; import { ANALYTICS_TABS } from "@/plane-web/components/analytics-v2/tabs"; const AnalyticsPage = observer(() => { + const router = useRouter(); + const searchParams = useSearchParams(); // plane imports const { t } = useTranslation(); // store hooks @@ -43,7 +44,11 @@ const AnalyticsPage = observer(() => { key: tab.key, label: t(tab.i18nKey), content: , - })), [t]); + onClick: () => { + router.push(`?tab=${tab.key}`); + } + })), [router, t]); + const defaultTab = searchParams.get("tab") || ANALYTICS_TABS[0].key; return ( <> @@ -55,7 +60,7 @@ const AnalyticsPage = observer(() => { Date: Wed, 7 May 2025 18:07:52 +0530 Subject: [PATCH 48/69] code cleanup --- .../components/analytics-v2/analytics-wrapper.tsx | 1 + web/core/components/analytics-v2/empty-state.tsx | 1 + .../analytics-v2/insight-table/data-table.tsx | 4 ++-- .../analytics-v2/overview/active-project-item.tsx | 4 +++- .../analytics-v2/overview/active-projects.tsx | 3 +++ .../analytics-v2/overview/project-insights.tsx | 5 ++++- .../analytics-v2/select/analytics-params.tsx | 8 ++++---- .../components/analytics-v2/select/duration.tsx | 6 ++++-- .../components/analytics-v2/select/project.tsx | 5 +++-- .../analytics-v2/select/select-x-axis.tsx | 3 +-- .../analytics-v2/select/select-y-axis.tsx | 2 +- .../work-items/created-vs-resolved.tsx | 6 +++++- .../work-items/customized-insights.tsx | 2 ++ .../analytics-v2/work-items/modal/content.tsx | 3 ++- .../analytics-v2/work-items/modal/index.tsx | 3 ++- .../analytics-v2/work-items/priority-chart.tsx | 15 ++++++++++++--- .../components/analytics-v2/work-items/utils.ts | 1 + .../work-items/workitems-insight-table.tsx | 8 +++++++- 18 files changed, 58 insertions(+), 22 deletions(-) diff --git a/web/core/components/analytics-v2/analytics-wrapper.tsx b/web/core/components/analytics-v2/analytics-wrapper.tsx index ec4df17bb58..09ce1b1932e 100644 --- a/web/core/components/analytics-v2/analytics-wrapper.tsx +++ b/web/core/components/analytics-v2/analytics-wrapper.tsx @@ -1,4 +1,5 @@ import React from 'react' +// plane package imports import { cn } from '@plane/utils'; type Props = { diff --git a/web/core/components/analytics-v2/empty-state.tsx b/web/core/components/analytics-v2/empty-state.tsx index f6f130e1c55..b34f3aae8aa 100644 --- a/web/core/components/analytics-v2/empty-state.tsx +++ b/web/core/components/analytics-v2/empty-state.tsx @@ -1,5 +1,6 @@ import React from 'react' import Image from 'next/image'; +// plane package imports import { cn } from '@plane/utils' type Props = { diff --git a/web/core/components/analytics-v2/insight-table/data-table.tsx b/web/core/components/analytics-v2/insight-table/data-table.tsx index 5c89a0d4004..e748b73ea18 100644 --- a/web/core/components/analytics-v2/insight-table/data-table.tsx +++ b/web/core/components/analytics-v2/insight-table/data-table.tsx @@ -15,8 +15,8 @@ import { getSortedRowModel, useReactTable, } from "@tanstack/react-table" - import { Search, X } from "lucide-react" +// plane package imports import { useTranslation } from "@plane/i18n" import { Table, @@ -26,8 +26,8 @@ import { TableHeader, TableRow, } from "@plane/propel/table" -import { Input } from "@plane/ui" import { cn } from "@plane/utils" +// plane web components import AnalyticsV2EmptyState from "../empty-state" interface DataTableProps { diff --git a/web/core/components/analytics-v2/overview/active-project-item.tsx b/web/core/components/analytics-v2/overview/active-project-item.tsx index 6e92d8d8617..b1d042bf06b 100644 --- a/web/core/components/analytics-v2/overview/active-project-item.tsx +++ b/web/core/components/analytics-v2/overview/active-project-item.tsx @@ -1,6 +1,8 @@ import { Briefcase } from 'lucide-react'; -import { Loader, Logo } from '@plane/ui'; +// plane package imports +import { Logo } from '@plane/ui'; import { cn } from '@plane/utils'; +// plane web hooks import { useProject } from '@/hooks/store'; diff --git a/web/core/components/analytics-v2/overview/active-projects.tsx b/web/core/components/analytics-v2/overview/active-projects.tsx index b2ffc81a56c..dfddaeac30d 100644 --- a/web/core/components/analytics-v2/overview/active-projects.tsx +++ b/web/core/components/analytics-v2/overview/active-projects.tsx @@ -2,9 +2,12 @@ import React from 'react' import { observer } from 'mobx-react' import { useParams } from 'next/navigation' import useSWR from 'swr' +// plane package imports import { useTranslation } from '@plane/i18n' import { Loader } from '@plane/ui' +// plane web hooks import { useAnalyticsV2, useProject } from '@/hooks/store' +// plane web components import AnalyticsSectionWrapper from '../analytics-section-wrapper' import ActiveProjectItem from './active-project-item' diff --git a/web/core/components/analytics-v2/overview/project-insights.tsx b/web/core/components/analytics-v2/overview/project-insights.tsx index a269b7b838d..9d8bbcc1c0b 100644 --- a/web/core/components/analytics-v2/overview/project-insights.tsx +++ b/web/core/components/analytics-v2/overview/project-insights.tsx @@ -2,11 +2,14 @@ import { observer } from 'mobx-react' import dynamic from 'next/dynamic' import { useParams } from 'next/navigation' import useSWR from 'swr' +// plane package imports import { useTranslation } from '@plane/i18n' import { TChartData } from '@plane/types' +// hooks import { useAnalyticsV2 } from '@/hooks/store/use-analytics-v2' +// services import { AnalyticsV2Service } from '@/services/analytics-v2.service' -// import useSWR from 'swr' +// plane web components import AnalyticsSectionWrapper from '../analytics-section-wrapper' import AnalyticsV2EmptyState from '../empty-state' import { ProjectInsightsLoader } from '../loaders' diff --git a/web/core/components/analytics-v2/select/analytics-params.tsx b/web/core/components/analytics-v2/select/analytics-params.tsx index f3a472eedb1..4d1fcee5fc3 100644 --- a/web/core/components/analytics-v2/select/analytics-params.tsx +++ b/web/core/components/analytics-v2/select/analytics-params.tsx @@ -1,18 +1,18 @@ import { useMemo } from "react"; import { observer } from "mobx-react"; import { Control, Controller, UseFormSetValue } from "react-hook-form"; -// plane imports import { Calendar, Download, SlidersHorizontal } from "lucide-react"; +// plane package imports import { ANALYTICS_V2_X_AXIS_VALUES, ANALYTICS_V2_Y_AXIS_VALUES, ChartYAxisMetric } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { IAnalyticsV2Params } from "@plane/types"; -import { Button, Row, setToast, TOAST_TYPE } from "@plane/ui"; -// components +import { Button, setToast, TOAST_TYPE } from "@plane/ui"; import { cn } from "@plane/utils"; +// plane web components import { AnalyticsV2Service } from "@/services/analytics-v2.service"; import { SelectXAxis } from "./select-x-axis"; import { SelectYAxis } from "./select-y-axis"; -// hooks + type Props = { control: Control; diff --git a/web/core/components/analytics-v2/select/duration.tsx b/web/core/components/analytics-v2/select/duration.tsx index 6ad6a8d5244..e673491d65e 100644 --- a/web/core/components/analytics-v2/select/duration.tsx +++ b/web/core/components/analytics-v2/select/duration.tsx @@ -1,13 +1,15 @@ // plane package imports import React, { ReactNode } from 'react' import { Calendar } from 'lucide-react' +// plane package imports import { ANALYTICS_V2_DURATION_FILTER_OPTIONS } from '@plane/constants' import { useTranslation } from '@plane/i18n' import { CustomSearchSelect } from '@plane/ui' -// plane web components -// components +// types import { TDropdownProps } from '@/components/dropdowns/types' + + type Props = TDropdownProps & { value: string | null onChange: (val: typeof ANALYTICS_V2_DURATION_FILTER_OPTIONS[number]['value']) => void diff --git a/web/core/components/analytics-v2/select/project.tsx b/web/core/components/analytics-v2/select/project.tsx index 40bcc34c9f7..aebd6223351 100644 --- a/web/core/components/analytics-v2/select/project.tsx +++ b/web/core/components/analytics-v2/select/project.tsx @@ -1,11 +1,12 @@ "use client"; import { observer } from "mobx-react"; -// hooks import { Briefcase } from "lucide-react"; +// plane package imports import { CustomSearchSelect } from "@plane/ui"; +// hooks import { useProject } from "@/hooks/store"; -// ui + type Props = { value: string[] | undefined; diff --git a/web/core/components/analytics-v2/select/select-x-axis.tsx b/web/core/components/analytics-v2/select/select-x-axis.tsx index 817992c442a..7b3dc7606e6 100644 --- a/web/core/components/analytics-v2/select/select-x-axis.tsx +++ b/web/core/components/analytics-v2/select/select-x-axis.tsx @@ -1,7 +1,6 @@ "use client"; - +// plane package imports import { ChartXAxisProperty } from "@plane/constants"; -// ui import { CustomSelect } from "@plane/ui"; type Props = { diff --git a/web/core/components/analytics-v2/select/select-y-axis.tsx b/web/core/components/analytics-v2/select/select-y-axis.tsx index c209f57aecf..37e4acbbfbf 100644 --- a/web/core/components/analytics-v2/select/select-y-axis.tsx +++ b/web/core/components/analytics-v2/select/select-y-axis.tsx @@ -2,8 +2,8 @@ import { observer } from "mobx-react"; import { useParams } from "next/navigation"; -// plane imports import { Briefcase } from "lucide-react"; +// plane package imports import { ChartYAxisMetric } from "@plane/constants"; import { CustomSelect } from "@plane/ui"; // hooks diff --git a/web/core/components/analytics-v2/work-items/created-vs-resolved.tsx b/web/core/components/analytics-v2/work-items/created-vs-resolved.tsx index 0f26e03cc44..3ba548ed544 100644 --- a/web/core/components/analytics-v2/work-items/created-vs-resolved.tsx +++ b/web/core/components/analytics-v2/work-items/created-vs-resolved.tsx @@ -3,12 +3,16 @@ import { useMemo } from 'react' import { observer } from 'mobx-react' import { useParams } from 'next/navigation' import useSWR from 'swr' +// plane package imports import { useTranslation } from '@plane/i18n' import { AreaChart } from '@plane/propel/charts/area-chart' -import { IChartResponseV2, TAreaItem } from '@plane/types' +import { IChartResponseV2 } from '@plane/types' import { renderFormattedDate } from '@plane/utils' +// hooks import { useAnalyticsV2 } from '@/hooks/store/use-analytics-v2' +// services import { AnalyticsV2Service } from '@/services/analytics-v2.service' +// plane web components import AnalyticsSectionWrapper from '../analytics-section-wrapper' import AnalyticsV2EmptyState from '../empty-state' import { ChartLoader } from '../loaders' diff --git a/web/core/components/analytics-v2/work-items/customized-insights.tsx b/web/core/components/analytics-v2/work-items/customized-insights.tsx index 5fdfecdd93e..17bbdb2dd34 100644 --- a/web/core/components/analytics-v2/work-items/customized-insights.tsx +++ b/web/core/components/analytics-v2/work-items/customized-insights.tsx @@ -1,10 +1,12 @@ import { observer } from 'mobx-react' import { useParams } from 'next/navigation' import { useForm } from 'react-hook-form' +// plane package imports import { ChartXAxisProperty, ChartYAxisMetric } from '@plane/constants' import { useTranslation } from '@plane/i18n' import { IAnalyticsV2Params } from '@plane/types' import { cn } from '@plane/utils' +// plane web components import AnalyticsSectionWrapper from '../analytics-section-wrapper' import { AnalyticsV2SelectParams } from '../select/analytics-params' import PriorityChart from './priority-chart' diff --git a/web/core/components/analytics-v2/work-items/modal/content.tsx b/web/core/components/analytics-v2/work-items/modal/content.tsx index e16e6eec973..db667a3ea85 100644 --- a/web/core/components/analytics-v2/work-items/modal/content.tsx +++ b/web/core/components/analytics-v2/work-items/modal/content.tsx @@ -3,9 +3,10 @@ import { observer } from "mobx-react"; import { Tab } from "@headlessui/react"; // plane package imports import { IProject } from "@plane/types"; -// components import { Spinner } from "@plane/ui"; +// hooks import { useAnalyticsV2 } from "@/hooks/store"; +// plane web components import TotalInsights from "../../total-insights"; import CreatedVsResolved from "../created-vs-resolved"; import CustomizedInsights from "../customized-insights"; diff --git a/web/core/components/analytics-v2/work-items/modal/index.tsx b/web/core/components/analytics-v2/work-items/modal/index.tsx index 624f930dcc9..90c9dec152e 100644 --- a/web/core/components/analytics-v2/work-items/modal/index.tsx +++ b/web/core/components/analytics-v2/work-items/modal/index.tsx @@ -1,8 +1,9 @@ import React, { useState } from "react"; import { observer } from "mobx-react"; import { Dialog, Transition } from "@headlessui/react"; +// plane package imports import { IProject } from "@plane/types"; - +// plane web components import { WorkItemsModalMainContent } from "./content"; import { WorkItemsModalHeader } from "./header"; diff --git a/web/core/components/analytics-v2/work-items/priority-chart.tsx b/web/core/components/analytics-v2/work-items/priority-chart.tsx index d79d103898b..7c2c819eeae 100644 --- a/web/core/components/analytics-v2/work-items/priority-chart.tsx +++ b/web/core/components/analytics-v2/work-items/priority-chart.tsx @@ -4,12 +4,15 @@ import { observer } from 'mobx-react' import { useParams } from 'next/navigation' import { useTheme } from 'next-themes' import useSWR from 'swr' +// plane package imports import { ANALYTICS_V2_X_AXIS_VALUES, ANALYTICS_V2_Y_AXIS_VALUES, ChartXAxisDateGrouping, ChartXAxisProperty, ChartYAxisMetric, EChartModels } from '@plane/constants' import { useTranslation } from '@plane/i18n' import { BarChart } from '@plane/propel/charts/bar-chart' import { IChartResponseV2 } from '@plane/types' import { TBarItem, TChartDatum } from '@plane/types/src/charts' +// plane web components import { CHART_COLOR_PALETTES, generateExtendedColors, parseChartData } from '@/components/chart/utils' +// hooks import { useProjectState } from '@/hooks/store' import { useAnalyticsV2 } from '@/hooks/store/use-analytics-v2' import { useWorkspaceIssueProperties } from '@/hooks/use-workspace-issue-properties' @@ -18,6 +21,10 @@ import AnalyticsV2EmptyState from '../empty-state' import { DataTable } from '../insight-table/data-table' import { ChartLoader } from '../loaders' import { generateBarColor } from './utils' + + + + interface Props { x_axis: ChartXAxisProperty y_axis: ChartYAxisMetric @@ -28,11 +35,13 @@ interface Props { const analyticsV2Service = new AnalyticsV2Service() const PriorityChart = observer((props: Props) => { const { x_axis, y_axis, group_by } = props; - const { selectedDuration, selectedProjects } = useAnalyticsV2() - const params = useParams(); - const { resolvedTheme } = useTheme(); const { t } = useTranslation() + // store hooks + const { selectedDuration, selectedProjects } = useAnalyticsV2() const { workspaceStates } = useProjectState() + const { resolvedTheme } = useTheme(); + // router + const params = useParams(); const workspaceSlug = params.workspaceSlug as string; useWorkspaceIssueProperties(workspaceSlug); diff --git a/web/core/components/analytics-v2/work-items/utils.ts b/web/core/components/analytics-v2/work-items/utils.ts index f42c73f9f64..3e3b3692dc5 100644 --- a/web/core/components/analytics-v2/work-items/utils.ts +++ b/web/core/components/analytics-v2/work-items/utils.ts @@ -1,3 +1,4 @@ +// plane package imports import { ChartXAxisProperty, ChartYAxisMetric } from "@plane/constants"; import { IState } from "@plane/types"; diff --git a/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx b/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx index 823836060b6..53622a07412 100644 --- a/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx +++ b/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx @@ -4,18 +4,24 @@ import { observer } from 'mobx-react'; import { useParams } from 'next/navigation'; import useSWR from 'swr'; import { Briefcase } from 'lucide-react'; +// plane package imports import { WorkItemInsightColumns, AnalyticsTableDataMap } from '@plane/types'; +// plane web components import { Logo } from '@/components/common/logo'; +// hooks import { useAnalyticsV2 } from '@/hooks/store/use-analytics-v2'; import { useProject } from '@/hooks/store/use-project'; import { AnalyticsV2Service } from '@/services/analytics-v2.service'; +// plane web components import { InsightTable } from '../insight-table'; const analyticsV2Service = new AnalyticsV2Service(); const WorkItemsInsightTable = observer(() => { + // router const params = useParams(); const workspaceSlug = params.workspaceSlug as string; + // store hooks const { getProjectById } = useProject(); const { selectedDuration, selectedProjects } = useAnalyticsV2() const { data: workItemsData, isLoading } = useSWR(`insights-table-work-items-${selectedDuration}-${selectedProjects}`, @@ -23,7 +29,7 @@ const WorkItemsInsightTable = observer(() => { date_filter: selectedDuration, ...(selectedProjects ? { project_ids: selectedProjects } : {}) })) - + // derived values const columns = useMemo(() => [ { accessorKey: "project__name", From 9c2877b169fe62842ebcb48348892fe75cad4274 Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Wed, 7 May 2025 19:55:42 +0530 Subject: [PATCH 49/69] chore: updated the export logic --- apiserver/plane/app/views/analytic/advance.py | 23 ++++++++++--------- apiserver/plane/utils/date_utils.py | 3 +++ 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/apiserver/plane/app/views/analytic/advance.py b/apiserver/plane/app/views/analytic/advance.py index 31342f6a788..8926fb469ae 100644 --- a/apiserver/plane/app/views/analytic/advance.py +++ b/apiserver/plane/app/views/analytic/advance.py @@ -347,9 +347,17 @@ class AdvanceAnalyticsExportEndpoint(AdvanceAnalyticsBaseView): @allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE") def post(self, request, slug): self.initialize_workspace(slug, type="chart") - data = ( - Issue.issue_objects.filter(**self.filters["base_filters"]) - .values("project_id", "project__name") + queryset = Issue.issue_objects.filter(**self.filters["base_filters"]) + + # Apply date range filter if available + if self.filters["chart_period_range"]: + start_date, end_date = self.filters["chart_period_range"] + queryset = queryset.filter( + created_at__date__gte=start_date, created_at__date__lte=end_date + ) + + queryset = ( + queryset.values("project_id", "project__name") .annotate( cancelled_work_items=Count("id", filter=Q(state__group="cancelled")), completed_work_items=Count("id", filter=Q(state__group="completed")), @@ -360,15 +368,8 @@ def post(self, request, slug): .order_by("project_id") ) - # Apply date range filter if available - if self.filters["chart_period_range"]: - start_date, end_date = self.filters["chart_period_range"] - data = data.filter( - created_at__date__gte=start_date, created_at__date__lte=end_date - ) - # Convert QuerySet to list of dictionaries for serialization - serialized_data = list(data) + serialized_data = list(queryset) headers = [ "Projects", diff --git a/apiserver/plane/utils/date_utils.py b/apiserver/plane/utils/date_utils.py index 0c1da6b7f5b..3550b789581 100644 --- a/apiserver/plane/utils/date_utils.py +++ b/apiserver/plane/utils/date_utils.py @@ -106,6 +106,9 @@ def get_chart_period_range(date_filter=None): Returns: tuple: A tuple containing (start_date, end_date) as date objects """ + if not date_filter: + return None + today = timezone.now().date() period_ranges = { "yesterday": ( From 214f04e53ccdfe8ce771ec91b9a39a36124367a5 Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Wed, 7 May 2025 20:53:44 +0530 Subject: [PATCH 50/69] fixed project_ids filter --- .../components/analytics-v2/overview/project-insights.tsx | 5 +++-- web/core/components/analytics-v2/total-insights.tsx | 2 +- .../analytics-v2/work-items/created-vs-resolved.tsx | 5 +++-- .../components/analytics-v2/work-items/priority-chart.tsx | 2 +- .../analytics-v2/work-items/workitems-insight-table.tsx | 2 +- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/web/core/components/analytics-v2/overview/project-insights.tsx b/web/core/components/analytics-v2/overview/project-insights.tsx index 9d8bbcc1c0b..2721fec72e5 100644 --- a/web/core/components/analytics-v2/overview/project-insights.tsx +++ b/web/core/components/analytics-v2/overview/project-insights.tsx @@ -27,12 +27,13 @@ const ProjectInsights = observer(() => { const params = useParams(); const { t } = useTranslation() const workspaceSlug = params.workspaceSlug as string; - const { selectedDuration, selectedDurationLabel } = useAnalyticsV2() + const { selectedDuration, selectedDurationLabel, selectedProjects } = useAnalyticsV2() const { data: projectInsightsData, isLoading: isLoadingProjectInsight } = useSWR( `radar-chart-${workspaceSlug}`, () => analyticsV2Service.getAdvanceAnalyticsCharts[]>(workspaceSlug, 'projects', { - created_at: selectedDuration + created_at: selectedDuration, + project_ids: selectedProjects?.join(','), }), ) diff --git a/web/core/components/analytics-v2/total-insights.tsx b/web/core/components/analytics-v2/total-insights.tsx index cd9355063a4..9a86bd35c6b 100644 --- a/web/core/components/analytics-v2/total-insights.tsx +++ b/web/core/components/analytics-v2/total-insights.tsx @@ -26,7 +26,7 @@ const TotalInsights: React.FC<{ analyticsType: TAnalyticsTabsV2Base, peekView?: const { data: totalInsightsData, isLoading } = useSWR(`total-insights-${analyticsType}-${selectedDuration}-${selectedProjects}`, () => analyticsV2Service.getAdvanceAnalytics(workspaceSlug, analyticsType, { date_filter: selectedDuration, - ...(selectedProjects ? { project_ids: selectedProjects } : {}) + ...(selectedProjects ? { project_ids: selectedProjects.join(',') } : {}) })) return (
{ - const { selectedDuration, selectedDurationLabel } = useAnalyticsV2() + const { selectedDuration, selectedDurationLabel, selectedProjects } = useAnalyticsV2() const params = useParams(); const { t } = useTranslation() const workspaceSlug = params.workspaceSlug as string; const { data: createdVsResolvedData, isLoading: isCreatedVsResolvedLoading } = useSWR( `created-vs-resolved-${workspaceSlug}-${selectedDuration}`, () => analyticsV2Service.getAdvanceAnalyticsCharts(workspaceSlug, 'work-items', { - date_filter: selectedDuration + date_filter: selectedDuration, + project_ids: selectedProjects?.join(','), }), ) const parsedData = useMemo(() => { diff --git a/web/core/components/analytics-v2/work-items/priority-chart.tsx b/web/core/components/analytics-v2/work-items/priority-chart.tsx index 7c2c819eeae..2f7e2cf3c33 100644 --- a/web/core/components/analytics-v2/work-items/priority-chart.tsx +++ b/web/core/components/analytics-v2/work-items/priority-chart.tsx @@ -50,7 +50,7 @@ const PriorityChart = observer((props: Props) => { `customized-insights-chart-${workspaceSlug}-${selectedDuration}-${props.x_axis}-${props.y_axis}-${props.group_by}`, () => analyticsV2Service.getAdvanceAnalyticsCharts(workspaceSlug, 'custom-work-items', { date_filter: selectedDuration, - project_ids: selectedProjects, + project_ids: selectedProjects?.join(','), ...props }), ) diff --git a/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx b/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx index 53622a07412..80d76a3c3bc 100644 --- a/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx +++ b/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx @@ -27,7 +27,7 @@ const WorkItemsInsightTable = observer(() => { const { data: workItemsData, isLoading } = useSWR(`insights-table-work-items-${selectedDuration}-${selectedProjects}`, () => analyticsV2Service.getAdvanceAnalyticsStats(workspaceSlug, "work-items", { date_filter: selectedDuration, - ...(selectedProjects ? { project_ids: selectedProjects } : {}) + ...(selectedProjects ? { project_ids: selectedProjects.join(',') } : {}) })) // derived values const columns = useMemo(() => [ From aecbb144fee851186e2b34532055cf97df7315ec Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Wed, 7 May 2025 20:57:00 +0530 Subject: [PATCH 51/69] added icon in projectdropdown --- web/core/components/analytics-v2/select/project.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/core/components/analytics-v2/select/project.tsx b/web/core/components/analytics-v2/select/project.tsx index aebd6223351..794c905ad2e 100644 --- a/web/core/components/analytics-v2/select/project.tsx +++ b/web/core/components/analytics-v2/select/project.tsx @@ -3,7 +3,7 @@ import { observer } from "mobx-react"; import { Briefcase } from "lucide-react"; // plane package imports -import { CustomSearchSelect } from "@plane/ui"; +import { CustomSearchSelect, Logo } from "@plane/ui"; // hooks import { useProject } from "@/hooks/store"; @@ -26,7 +26,7 @@ export const ProjectSelect: React.FC = observer((props) => { query: `${projectDetails?.name} ${projectDetails?.identifier}`, content: (
- {projectDetails?.identifier} + {projectDetails?.logo_props ? : } {projectDetails?.name}
), From 47c6d6d97735cd1232d777f0c3c5be2c0e31c557 Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Wed, 7 May 2025 21:08:32 +0530 Subject: [PATCH 52/69] updated export button position --- .../analytics-v2/insight-table/data-table.tsx | 89 ++++++++++--------- .../analytics-v2/select/analytics-params.tsx | 22 +---- .../work-items/priority-chart.tsx | 23 +++++ 3 files changed, 71 insertions(+), 63 deletions(-) diff --git a/web/core/components/analytics-v2/insight-table/data-table.tsx b/web/core/components/analytics-v2/insight-table/data-table.tsx index e748b73ea18..0a29d7985ac 100644 --- a/web/core/components/analytics-v2/insight-table/data-table.tsx +++ b/web/core/components/analytics-v2/insight-table/data-table.tsx @@ -34,12 +34,14 @@ interface DataTableProps { columns: ColumnDef[] data: TData[] searchPlaceholder: string + actions?: React.ReactNode } export function DataTable({ columns, data, searchPlaceholder, + actions, }: DataTableProps) { const [rowSelection, setRowSelection] = React.useState({}) const [columnVisibility, setColumnVisibility] = @@ -76,56 +78,59 @@ export function DataTable({ return (
-
- {table.getHeaderGroups()?.[0]?.headers?.[0]?.id &&
- {searchPlaceholder} -
} - {!isSearchOpen && ( - - )} -
- - table.getColumn(table.getHeaderGroups()?.[0].headers[0].id)?.setFilterValue(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter") { - setIsSearchOpen(true) - } - }} - /> - {isSearchOpen && ( +
+
+ {table.getHeaderGroups()?.[0]?.headers?.[0]?.id &&
+ {searchPlaceholder} +
} + {!isSearchOpen && ( )} +
+ + table.getColumn(table.getHeaderGroups()?.[0].headers[0].id)?.setFilterValue(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + setIsSearchOpen(true) + } + }} + /> + {isSearchOpen && ( + + )} +
+ {actions &&
{actions}
}
diff --git a/web/core/components/analytics-v2/select/analytics-params.tsx b/web/core/components/analytics-v2/select/analytics-params.tsx index 4d1fcee5fc3..91782fe6f1c 100644 --- a/web/core/components/analytics-v2/select/analytics-params.tsx +++ b/web/core/components/analytics-v2/select/analytics-params.tsx @@ -30,24 +30,7 @@ export const AnalyticsV2SelectParams: React.FC = observer((props) => { const xAxisOptions = useMemo(() => ANALYTICS_V2_X_AXIS_VALUES.filter((option) => option.value !== params.group_by), [params.group_by]); const groupByOptions = useMemo(() => ANALYTICS_V2_X_AXIS_VALUES.filter((option) => option.value !== params.x_axis), [params.x_axis]); - const exportAnalytics = () => { - analyticsV2Service - .exportAnalytics(workspaceSlug, params) - .then((res) => { - setToast({ - type: TOAST_TYPE.SUCCESS, - title: "Success!", - message: res.message, - }); - }) - .catch(() => - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: "There was some error in exporting the analytics. Please try again.", - }) - ); - }; + return (
= observer((props) => { )} />
-
); diff --git a/web/core/components/analytics-v2/work-items/priority-chart.tsx b/web/core/components/analytics-v2/work-items/priority-chart.tsx index 2f7e2cf3c33..68fbc9f3a02 100644 --- a/web/core/components/analytics-v2/work-items/priority-chart.tsx +++ b/web/core/components/analytics-v2/work-items/priority-chart.tsx @@ -5,12 +5,14 @@ import { useParams } from 'next/navigation' import { useTheme } from 'next-themes' import useSWR from 'swr' // plane package imports +import { Download } from 'lucide-react' import { ANALYTICS_V2_X_AXIS_VALUES, ANALYTICS_V2_Y_AXIS_VALUES, ChartXAxisDateGrouping, ChartXAxisProperty, ChartYAxisMetric, EChartModels } from '@plane/constants' import { useTranslation } from '@plane/i18n' import { BarChart } from '@plane/propel/charts/bar-chart' import { IChartResponseV2 } from '@plane/types' import { TBarItem, TChartDatum } from '@plane/types/src/charts' // plane web components +import { Button, setToast, TOAST_TYPE } from '@plane/ui' import { CHART_COLOR_PALETTES, generateExtendedColors, parseChartData } from '@/components/chart/utils' // hooks import { useProjectState } from '@/hooks/store' @@ -54,6 +56,24 @@ const PriorityChart = observer((props: Props) => { ...props }), ) + const exportAnalytics = () => { + analyticsV2Service + .exportAnalytics(workspaceSlug, params) + .then((res) => { + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Success!", + message: res.message, + }); + }) + .catch(() => + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "There was some error in exporting the analytics. Please try again.", + }) + ); + }; const parsedData = useMemo(() => parseChartData(priorityChartData, props.x_axis, props.group_by, props.x_axis_date_grouping) , [priorityChartData, props.x_axis, props.group_by, props.x_axis_date_grouping]) const chart_model = props.group_by ? EChartModels.STACKED : EChartModels.BASIC; @@ -168,6 +188,9 @@ const PriorityChart = observer((props: Props) => { data={parsedData.data} columns={[...defaultColumns, ...columns]} searchPlaceholder={`${parsedData.data.length} ${yAxisLabel}`} + actions={} /> : Date: Thu, 8 May 2025 18:20:54 +0530 Subject: [PATCH 53/69] export csv and emptystates icons --- .../components/analytics-v2/empty-state.tsx | 68 ++++--- .../analytics-v2/insight-table/data-table.tsx | 9 +- .../analytics-v2/insight-table/root.tsx | 41 ++++- .../overview/project-insights.tsx | 6 +- .../analytics-v2/select/analytics-params.tsx | 8 +- .../work-items/created-vs-resolved.tsx | 3 + .../work-items/priority-chart.tsx | 47 +++-- .../work-items/workitems-insight-table.tsx | 25 ++- web/package.json | 3 +- .../analytics-v2/empty-chart-area-dark.webp | Bin 0 -> 2720 bytes .../analytics-v2/empty-chart-area-light.webp | Bin 0 -> 694 bytes .../analytics-v2/empty-chart-bar-dark.webp | Bin 0 -> 2508 bytes .../analytics-v2/empty-chart-bar-light.webp | Bin 0 -> 512 bytes .../analytics-v2/empty-chart-radar-dark.webp | Bin 0 -> 3076 bytes .../analytics-v2/empty-chart-radar-light.webp | Bin 0 -> 716 bytes .../empty-grid-background-dark.webp | Bin 0 -> 35070 bytes .../empty-grid-background-light.webp | Bin 0 -> 2576 bytes .../analytics-v2/empty-table-dark.webp | Bin 0 -> 3280 bytes .../analytics-v2/empty-table-light.webp | Bin 0 -> 862 bytes yarn.lock | 169 +++++++++++++++++- 20 files changed, 307 insertions(+), 72 deletions(-) create mode 100644 web/public/empty-state/analytics-v2/empty-chart-area-dark.webp create mode 100644 web/public/empty-state/analytics-v2/empty-chart-area-light.webp create mode 100644 web/public/empty-state/analytics-v2/empty-chart-bar-dark.webp create mode 100644 web/public/empty-state/analytics-v2/empty-chart-bar-light.webp create mode 100644 web/public/empty-state/analytics-v2/empty-chart-radar-dark.webp create mode 100644 web/public/empty-state/analytics-v2/empty-chart-radar-light.webp create mode 100644 web/public/empty-state/analytics-v2/empty-grid-background-dark.webp create mode 100644 web/public/empty-state/analytics-v2/empty-grid-background-light.webp create mode 100644 web/public/empty-state/analytics-v2/empty-table-dark.webp create mode 100644 web/public/empty-state/analytics-v2/empty-table-light.webp diff --git a/web/core/components/analytics-v2/empty-state.tsx b/web/core/components/analytics-v2/empty-state.tsx index b34f3aae8aa..ac8d0e17748 100644 --- a/web/core/components/analytics-v2/empty-state.tsx +++ b/web/core/components/analytics-v2/empty-state.tsx @@ -2,39 +2,63 @@ import React from 'react' import Image from 'next/image'; // plane package imports import { cn } from '@plane/utils' +import { useResolvedAssetPath } from '@/hooks/use-resolved-asset-path'; type Props = { - title: string; - description?: string; - assetPath?: string; - className?: string; + title: string; + description?: string; + assetPath?: string; + className?: string; } const AnalyticsV2EmptyState = ({ - title, - description, - assetPath, - className, -}: Props) => ( + title, + description, + assetPath, + className, +}: Props) => { + const backgroundReolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics-v2/empty-grid-background" }); + + return (
-
- {assetPath && ( - - )} -
-

{title}

- {description &&

{description}

} +
+ {assetPath && ( +
+ +
+
+
+ )} +
+

{title}

+ {description &&

{description}

}
-
-) +
+
+ ) +} export default AnalyticsV2EmptyState diff --git a/web/core/components/analytics-v2/insight-table/data-table.tsx b/web/core/components/analytics-v2/insight-table/data-table.tsx index 0a29d7985ac..bf0722cf5a8 100644 --- a/web/core/components/analytics-v2/insight-table/data-table.tsx +++ b/web/core/components/analytics-v2/insight-table/data-table.tsx @@ -6,6 +6,7 @@ import { ColumnFiltersState, SortingState, VisibilityState, + Table as TanstackTable, flexRender, getCoreRowModel, getFacetedRowModel, @@ -28,13 +29,14 @@ import { } from "@plane/propel/table" import { cn } from "@plane/utils" // plane web components +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path" import AnalyticsV2EmptyState from "../empty-state" interface DataTableProps { columns: ColumnDef[] data: TData[] searchPlaceholder: string - actions?: React.ReactNode + actions?: (table: TanstackTable) => React.ReactNode } export function DataTable({ @@ -53,6 +55,8 @@ export function DataTable({ const { t } = useTranslation() const inputRef = React.useRef(null) const [isSearchOpen, setIsSearchOpen] = React.useState(false) + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics-v2/empty-table" }); + const table = useReactTable({ data, @@ -130,7 +134,7 @@ export function DataTable({ )}
- {actions &&
{actions}
} + {actions &&
{actions(table)}
}
@@ -176,6 +180,7 @@ export function DataTable({ title={t('workspace_analytics.empty_state_v2.customized_insights.title')} description={t('workspace_analytics.empty_state_v2.customized_insights.description')} className="border-0" + assetPath={resolvedPath} />
diff --git a/web/core/components/analytics-v2/insight-table/root.tsx b/web/core/components/analytics-v2/insight-table/root.tsx index e2f16e1b3c6..69e40718c4a 100644 --- a/web/core/components/analytics-v2/insight-table/root.tsx +++ b/web/core/components/analytics-v2/insight-table/root.tsx @@ -1,30 +1,61 @@ -import { ColumnDef } from "@tanstack/react-table"; +import { ColumnDef, Row, Table } from "@tanstack/react-table"; +import { download, generateCsv, mkConfig } from "export-to-csv"; +import { useParams } from "next/navigation"; +import { Download } from "lucide-react"; +import { useTranslation } from "@plane/i18n"; import { AnalyticsTableDataMap, TAnalyticsTabsV2Base } from "@plane/types"; +import { Button } from "@plane/ui"; import { DataTable } from "./data-table"; import { TableLoader } from "./loader"; - - interface InsightTableProps> { analyticsType: T; data?: AnalyticsTableDataMap[T][]; isLoading?: boolean; columns: ColumnDef[]; + columnsLabels?: Record; } export const InsightTable = >( props: InsightTableProps ): React.ReactElement => { - const { analyticsType, data, isLoading, columns } = props - + const { data, isLoading, columns, columnsLabels } = props + const params = useParams(); + const { t } = useTranslation(); + const workspaceSlug = params.workspaceSlug as string; if (isLoading) { return } + + const csvConfig = mkConfig({ + fieldSeparator: ',', + filename: `${workspaceSlug}-analytics`, + decimalSeparator: '.', + useKeysAsHeaders: true, + }) + + const exportCSV = (rows: Row[]) => { + const rowData: any = rows.map((row) => { + const { project_id, ...exportableData } = row.original + return Object.fromEntries(Object.entries(exportableData).map(([key, value]) => { + if (columnsLabels?.[key]) { + return [columnsLabels[key], value] + } + return [key, value] + })) + }) + const csv = generateCsv(csvConfig)(rowData) + download(csvConfig)(csv) + } + return (
{data ? ) => } /> :
No data
}
); diff --git a/web/core/components/analytics-v2/overview/project-insights.tsx b/web/core/components/analytics-v2/overview/project-insights.tsx index 2721fec72e5..376a1fde8c7 100644 --- a/web/core/components/analytics-v2/overview/project-insights.tsx +++ b/web/core/components/analytics-v2/overview/project-insights.tsx @@ -8,6 +8,7 @@ import { TChartData } from '@plane/types' // hooks import { useAnalyticsV2 } from '@/hooks/store/use-analytics-v2' // services +import { useResolvedAssetPath } from '@/hooks/use-resolved-asset-path' import { AnalyticsV2Service } from '@/services/analytics-v2.service' // plane web components import AnalyticsSectionWrapper from '../analytics-section-wrapper' @@ -28,6 +29,8 @@ const ProjectInsights = observer(() => { const { t } = useTranslation() const workspaceSlug = params.workspaceSlug as string; const { selectedDuration, selectedDurationLabel, selectedProjects } = useAnalyticsV2() + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics-v2/empty-chart-radar" }); + const { data: projectInsightsData, isLoading: isLoadingProjectInsight } = useSWR( `radar-chart-${workspaceSlug}`, @@ -45,7 +48,8 @@ const ProjectInsights = observer(() => { :
{projectInsightsData && = observer((props) => { - const { control, params, workspaceSlug, classNames } = props; - const { t } = useTranslation(); + const { control, params, classNames } = props; const xAxisOptions = useMemo(() => ANALYTICS_V2_X_AXIS_VALUES.filter((option) => option.value !== params.group_by), [params.group_by]); const groupByOptions = useMemo(() => ANALYTICS_V2_X_AXIS_VALUES.filter((option) => option.value !== params.x_axis), [params.x_axis]); diff --git a/web/core/components/analytics-v2/work-items/created-vs-resolved.tsx b/web/core/components/analytics-v2/work-items/created-vs-resolved.tsx index c4da8b4afe5..9150076cc45 100644 --- a/web/core/components/analytics-v2/work-items/created-vs-resolved.tsx +++ b/web/core/components/analytics-v2/work-items/created-vs-resolved.tsx @@ -11,6 +11,7 @@ import { renderFormattedDate } from '@plane/utils' // hooks import { useAnalyticsV2 } from '@/hooks/store/use-analytics-v2' // services +import { useResolvedAssetPath } from '@/hooks/use-resolved-asset-path' import { AnalyticsV2Service } from '@/services/analytics-v2.service' // plane web components import AnalyticsSectionWrapper from '../analytics-section-wrapper' @@ -25,6 +26,7 @@ const CreatedVsResolved = observer(() => { const params = useParams(); const { t } = useTranslation() const workspaceSlug = params.workspaceSlug as string; + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics-v2/empty-chart-area" }); const { data: createdVsResolvedData, isLoading: isCreatedVsResolvedLoading } = useSWR( `created-vs-resolved-${workspaceSlug}-${selectedDuration}`, () => analyticsV2Service.getAdvanceAnalyticsCharts(workspaceSlug, 'work-items', { @@ -100,6 +102,7 @@ const CreatedVsResolved = observer(() => { title={t('workspace_analytics.empty_state_v2.created_vs_resolved.title')} description={t('workspace_analytics.empty_state_v2.created_vs_resolved.description')} className='h-[350px]' + assetPath={resolvedPath} /> } diff --git a/web/core/components/analytics-v2/work-items/priority-chart.tsx b/web/core/components/analytics-v2/work-items/priority-chart.tsx index 68fbc9f3a02..e35a42f5e64 100644 --- a/web/core/components/analytics-v2/work-items/priority-chart.tsx +++ b/web/core/components/analytics-v2/work-items/priority-chart.tsx @@ -1,5 +1,6 @@ import { useMemo } from 'react' -import { ColumnDef } from '@tanstack/react-table' +import { ColumnDef, Row, Table } from '@tanstack/react-table' +import { mkConfig, generateCsv, download } from 'export-to-csv' import { observer } from 'mobx-react' import { useParams } from 'next/navigation' import { useTheme } from 'next-themes' @@ -12,12 +13,12 @@ import { BarChart } from '@plane/propel/charts/bar-chart' import { IChartResponseV2 } from '@plane/types' import { TBarItem, TChartDatum } from '@plane/types/src/charts' // plane web components -import { Button, setToast, TOAST_TYPE } from '@plane/ui' +import { Button } from '@plane/ui' import { CHART_COLOR_PALETTES, generateExtendedColors, parseChartData } from '@/components/chart/utils' // hooks import { useProjectState } from '@/hooks/store' import { useAnalyticsV2 } from '@/hooks/store/use-analytics-v2' -import { useWorkspaceIssueProperties } from '@/hooks/use-workspace-issue-properties' +import { useResolvedAssetPath } from '@/hooks/use-resolved-asset-path' import { AnalyticsV2Service } from '@/services/analytics-v2.service' import AnalyticsV2EmptyState from '../empty-state' import { DataTable } from '../insight-table/data-table' @@ -38,15 +39,14 @@ const analyticsV2Service = new AnalyticsV2Service() const PriorityChart = observer((props: Props) => { const { x_axis, y_axis, group_by } = props; const { t } = useTranslation() + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics-v2/empty-chart-bar" }); // store hooks const { selectedDuration, selectedProjects } = useAnalyticsV2() const { workspaceStates } = useProjectState() const { resolvedTheme } = useTheme(); // router const params = useParams(); - const workspaceSlug = params.workspaceSlug as string; - useWorkspaceIssueProperties(workspaceSlug); const { data: priorityChartData, isLoading: priorityChartLoading } = useSWR( `customized-insights-chart-${workspaceSlug}-${selectedDuration}-${props.x_axis}-${props.y_axis}-${props.group_by}`, @@ -56,24 +56,6 @@ const PriorityChart = observer((props: Props) => { ...props }), ) - const exportAnalytics = () => { - analyticsV2Service - .exportAnalytics(workspaceSlug, params) - .then((res) => { - setToast({ - type: TOAST_TYPE.SUCCESS, - title: "Success!", - message: res.message, - }); - }) - .catch(() => - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: "There was some error in exporting the analytics. Please try again.", - }) - ); - }; const parsedData = useMemo(() => parseChartData(priorityChartData, props.x_axis, props.group_by, props.x_axis_date_grouping) , [priorityChartData, props.x_axis, props.group_by, props.x_axis_date_grouping]) const chart_model = props.group_by ? EChartModels.STACKED : EChartModels.BASIC; @@ -157,6 +139,22 @@ const PriorityChart = observer((props: Props) => { cell: ({ row }) =>
{row.original[key]}
})), [parsedData.schema]); + const csvConfig = mkConfig({ + fieldSeparator: ',', + filename: `${workspaceSlug}-analytics`, + decimalSeparator: '.', + useKeysAsHeaders: true, + }) + + const exportCSV = (rows: Row[]) => { + const rowData = rows.map((row) => ({ + name: row.original.name, + count: row.original.count + })) + const csv = generateCsv(csvConfig)(rowData) + download(csvConfig)(csv) + } + const yAxisLabel = useMemo(() => ANALYTICS_V2_Y_AXIS_VALUES.find((item) => item.value === props.y_axis)?.label ?? props.y_axis, [props.y_axis]); const xAxisLabel = useMemo(() => ANALYTICS_V2_X_AXIS_VALUES.find((item) => item.value === props.x_axis)?.label ?? props.x_axis, [props.x_axis]); @@ -188,7 +186,7 @@ const PriorityChart = observer((props: Props) => { data={parsedData.data} columns={[...defaultColumns, ...columns]} searchPlaceholder={`${parsedData.data.length} ${yAxisLabel}`} - actions={} /> @@ -197,6 +195,7 @@ const PriorityChart = observer((props: Props) => { title={t('workspace_analytics.empty_state_v2.customized_insights.title')} description={t('workspace_analytics.empty_state_v2.customized_insights.description')} className='h-[350px]' + assetPath={resolvedPath} /> }
diff --git a/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx b/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx index 80d76a3c3bc..ef82d34ae26 100644 --- a/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx +++ b/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx @@ -14,6 +14,7 @@ import { useProject } from '@/hooks/store/use-project'; import { AnalyticsV2Service } from '@/services/analytics-v2.service'; // plane web components import { InsightTable } from '../insight-table'; +import { useTranslation } from '@plane/i18n'; const analyticsV2Service = new AnalyticsV2Service(); @@ -21,6 +22,7 @@ const WorkItemsInsightTable = observer(() => { // router const params = useParams(); const workspaceSlug = params.workspaceSlug as string; + const { t } = useTranslation(); // store hooks const { getProjectById } = useProject(); const { selectedDuration, selectedProjects } = useAnalyticsV2() @@ -30,10 +32,18 @@ const WorkItemsInsightTable = observer(() => { ...(selectedProjects ? { project_ids: selectedProjects.join(',') } : {}) })) // derived values + const columnsLabels: Record = { + backlog_work_items: t("workspace_projects.state.backlog"), + started_work_items: t("workspace_projects.state.started"), + un_started_work_items: t("workspace_projects.state.unstarted"), + completed_work_items: t("workspace_projects.state.completed"), + cancelled_work_items: t("workspace_projects.state.cancelled"), + project__name: t("common.project") + } const columns = useMemo(() => [ { accessorKey: "project__name", - header: () =>
Project
, + header: () =>
{columnsLabels["project__name"]}
, cell: ({ row }) => { const project = getProjectById(row.original.project_id); return
@@ -44,31 +54,31 @@ const WorkItemsInsightTable = observer(() => { )} {project?.name}
- } + }, }, { accessorKey: "backlog_work_items", - header: () =>
Backlog
, + header: () =>
{columnsLabels["backlog_work_items"]}
, cell: ({ row }) =>
{row.original.backlog_work_items}
}, { accessorKey: "started_work_items", - header: () =>
Started
, + header: () =>
{columnsLabels["started_work_items"]}
, cell: ({ row }) =>
{row.original.started_work_items}
}, { accessorKey: "un_started_work_items", - header: () =>
Unstarted
, + header: () =>
{columnsLabels["un_started_work_items"]}
, cell: ({ row }) =>
{row.original.un_started_work_items}
}, { accessorKey: "completed_work_items", - header: () =>
Completed
, + header: () =>
{columnsLabels["completed_work_items"]}
, cell: ({ row }) =>
{row.original.completed_work_items}
}, { accessorKey: "cancelled_work_items", - header: () =>
Cancelled
, + header: () =>
{columnsLabels["cancelled_work_items"]}
, cell: ({ row }) =>
{row.original.cancelled_work_items}
} ] as ColumnDef[], [getProjectById]) @@ -79,6 +89,7 @@ const WorkItemsInsightTable = observer(() => { data={workItemsData} isLoading={isLoading} columns={columns} + columnsLabels={columnsLabels} /> ) }) diff --git a/web/package.json b/web/package.json index 3656bb0431b..b5e6b80ebad 100644 --- a/web/package.json +++ b/web/package.json @@ -46,6 +46,7 @@ "comlink": "^4.4.1", "date-fns": "^4.1.0", "dotenv": "^16.0.3", + "export-to-csv": "^1.4.0", "isomorphic-dompurify": "^2.12.0", "lodash": "^4.17.21", "lucide-react": "^0.469.0", @@ -92,4 +93,4 @@ "prettier": "^3.2.5", "typescript": "5.3.3" } -} +} \ No newline at end of file diff --git a/web/public/empty-state/analytics-v2/empty-chart-area-dark.webp b/web/public/empty-state/analytics-v2/empty-chart-area-dark.webp new file mode 100644 index 0000000000000000000000000000000000000000..509f66ccc559cb781f306cde52e6abad566a26bb GIT binary patch literal 2720 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 literal 0 HcmV?d00001 diff --git a/web/public/empty-state/analytics-v2/empty-chart-bar-light.webp b/web/public/empty-state/analytics-v2/empty-chart-bar-light.webp new file mode 100644 index 0000000000000000000000000000000000000000..e6eeb09dbc25f79894682611568dca5b2bcb0035 GIT binary patch literal 512 zcmV+b0{{I|Nk&Ha0RRA3MM6+kP&gp$0RRBd8~~jGD!~B606vjGnMoz1rJ*Sp3s|rc z32AQKU*xHAr@)7Q0WklLlAmSpE=?!u4|6CE$86ll*~FlB%}UdR2;pP$WERiX+zP!b zYu921#r~*TpD3^slpS6q*Bfuoua)P6#2Gr}TjkTapRc$V@}TY2=b> z2QbTw539Kr;GtO1WV6UK0XFIkW>*I6_!XdrQK0ZkBM)wyI3ZUXF zTedAPT>t?7{#IhZHXfhr;xZ-jf}cw_%mp5a%D&2PeSa!lE?D?+AXJ;s$#7up6!zz@L*HB7%c&FhSp0E%2>gKM#s^0Ihda6 zZU$Cx+{lXWsdRazP~C$p@IRd+J7$=BtWt~p3MRyg;8dN12I=dgKuJERSr<9(7K^No zytht@G3(9<7_s}Ye{d5UF1$p5J%b$k(_@Q4zeg-R#xPra=*1#%s@C1C+Ki=`U;qI7 CC-ZUu literal 0 HcmV?d00001 diff --git a/web/public/empty-state/analytics-v2/empty-chart-radar-dark.webp b/web/public/empty-state/analytics-v2/empty-chart-radar-dark.webp new file mode 100644 index 0000000000000000000000000000000000000000..94e2f514b1572e08549a11dc058f3642dee6f9d2 GIT binary patch literal 3076 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 S0s?R4hyV_t`JfIk$N&I#U-Q5K literal 0 HcmV?d00001 diff --git a/web/public/empty-state/analytics-v2/empty-chart-radar-light.webp b/web/public/empty-state/analytics-v2/empty-chart-radar-light.webp new file mode 100644 index 0000000000000000000000000000000000000000..29820169bbc372b4ae0b61a43efc5a53cc41d594 GIT binary patch literal 716 zcmV;-0yF(mNk&G*0ssJ4MM6+kP&gpC0ssK;CjgxRD!~B606vjKno1?4C84Fb3_!3F z32AQKU{0vJjzWQJ?wR(`JP@-ppj^rWvGkVc{pk%d_kOab@vhON)!=FW{eUYH&6S1> zQ^>=-$5wy1=-vBLX&d*I8-NVIX0wI4p8Lp&!;(^JM;scebiV{+%G>(4dIt>;Sv*B1 z^hwYcCIPB?R;5hqo5u9T;6cKuU{q9q+wxp8zcNO_B;3k=@&nHsPCz8BnF{8u zYL~v75WK;E{pRCW-#cL%d2=$$hVV}|)lcf@Mj(koq=~AY%`*3Xvh#od{`kvqWr3wt z@Gy?xLXgsraqautwkMuR0|1;L6G^7`k?WMakkO8ANJ4CSL}PIP5oq}pH=35P!D%PB zf=3lU`bLt6T1TWrc3_o7Pydi<8&iRb2HQt{S66%r#iMNVPAQ3q2Clb;JpL@%f88~n z221$JGWJsgusv*h9uhu0BI%aLxyh)%mc#K0X4K_~jUX^ed8PhZKJA{#7Dz|C%)JMd z>Yvd-3i|s|)-5tygSRc8iWQAmWfyj0+~L=;YwBu1%)Xp21!st8RnSZ|Z%yB)#ln5j z4#Erm`-h`_i7nCDQ*AG#=hfc?A|0e$Q%Jf3Y5WseTN$CtR`u~SkqJ3S)ruZ|Zq19y zik>ZjIT3nMjJv91Q}$~%Ib?XXMq#p6l{?^2#>%fdsnV$|%>W&@XGTQmZ>Si7p&0=1 y9S7h=J!qCXnB&N1>!OShAQ8~*>mrQ`22aiMwOo=0>9Yrx5)iL_W&-Rm0001fdtw~` literal 0 HcmV?d00001 diff --git a/web/public/empty-state/analytics-v2/empty-grid-background-dark.webp b/web/public/empty-state/analytics-v2/empty-grid-background-dark.webp new file mode 100644 index 0000000000000000000000000000000000000000..5e420f2852caa36e45aa85094c4f569037a769e9 GIT binary patch literal 35070 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 literal 0 HcmV?d00001 diff --git a/web/public/empty-state/analytics-v2/empty-grid-background-light.webp b/web/public/empty-state/analytics-v2/empty-grid-background-light.webp new file mode 100644 index 0000000000000000000000000000000000000000..592a1281668b28a05bc2ba3f3c57f7ce575ae1d5 GIT binary patch literal 2576 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 literal 0 HcmV?d00001 diff --git a/web/public/empty-state/analytics-v2/empty-table-dark.webp b/web/public/empty-state/analytics-v2/empty-table-dark.webp new file mode 100644 index 0000000000000000000000000000000000000000..f5fcedd51013fd03aa14665e9e0f9c8fb2a819d9 GIT binary patch literal 3280 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 O64I+n2Ul-E@0b9}W>mod literal 0 HcmV?d00001 diff --git a/web/public/empty-state/analytics-v2/empty-table-light.webp b/web/public/empty-state/analytics-v2/empty-table-light.webp new file mode 100644 index 0000000000000000000000000000000000000000..c9e8fec9515186717b828acbde0ca388e849a5c2 GIT binary patch literal 862 zcmV-k1EKs8g*kE3qfy8>oQrA%$8)F6G&Fg`$oj?gV?c;l`xZD!&zNT z$hdGyyu&-Ty}vEn#H+kYn3+S4RSh1ixziaB_INI?A6!GHS;wFZPNuZ+L>TE>+&5j( zc3;U$e|c=FY>9H{KcJZD0>Vf5?Qv=O0PlOH0cxpfT)150%C|t;F^F_0+3M;}$IA&|lN`z4OT>2!G3@ygLY70*m({$~KRcKs@fVC7CSAN-&F+p!wRdsvb6BmD4le8gQ!%{PTP_3rArg{{{d6 z{{5us>Y#*C>??xWO-s|$PcAG33Dz5si2ieDQ9AH4V&B`E#DMOyt!i;ZWM zPHHUpn&)O#7h>>KYWi*_E{HG7YrI6+w@9oaJ;{O>ZwwdB*=49)O2{3SVB%x{h|%_B z_gchyrX*mh+Lp7!pwD+sRYbwuPj>xVfy5h?k9U|%5A|vque>dvfSKji&!4<;agEtA z(v@lr%C9v{ME$XIDipici8@tgzPKn}lV!?Zub5qs`Vk9S6Yw$P^~JE_Kti8nbX_!6 zZ<=UGkf_69UP`zAoxRjBqd+hdn%l?fl4dW}Y!6@_W|O!R z#k_E6X`Y1)k|U)c%XUZBz-+#UeMc0ye7AGpGF~yfTY_dc<|oAVSfjr@-U$-=`~kYz zEOx9h;g|tmAKmY(1iue6RrHvA$#`9n~ww4;vW1&z3gD^({{pdAWB^FIy oT#|Fs=VdS0yTn{dqC^2@3${CQ4hu8ebVB%{QRIhU3V;9r0Qne~5&!@I literal 0 HcmV?d00001 diff --git a/yarn.lock b/yarn.lock index 070a3c2640e..47ce955c875 100644 --- a/yarn.lock +++ b/yarn.lock @@ -257,7 +257,7 @@ "@babel/traverse" "^7.25.9" "@babel/types" "^7.25.9" -"@babel/helpers@7.26.10", "@babel/helpers@^7.26.7": +"@babel/helpers@^7.26.7": version "7.26.10" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.26.10.tgz#6baea3cd62ec2d0c1068778d63cb1314f6637384" integrity sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g== @@ -852,7 +852,7 @@ "@babel/plugin-transform-modules-commonjs" "^7.25.9" "@babel/plugin-transform-typescript" "^7.25.9" -"@babel/runtime@7.26.10", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.13", "@babel/runtime@^7.23.9", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.13", "@babel/runtime@^7.23.9", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": version "7.26.10" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.10.tgz#a07b4d8fa27af131a633d7b3524db803eb4764c2" integrity sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw== @@ -1124,126 +1124,251 @@ resolved "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz#5e13fac887f08c44f76b0ccaf3370eb00fec9bb6" integrity sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg== +"@esbuild/aix-ppc64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz#38848d3e25afe842a7943643cbcd387cc6e13461" + integrity sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA== + "@esbuild/aix-ppc64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz#499600c5e1757a524990d5d92601f0ac3ce87f64" integrity sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ== +"@esbuild/android-arm64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz#f592957ae8b5643129fa889c79e69cd8669bb894" + integrity sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg== + "@esbuild/android-arm64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz#b9b8231561a1dfb94eb31f4ee056b92a985c324f" integrity sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g== +"@esbuild/android-arm@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.24.2.tgz#72d8a2063aa630308af486a7e5cbcd1e134335b3" + integrity sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q== + "@esbuild/android-arm@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.0.tgz#ca6e7888942505f13e88ac9f5f7d2a72f9facd2b" integrity sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g== +"@esbuild/android-x64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.24.2.tgz#9a7713504d5f04792f33be9c197a882b2d88febb" + integrity sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw== + "@esbuild/android-x64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.0.tgz#e765ea753bac442dfc9cb53652ce8bd39d33e163" integrity sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg== +"@esbuild/darwin-arm64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz#02ae04ad8ebffd6e2ea096181b3366816b2b5936" + integrity sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA== + "@esbuild/darwin-arm64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz#fa394164b0d89d4fdc3a8a21989af70ef579fa2c" integrity sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw== +"@esbuild/darwin-x64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz#9ec312bc29c60e1b6cecadc82bd504d8adaa19e9" + integrity sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA== + "@esbuild/darwin-x64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz#91979d98d30ba6e7d69b22c617cc82bdad60e47a" integrity sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg== +"@esbuild/freebsd-arm64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz#5e82f44cb4906d6aebf24497d6a068cfc152fa00" + integrity sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg== + "@esbuild/freebsd-arm64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz#b97e97073310736b430a07b099d837084b85e9ce" integrity sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w== +"@esbuild/freebsd-x64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz#3fb1ce92f276168b75074b4e51aa0d8141ecce7f" + integrity sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q== + "@esbuild/freebsd-x64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz#f3b694d0da61d9910ec7deff794d444cfbf3b6e7" integrity sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A== +"@esbuild/linux-arm64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz#856b632d79eb80aec0864381efd29de8fd0b1f43" + integrity sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg== + "@esbuild/linux-arm64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz#f921f699f162f332036d5657cad9036f7a993f73" integrity sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg== +"@esbuild/linux-arm@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz#c846b4694dc5a75d1444f52257ccc5659021b736" + integrity sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA== + "@esbuild/linux-arm@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz#cc49305b3c6da317c900688995a4050e6cc91ca3" integrity sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg== +"@esbuild/linux-ia32@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz#f8a16615a78826ccbb6566fab9a9606cfd4a37d5" + integrity sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw== + "@esbuild/linux-ia32@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz#3e0736fcfab16cff042dec806247e2c76e109e19" integrity sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg== +"@esbuild/linux-loong64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz#1c451538c765bf14913512c76ed8a351e18b09fc" + integrity sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ== + "@esbuild/linux-loong64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz#ea2bf730883cddb9dfb85124232b5a875b8020c7" integrity sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw== +"@esbuild/linux-mips64el@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz#0846edeefbc3d8d50645c51869cc64401d9239cb" + integrity sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw== + "@esbuild/linux-mips64el@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz#4cababb14eede09248980a2d2d8b966464294ff1" integrity sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ== +"@esbuild/linux-ppc64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz#8e3fc54505671d193337a36dfd4c1a23b8a41412" + integrity sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw== + "@esbuild/linux-ppc64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz#8860a4609914c065373a77242e985179658e1951" integrity sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw== +"@esbuild/linux-riscv64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz#6a1e92096d5e68f7bb10a0d64bb5b6d1daf9a694" + integrity sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q== + "@esbuild/linux-riscv64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz#baf26e20bb2d38cfb86ee282dff840c04f4ed987" integrity sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA== +"@esbuild/linux-s390x@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz#ab18e56e66f7a3c49cb97d337cd0a6fea28a8577" + integrity sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw== + "@esbuild/linux-s390x@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz#8323afc0d6cb1b6dc6e9fd21efd9e1542c3640a4" integrity sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA== +"@esbuild/linux-x64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz#8140c9b40da634d380b0b29c837a0b4267aff38f" + integrity sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q== + "@esbuild/linux-x64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz#08fcf60cb400ed2382e9f8e0f5590bac8810469a" integrity sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw== +"@esbuild/netbsd-arm64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz#65f19161432bafb3981f5f20a7ff45abb2e708e6" + integrity sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw== + "@esbuild/netbsd-arm64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz#935c6c74e20f7224918fbe2e6c6fe865b6c6ea5b" integrity sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw== +"@esbuild/netbsd-x64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz#7a3a97d77abfd11765a72f1c6f9b18f5396bcc40" + integrity sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw== + "@esbuild/netbsd-x64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz#414677cef66d16c5a4d210751eb2881bb9c1b62b" integrity sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA== +"@esbuild/openbsd-arm64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz#58b00238dd8f123bfff68d3acc53a6ee369af89f" + integrity sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A== + "@esbuild/openbsd-arm64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz#8fd55a4d08d25cdc572844f13c88d678c84d13f7" integrity sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw== +"@esbuild/openbsd-x64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz#0ac843fda0feb85a93e288842936c21a00a8a205" + integrity sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA== + "@esbuild/openbsd-x64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz#0c48ddb1494bbc2d6bcbaa1429a7f465fa1dedde" integrity sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg== +"@esbuild/sunos-x64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz#8b7aa895e07828d36c422a4404cc2ecf27fb15c6" + integrity sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig== + "@esbuild/sunos-x64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz#86ff9075d77962b60dd26203d7352f92684c8c92" integrity sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg== +"@esbuild/win32-arm64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz#c023afb647cabf0c3ed13f0eddfc4f1d61c66a85" + integrity sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ== + "@esbuild/win32-arm64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz#849c62327c3229467f5b5cd681bf50588442e96c" integrity sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw== +"@esbuild/win32-ia32@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz#96c356132d2dda990098c8b8b951209c3cd743c2" + integrity sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA== + "@esbuild/win32-ia32@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz#f62eb480cd7cca088cb65bb46a6db25b725dc079" integrity sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA== +"@esbuild/win32-x64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz#34aa0b52d0fbb1a654b596acfa595f0c7b77a77b" + integrity sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg== + "@esbuild/win32-x64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz#c8e119a30a7c8d60b9d2e22d2073722dde3b710b" @@ -5930,7 +6055,38 @@ esbuild-register@^3.5.0: dependencies: debug "^4.3.4" -esbuild@0.25.0, "esbuild@^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0", esbuild@^0.25.0: +"esbuild@^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0": + version "0.24.2" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.24.2.tgz#b5b55bee7de017bff5fb8a4e3e44f2ebe2c3567d" + integrity sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA== + optionalDependencies: + "@esbuild/aix-ppc64" "0.24.2" + "@esbuild/android-arm" "0.24.2" + "@esbuild/android-arm64" "0.24.2" + "@esbuild/android-x64" "0.24.2" + "@esbuild/darwin-arm64" "0.24.2" + "@esbuild/darwin-x64" "0.24.2" + "@esbuild/freebsd-arm64" "0.24.2" + "@esbuild/freebsd-x64" "0.24.2" + "@esbuild/linux-arm" "0.24.2" + "@esbuild/linux-arm64" "0.24.2" + "@esbuild/linux-ia32" "0.24.2" + "@esbuild/linux-loong64" "0.24.2" + "@esbuild/linux-mips64el" "0.24.2" + "@esbuild/linux-ppc64" "0.24.2" + "@esbuild/linux-riscv64" "0.24.2" + "@esbuild/linux-s390x" "0.24.2" + "@esbuild/linux-x64" "0.24.2" + "@esbuild/netbsd-arm64" "0.24.2" + "@esbuild/netbsd-x64" "0.24.2" + "@esbuild/openbsd-arm64" "0.24.2" + "@esbuild/openbsd-x64" "0.24.2" + "@esbuild/sunos-x64" "0.24.2" + "@esbuild/win32-arm64" "0.24.2" + "@esbuild/win32-ia32" "0.24.2" + "@esbuild/win32-x64" "0.24.2" + +esbuild@^0.25.0: version "0.25.0" resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.0.tgz#0de1787a77206c5a79eeb634a623d39b5006ce92" integrity sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw== @@ -6266,6 +6422,11 @@ expand-template@^2.0.3: resolved "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== +export-to-csv@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/export-to-csv/-/export-to-csv-1.4.0.tgz#03fb42a4a4262cd03bde57a7b9bcad115149cf4b" + integrity sha512-6CX17Cu+rC2Fi2CyZ4CkgVG3hLl6BFsdAxfXiZkmDFIDY4mRx2y2spdeH6dqPHI9rP+AsHEfGeKz84Uuw7+Pmg== + express-ws@^5.0.2: version "5.0.2" resolved "https://registry.npmjs.org/express-ws/-/express-ws-5.0.2.tgz#5b02d41b937d05199c6c266d7cc931c823bda8eb" @@ -8391,7 +8552,7 @@ mz@^2.7.0: object-assign "^4.0.1" thenify-all "^1.0.0" -nanoid@3.3.8, nanoid@^3.3.6, nanoid@^3.3.8: +nanoid@^3.3.6, nanoid@^3.3.8: version "3.3.8" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf" integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w== From f822621e63691dbcde08302c91393d0df37d67c0 Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Thu, 8 May 2025 18:56:04 +0530 Subject: [PATCH 54/69] refactor --- packages/propel/src/charts/area-chart/root.tsx | 16 ++++++++++++---- .../propel/src/charts/scatter-chart/root.tsx | 14 ++++++++++---- packages/types/src/analytics-v2.d.ts | 3 ++- .../analytics-v2/insight-table/loader.tsx | 3 ++- .../work-items/created-vs-resolved.tsx | 6 +++--- .../analytics-v2/work-items/priority-chart.tsx | 15 ++++++++------- .../work-items/workitems-insight-table.tsx | 2 +- web/core/components/chart/utils.ts | 2 +- 8 files changed, 39 insertions(+), 22 deletions(-) diff --git a/packages/propel/src/charts/area-chart/root.tsx b/packages/propel/src/charts/area-chart/root.tsx index 6ddaa09813d..350f68b2215 100644 --- a/packages/propel/src/charts/area-chart/root.tsx +++ b/packages/propel/src/charts/area-chart/root.tsx @@ -29,14 +29,22 @@ export const AreaChart = React.memo((props: // states const [activeArea, setActiveArea] = useState(null); const [activeLegend, setActiveLegend] = useState(null); + // derived values - const itemKeys = useMemo(() => areas.map((area) => area.key), [areas]); - const itemLabels: Record = useMemo( - () => areas.reduce((acc, area) => ({ ...acc, [area.key]: area.label }), {}), + const itemKeys = useMemo( + () => Array.from(areas, area => area.key), + [areas] + ); + + const itemLabels = useMemo( + () => Object.fromEntries(areas.map(area => [area.key, area.label])), [areas] ); - const itemDotColors = useMemo(() => areas.reduce((acc, area) => ({ ...acc, [area.key]: area.fill }), {}), [areas]); + const itemDotColors = useMemo( + () => Object.fromEntries(areas.map(area => [area.key, area.fill])), + [areas] + ); const renderAreas = useMemo( () => areas.map((area) => ( diff --git a/packages/propel/src/charts/scatter-chart/root.tsx b/packages/propel/src/charts/scatter-chart/root.tsx index ea625db69ec..3a2cc3c5785 100644 --- a/packages/propel/src/charts/scatter-chart/root.tsx +++ b/packages/propel/src/charts/scatter-chart/root.tsx @@ -41,13 +41,19 @@ export const ScatterChart = React.memo((prop const [activePoint, setActivePoint] = useState(null); const [activeLegend, setActiveLegend] = useState(null); // derived values - const itemKeys = useMemo(() => scatterPoints.map((point) => point.key), [scatterPoints]); - const itemLabels: Record = useMemo( - () => scatterPoints.reduce((acc, point) => ({ ...acc, [point.key]: point.label }), {}), + const itemKeys = useMemo( + () => Array.from(scatterPoints, point => point.key), + [scatterPoints] + ); + const itemLabels = useMemo( + () => Object.fromEntries(scatterPoints.map(point => [point.key, point.label])), [scatterPoints] ); - const itemDotColors = useMemo(() => scatterPoints.reduce((acc, point) => ({ ...acc, [point.key]: point.fill }), {}), [scatterPoints]); + const itemDotColors = useMemo( + () => Object.fromEntries(scatterPoints.map(point => [point.key, point.fill])), + [scatterPoints] + ); const renderPoints = useMemo( () => scatterPoints.map((point) => ( diff --git a/packages/types/src/analytics-v2.d.ts b/packages/types/src/analytics-v2.d.ts index 4b7090ccb0e..2e1eda91887 100644 --- a/packages/types/src/analytics-v2.d.ts +++ b/packages/types/src/analytics-v2.d.ts @@ -1,4 +1,5 @@ -import { TBarItem } from "./charts"; +import { ChartXAxisProperty, ChartYAxisMetric } from "@plane/constants"; +import { TChartData } from "./charts"; export type TAnalyticsTabsV2Base = "overview" | "work-items" export type TAnalyticsGraphsV2Base = "projects" | "work-items" | "custom-work-items" diff --git a/web/core/components/analytics-v2/insight-table/loader.tsx b/web/core/components/analytics-v2/insight-table/loader.tsx index 599648783d7..ee5bbb8c5b2 100644 --- a/web/core/components/analytics-v2/insight-table/loader.tsx +++ b/web/core/components/analytics-v2/insight-table/loader.tsx @@ -1,4 +1,5 @@ import * as React from "react"; +import { ColumnDef } from "@tanstack/react-table"; import { Table, TableBody, @@ -10,7 +11,7 @@ import { import { Loader } from "@plane/ui"; interface TableSkeletonProps { - columns: any[]; + columns: ColumnDef[]; rows: number; } diff --git a/web/core/components/analytics-v2/work-items/created-vs-resolved.tsx b/web/core/components/analytics-v2/work-items/created-vs-resolved.tsx index 9150076cc45..fe439f48ef2 100644 --- a/web/core/components/analytics-v2/work-items/created-vs-resolved.tsx +++ b/web/core/components/analytics-v2/work-items/created-vs-resolved.tsx @@ -6,7 +6,7 @@ import useSWR from 'swr' // plane package imports import { useTranslation } from '@plane/i18n' import { AreaChart } from '@plane/propel/charts/area-chart' -import { IChartResponseV2 } from '@plane/types' +import { IChartResponseV2, TChartData } from '@plane/types' import { renderFormattedDate } from '@plane/utils' // hooks import { useAnalyticsV2 } from '@/hooks/store/use-analytics-v2' @@ -34,12 +34,12 @@ const CreatedVsResolved = observer(() => { project_ids: selectedProjects?.join(','), }), ) - const parsedData = useMemo(() => { + const parsedData: TChartData[] = useMemo(() => { if (!createdVsResolvedData?.data) return [] return createdVsResolvedData.data.map((datum) => ({ ...datum, [datum.key]: datum.count, - name: renderFormattedDate(datum.key) + name: renderFormattedDate(datum.key) ?? datum.key })) }, [createdVsResolvedData]) diff --git a/web/core/components/analytics-v2/work-items/priority-chart.tsx b/web/core/components/analytics-v2/work-items/priority-chart.tsx index e35a42f5e64..f4fadab2634 100644 --- a/web/core/components/analytics-v2/work-items/priority-chart.tsx +++ b/web/core/components/analytics-v2/work-items/priority-chart.tsx @@ -11,7 +11,7 @@ import { ANALYTICS_V2_X_AXIS_VALUES, ANALYTICS_V2_Y_AXIS_VALUES, ChartXAxisDateG import { useTranslation } from '@plane/i18n' import { BarChart } from '@plane/propel/charts/bar-chart' import { IChartResponseV2 } from '@plane/types' -import { TBarItem, TChartDatum } from '@plane/types/src/charts' +import { TBarItem, TChart, TChartData, TChartDatum } from '@plane/types/src/charts' // plane web components import { Button } from '@plane/ui' import { CHART_COLOR_PALETTES, generateExtendedColors, parseChartData } from '@/components/chart/utils' @@ -50,17 +50,18 @@ const PriorityChart = observer((props: Props) => { const { data: priorityChartData, isLoading: priorityChartLoading } = useSWR( `customized-insights-chart-${workspaceSlug}-${selectedDuration}-${props.x_axis}-${props.y_axis}-${props.group_by}`, - () => analyticsV2Service.getAdvanceAnalyticsCharts(workspaceSlug, 'custom-work-items', { + () => analyticsV2Service.getAdvanceAnalyticsCharts(workspaceSlug, 'custom-work-items', { date_filter: selectedDuration, project_ids: selectedProjects?.join(','), ...props }), ) - const parsedData = useMemo(() => parseChartData(priorityChartData, props.x_axis, props.group_by, props.x_axis_date_grouping) + const parsedData = useMemo(() => priorityChartData && parseChartData(priorityChartData, props.x_axis, props.group_by, props.x_axis_date_grouping) , [priorityChartData, props.x_axis, props.group_by, props.x_axis_date_grouping]) const chart_model = props.group_by ? EChartModels.STACKED : EChartModels.BASIC; const bars: TBarItem[] = useMemo(() => { + if (!parsedData) return []; let parsedBars: TBarItem[]; const schemaKeys = Object.keys(parsedData.schema); const baseColors = CHART_COLOR_PALETTES[0]?.[ @@ -119,7 +120,7 @@ const PriorityChart = observer((props: Props) => { parsedBars = []; } return parsedBars; - }, [chart_model, group_by, parsedData.data, parsedData.schema, resolvedTheme, workspaceStates, x_axis, y_axis]); + }, [chart_model, group_by, parsedData, resolvedTheme, workspaceStates, x_axis, y_axis]); const defaultColumns: ColumnDef[] = useMemo(() => [ { @@ -133,11 +134,11 @@ const PriorityChart = observer((props: Props) => { }, ], []); - const columns: ColumnDef[] = useMemo(() => Object.keys(parsedData.schema).map((key) => ({ + const columns: ColumnDef[] = useMemo(() => parsedData ? Object.keys(parsedData?.schema ?? {}).map((key) => ({ accessorKey: key, header: () =>
{parsedData.schema[key]}
, cell: ({ row }) =>
{row.original[key]}
- })), [parsedData.schema]); + })) : [], [parsedData]); const csvConfig = mkConfig({ fieldSeparator: ',', @@ -161,7 +162,7 @@ const PriorityChart = observer((props: Props) => { return (
{priorityChartLoading ? : - parsedData.data && parsedData.data.length > 0 ? + parsedData?.data && parsedData.data.length > 0 ? <> { const keys = Object.keys(datum); const missingKeys = allKeys.filter((key) => !keys.includes(key)); - const missingValues: Record = missingKeys.reduce((acc, key) => ({ ...acc, [key]: 0 }), {}); + const missingValues: Record = Object.fromEntries(missingKeys.map(key => [key, 0])); if (xAxisProperty) { // capitalize first letter if xAxisProperty is in TO_CAPITALIZE_PROPERTIES and no groupByProperty is set From b779e497b4df11caf50d3df2b66b5433a198232e Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Thu, 8 May 2025 19:25:06 +0530 Subject: [PATCH 55/69] code refactor --- packages/constants/src/analytics-v2/common.ts | 165 +++++++++--------- packages/types/src/analytics-v2.d.ts | 2 +- .../analytics-v2/insight-table/loader.tsx | 70 ++++---- .../overview/active-project-item.tsx | 2 + web/core/store/analytics-v2.store.ts | 7 - 5 files changed, 120 insertions(+), 126 deletions(-) diff --git a/packages/constants/src/analytics-v2/common.ts b/packages/constants/src/analytics-v2/common.ts index 9183322172e..24d0c46cd45 100644 --- a/packages/constants/src/analytics-v2/common.ts +++ b/packages/constants/src/analytics-v2/common.ts @@ -2,100 +2,99 @@ import { TAnalyticsTabsV2Base } from "@plane/types"; import { ChartXAxisProperty, ChartYAxisMetric } from "../chart"; export const insightsFields: Record = { - "overview": ["total_users", "total_admins", "total_members", "total_guests", "total_projects", "total_work_items", "total_cycles", "total_intake"], - "work-items": ["total_work_items", "started_work_items", "backlog_work_items", "un_started_work_items", "completed_work_items"], + "overview": ["total_users", "total_admins", "total_members", "total_guests", "total_projects", "total_work_items", "total_cycles", "total_intake"], + "work-items": ["total_work_items", "started_work_items", "backlog_work_items", "un_started_work_items", "completed_work_items"], } export const ANALYTICS_V2_DURATION_FILTER_OPTIONS = [ - { - name: "Yesterday", - value: "yesterday", - }, - { - name: "Last 7 days", - value: "last_7_days", - }, - { - name: "Last 30 days", - value: "last_30_days", - }, - { - name: "Last 3 months", - value: "last_3_months", - } + { + name: "Yesterday", + value: "yesterday", + }, + { + name: "Last 7 days", + value: "last_7_days", + }, + { + name: "Last 30 days", + value: "last_30_days", + }, + { + name: "Last 3 months", + value: "last_3_months", + } ]; -// TODO: add translations of the labels export const ANALYTICS_V2_X_AXIS_VALUES: { value: ChartXAxisProperty; label: string }[] = - [ - { - value: ChartXAxisProperty.STATES, - label: "State name", - }, - { - value: ChartXAxisProperty.STATE_GROUPS, - label: "State group", - }, - { - value: ChartXAxisProperty.PRIORITY, - label: "Priority", - }, - { - value: ChartXAxisProperty.LABELS, - label: "Label", - }, - { - value: ChartXAxisProperty.ASSIGNEES, - label: "Assignee", - }, - { - value: ChartXAxisProperty.ESTIMATE_POINTS, - label: "Estimate point", - }, - { - value: ChartXAxisProperty.CYCLES, - label: "Cycle", - }, - { - value: ChartXAxisProperty.MODULES, - label: "Module", - }, - { - value: ChartXAxisProperty.COMPLETED_AT, - label: "Completed date", - }, - { - value: ChartXAxisProperty.TARGET_DATE, - label: "Due date", - }, - { - value: ChartXAxisProperty.START_DATE, - label: "Start date", - }, - { - value: ChartXAxisProperty.CREATED_AT, - label: "Created date", - }, - ]; + [ + { + value: ChartXAxisProperty.STATES, + label: "State name", + }, + { + value: ChartXAxisProperty.STATE_GROUPS, + label: "State group", + }, + { + value: ChartXAxisProperty.PRIORITY, + label: "Priority", + }, + { + value: ChartXAxisProperty.LABELS, + label: "Label", + }, + { + value: ChartXAxisProperty.ASSIGNEES, + label: "Assignee", + }, + { + value: ChartXAxisProperty.ESTIMATE_POINTS, + label: "Estimate point", + }, + { + value: ChartXAxisProperty.CYCLES, + label: "Cycle", + }, + { + value: ChartXAxisProperty.MODULES, + label: "Module", + }, + { + value: ChartXAxisProperty.COMPLETED_AT, + label: "Completed date", + }, + { + value: ChartXAxisProperty.TARGET_DATE, + label: "Due date", + }, + { + value: ChartXAxisProperty.START_DATE, + label: "Start date", + }, + { + value: ChartXAxisProperty.CREATED_AT, + label: "Created date", + }, + ]; export const ANALYTICS_V2_Y_AXIS_VALUES: { value: ChartYAxisMetric; label: string }[] = - [ - { - value: ChartYAxisMetric.WORK_ITEM_COUNT, - label: "Work item", - }, - { - value: ChartYAxisMetric.ESTIMATE_POINT_COUNT, - label: "Estimate", - }, - ]; + [ + { + value: ChartYAxisMetric.WORK_ITEM_COUNT, + label: "Work item", + }, + { + value: ChartYAxisMetric.ESTIMATE_POINT_COUNT, + label: "Estimate", + }, + ]; export const ANALYTICS_V2_DATE_KEYS = [ - "completed_at", - "target_date", - "start_date", - "created_at", + "completed_at", + "target_date", + "start_date", + "created_at", ]; diff --git a/packages/types/src/analytics-v2.d.ts b/packages/types/src/analytics-v2.d.ts index 2e1eda91887..176cd1191cd 100644 --- a/packages/types/src/analytics-v2.d.ts +++ b/packages/types/src/analytics-v2.d.ts @@ -41,7 +41,7 @@ export interface WorkItemInsightColumns { started_work_items: number; } -type AnalyticsTableDataMap = { +export type AnalyticsTableDataMap = { "work-items": WorkItemInsightColumns, } diff --git a/web/core/components/analytics-v2/insight-table/loader.tsx b/web/core/components/analytics-v2/insight-table/loader.tsx index ee5bbb8c5b2..2e6620cbdcf 100644 --- a/web/core/components/analytics-v2/insight-table/loader.tsx +++ b/web/core/components/analytics-v2/insight-table/loader.tsx @@ -1,46 +1,46 @@ import * as React from "react"; import { ColumnDef } from "@tanstack/react-table"; import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, } from "@plane/propel/table"; import { Loader } from "@plane/ui"; interface TableSkeletonProps { - columns: ColumnDef[]; - rows: number; + columns: ColumnDef[]; + rows: number; } export const TableLoader: React.FC = ({ columns, rows }) => ( -
- - - { - columns.map((column, index) => ( - - {typeof column.header === 'string' ? column.header : ''} - - )) - } - - - - { - Array.from({ length: rows }).map((_, rowIndex) => ( - - { - columns.map((_, colIndex) => ( - - - - )) - } - - ))} - -
+ + + + { + columns.map((column, index) => ( + + {typeof column.header === 'string' ? column.header : ''} + + )) + } + + + + { + Array.from({ length: rows }).map((_, rowIndex) => ( + + { + columns.map((_, colIndex) => ( + + + + )) + } + + ))} + +
); diff --git a/web/core/components/analytics-v2/overview/active-project-item.tsx b/web/core/components/analytics-v2/overview/active-project-item.tsx index b1d042bf06b..e82f6e3b8ad 100644 --- a/web/core/components/analytics-v2/overview/active-project-item.tsx +++ b/web/core/components/analytics-v2/overview/active-project-item.tsx @@ -33,6 +33,8 @@ const ActiveProjectItem = (props: Props) => { const projectDetails = getProjectById(id); + if (!projectDetails) return null; + return (
diff --git a/web/core/store/analytics-v2.store.ts b/web/core/store/analytics-v2.store.ts index da540eda448..8684a01eeaf 100644 --- a/web/core/store/analytics-v2.store.ts +++ b/web/core/store/analytics-v2.store.ts @@ -33,7 +33,6 @@ export class AnalyticsStoreV2 implements IAnalyticsStoreV2 { selectedDuration: observable, selectedProjects: observable, // computed - selectedProjectLabel: computed, selectedDurationLabel: computed, // actions updateSelectedProjects: action, @@ -41,11 +40,6 @@ export class AnalyticsStoreV2 implements IAnalyticsStoreV2 { }) } - get selectedProjectLabel() { // TODO: get the project label from the project id - return "All Projects" - } - - get selectedDurationLabel() { return ANALYTICS_V2_DURATION_FILTER_OPTIONS.find(item => item.value === this.selectedDuration)?.name ?? null } @@ -63,7 +57,6 @@ export class AnalyticsStoreV2 implements IAnalyticsStoreV2 { } updateSelectedDuration = (duration: DurationType) => { - const initialState = this.selectedDuration; try { runInAction(() => { this.selectedDuration = duration; From 3c1e891632db58d9935b2eea971a29bdfcb50612 Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Fri, 9 May 2025 14:09:28 +0530 Subject: [PATCH 56/69] updated loaders, moved color pallete to contants, added nullish collasece operator in neccessary places --- packages/constants/src/chart.ts | 94 ++++++++++++++++++ .../propel/src/charts/area-chart/root.tsx | 4 +- .../propel/src/charts/radar-chart/root.tsx | 10 +- web/ce/components/analytics-v2/tabs.ts | 13 ++- web/core/components/analytics-v2/loaders.tsx | 21 ++-- .../work-items/priority-chart.tsx | 4 +- web/core/components/chart/utils.ts | 95 +------------------ 7 files changed, 128 insertions(+), 113 deletions(-) diff --git a/packages/constants/src/chart.ts b/packages/constants/src/chart.ts index 156a7c7ca6f..be736d80749 100644 --- a/packages/constants/src/chart.ts +++ b/packages/constants/src/chart.ts @@ -1,3 +1,5 @@ +import { TChartColorScheme } from "@plane/types"; + export const LABEL_CLASSNAME = "uppercase text-custom-text-300/60 text-sm tracking-wide"; export const AXIS_LABEL_CLASSNAME = "uppercase text-custom-text-300/60 text-sm tracking-wide"; @@ -61,3 +63,95 @@ export enum EChartModels { COMPARISON = "COMPARISON", PROGRESS = "PROGRESS", } + +export const CHART_COLOR_PALETTES: { + key: TChartColorScheme; + i18n_label: string; + light: string[]; + dark: string[]; +}[] = [ + { + key: "modern", + i18n_label: "dashboards.widget.color_palettes.modern", + light: [ + "#6172E8", + "#8B6EDB", + "#E05F99", + "#29A383", + "#CB8A37", + "#3AA7C1", + "#F1B24A", + "#E84855", + "#50C799", + "#B35F9E", + ], + dark: [ + "#6B7CDE", + "#8E9DE6", + "#D45D9E", + "#2EAF85", + "#D4A246", + "#29A7C1", + "#B89F6A", + "#D15D64", + "#4ED079", + "#A169A4", + ], + }, + { + key: "horizon", + i18n_label: "dashboards.widget.color_palettes.horizon", + light: [ + "#E76E50", + "#289D90", + "#F3A362", + "#E9C368", + "#264753", + "#8A6FA0", + "#5B9EE5", + "#7CC474", + "#BA7DB5", + "#CF8640", + ], + dark: [ + "#E05A3A", + "#1D8A7E", + "#D98B4D", + "#D1AC50", + "#3A6B7C", + "#7D6297", + "#4D8ACD", + "#569C64", + "#C16A8C", + "#B77436", + ], + }, + { + key: "earthen", + i18n_label: "dashboards.widget.color_palettes.earthen", + light: [ + "#386641", + "#6A994E", + "#A7C957", + "#E97F4E", + "#BC4749", + "#9E2A2B", + "#80CED1", + "#5C3E79", + "#526EAB", + "#6B5B95", + ], + dark: [ + "#497752", + "#7BAA5F", + "#B8DA68", + "#FA905F", + "#CD585A", + "#AF3B3C", + "#91DFE2", + "#6D4F8A", + "#637FBC", + "#7C6CA6", + ], + }, + ]; diff --git a/packages/propel/src/charts/area-chart/root.tsx b/packages/propel/src/charts/area-chart/root.tsx index 350f68b2215..02e2b070719 100644 --- a/packages/propel/src/charts/area-chart/root.tsx +++ b/packages/propel/src/charts/area-chart/root.tsx @@ -135,8 +135,8 @@ export const AreaChart = React.memo((props: value: yAxis.label, angle: -90, position: "bottom", - offset: yAxis.offset || -24, - dx: yAxis.dx || -16, + offset: yAxis.offset ?? -24, + dx: yAxis.dx ?? -16, className: AXIS_LABEL_CLASSNAME, } } diff --git a/packages/propel/src/charts/radar-chart/root.tsx b/packages/propel/src/charts/radar-chart/root.tsx index 56ef7f44c40..4036e08cf10 100644 --- a/packages/propel/src/charts/radar-chart/root.tsx +++ b/packages/propel/src/charts/radar-chart/root.tsx @@ -1,15 +1,15 @@ -import { TRadarChartProps } from '@plane/types'; import { useMemo, useState } from 'react' -import { PolarGrid, Radar, RadarChart as CoreRadarChart, ResponsiveContainer, PolarAngleAxis, RadarProps, Tooltip, Legend } from 'recharts'; -import { CustomTooltip } from '../components/tooltip'; +import { PolarGrid, Radar, RadarChart as CoreRadarChart, ResponsiveContainer, PolarAngleAxis, Tooltip, Legend } from 'recharts'; +import { TRadarChartProps } from '@plane/types'; import { getLegendProps } from '../components/legend'; import { CustomRadarAxisTick } from '../components/tick'; +import { CustomTooltip } from '../components/tooltip'; const RadarChart = (props: TRadarChartProps) => { - const { data, radars, dataKey, margin, showTooltip, legend, className, angleAxis } = props; + const { data, radars, margin, showTooltip, legend, className, angleAxis } = props; // states - const [activeIndex, setActiveIndex] = useState(null); + const [, setActiveIndex] = useState(null); const [activeLegend, setActiveLegend] = useState(null); const itemKeys = useMemo(() => radars.map((radar) => radar.key), [radars]); diff --git a/web/ce/components/analytics-v2/tabs.ts b/web/ce/components/analytics-v2/tabs.ts index 154029318f7..8390601eba3 100644 --- a/web/ce/components/analytics-v2/tabs.ts +++ b/web/ce/components/analytics-v2/tabs.ts @@ -1,6 +1,11 @@ +import { TAnalyticsTabsV2Base } from "@plane/types"; import { Overview } from "@/components/analytics-v2/overview"; import { WorkItems } from "@/components/analytics-v2/work-items"; -export const ANALYTICS_TABS = [ - { key: "overview", i18nKey: "common.overview", content: Overview }, - { key: "workitems", i18nKey: "sidebar.work_items", content: WorkItems }, -]; +export const ANALYTICS_TABS: { + key: TAnalyticsTabsV2Base; + i18nKey: string; + content: React.FC; +}[] = [ + { key: "overview", i18nKey: "common.overview", content: Overview }, + { key: "work-items", i18nKey: "sidebar.work_items", content: WorkItems }, + ]; diff --git a/web/core/components/analytics-v2/loaders.tsx b/web/core/components/analytics-v2/loaders.tsx index ebd51225b61..406aaf39410 100644 --- a/web/core/components/analytics-v2/loaders.tsx +++ b/web/core/components/analytics-v2/loaders.tsx @@ -1,16 +1,25 @@ +import { Loader } from "@plane/ui"; + export const ProjectInsightsLoader = () => ( -
-
+
+ + +
-
-
+ + + + + +
); - export const ChartLoader = () => ( -
+ + + ); diff --git a/web/core/components/analytics-v2/work-items/priority-chart.tsx b/web/core/components/analytics-v2/work-items/priority-chart.tsx index f4fadab2634..a3413366774 100644 --- a/web/core/components/analytics-v2/work-items/priority-chart.tsx +++ b/web/core/components/analytics-v2/work-items/priority-chart.tsx @@ -7,14 +7,14 @@ import { useTheme } from 'next-themes' import useSWR from 'swr' // plane package imports import { Download } from 'lucide-react' -import { ANALYTICS_V2_X_AXIS_VALUES, ANALYTICS_V2_Y_AXIS_VALUES, ChartXAxisDateGrouping, ChartXAxisProperty, ChartYAxisMetric, EChartModels } from '@plane/constants' +import { ANALYTICS_V2_X_AXIS_VALUES, ANALYTICS_V2_Y_AXIS_VALUES, CHART_COLOR_PALETTES, ChartXAxisDateGrouping, ChartXAxisProperty, ChartYAxisMetric, EChartModels } from '@plane/constants' import { useTranslation } from '@plane/i18n' import { BarChart } from '@plane/propel/charts/bar-chart' import { IChartResponseV2 } from '@plane/types' import { TBarItem, TChart, TChartData, TChartDatum } from '@plane/types/src/charts' // plane web components import { Button } from '@plane/ui' -import { CHART_COLOR_PALETTES, generateExtendedColors, parseChartData } from '@/components/chart/utils' +import { generateExtendedColors, parseChartData } from '@/components/chart/utils' // hooks import { useProjectState } from '@/hooks/store' import { useAnalyticsV2 } from '@/hooks/store/use-analytics-v2' diff --git a/web/core/components/chart/utils.ts b/web/core/components/chart/utils.ts index a5e4b862e0c..9e1d779bf8a 100644 --- a/web/core/components/chart/utils.ts +++ b/web/core/components/chart/utils.ts @@ -1,102 +1,9 @@ import { getWeekOfMonth, isValid } from "date-fns"; import { CHART_X_AXIS_DATE_PROPERTIES, ChartXAxisDateGrouping, ChartXAxisProperty, TO_CAPITALIZE_PROPERTIES } from "@plane/constants"; -import { TChart, TChartColorScheme, TChartDatum } from "@plane/types"; +import { TChart, TChartDatum } from "@plane/types"; import { capitalizeFirstLetter, hexToHsl, hslToHex, renderFormattedDate } from "@plane/utils"; import { renderFormattedDateWithoutYear } from "@/helpers/date-time.helper"; - -export const CHART_COLOR_PALETTES: { - key: TChartColorScheme; - i18n_label: string; - light: string[]; - dark: string[]; -}[] = [ - { - key: "modern", - i18n_label: "dashboards.widget.color_palettes.modern", - light: [ - "#6172E8", - "#8B6EDB", - "#E05F99", - "#29A383", - "#CB8A37", - "#3AA7C1", - "#F1B24A", - "#E84855", - "#50C799", - "#B35F9E", - ], - dark: [ - "#6B7CDE", - "#8E9DE6", - "#D45D9E", - "#2EAF85", - "#D4A246", - "#29A7C1", - "#B89F6A", - "#D15D64", - "#4ED079", - "#A169A4", - ], - }, - { - key: "horizon", - i18n_label: "dashboards.widget.color_palettes.horizon", - light: [ - "#E76E50", - "#289D90", - "#F3A362", - "#E9C368", - "#264753", - "#8A6FA0", - "#5B9EE5", - "#7CC474", - "#BA7DB5", - "#CF8640", - ], - dark: [ - "#E05A3A", - "#1D8A7E", - "#D98B4D", - "#D1AC50", - "#3A6B7C", - "#7D6297", - "#4D8ACD", - "#569C64", - "#C16A8C", - "#B77436", - ], - }, - { - key: "earthen", - i18n_label: "dashboards.widget.color_palettes.earthen", - light: [ - "#386641", - "#6A994E", - "#A7C957", - "#E97F4E", - "#BC4749", - "#9E2A2B", - "#80CED1", - "#5C3E79", - "#526EAB", - "#6B5B95", - ], - dark: [ - "#497752", - "#7BAA5F", - "#B8DA68", - "#FA905F", - "#CD585A", - "#AF3B3C", - "#91DFE2", - "#6D4F8A", - "#637FBC", - "#7C6CA6", - ], - }, - ]; - const getDateGroupingName = (date: string, dateGrouping: ChartXAxisDateGrouping): string => { if (!date || ["none", "null"].includes(date.toLowerCase())) return "None"; From 458be60db5365b767a875b56cf84bc9b396e7de5 Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Fri, 9 May 2025 14:10:55 +0530 Subject: [PATCH 57/69] removed uneccessary cn --- web/core/components/analytics-v2/analytics-section-wrapper.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/core/components/analytics-v2/analytics-section-wrapper.tsx b/web/core/components/analytics-v2/analytics-section-wrapper.tsx index 6b4dfa6a770..85057dccf40 100644 --- a/web/core/components/analytics-v2/analytics-section-wrapper.tsx +++ b/web/core/components/analytics-v2/analytics-section-wrapper.tsx @@ -12,7 +12,7 @@ type Props = { const AnalyticsSectionWrapper: React.FC = (props) => { const { title, children, className, subtitle, actions, headerClassName } = props return ( -
+
{title &&

{title}

From db3f85d426aead202c4e87b71dc466def4c9dc5b Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Fri, 9 May 2025 14:33:12 +0530 Subject: [PATCH 58/69] fixed formatting issues --- packages/constants/src/analytics-v2/common.ts | 183 ++++++------ packages/constants/src/workspace.ts | 48 ++-- .../(projects)/analytics-v2/page.tsx | 20 +- .../analytics-v2/analytics-filter-actions.tsx | 47 ++-- .../analytics-section-wrapper.tsx | 36 +-- .../analytics-v2/analytics-wrapper.tsx | 18 +- .../components/analytics-v2/empty-state.tsx | 46 +-- web/core/components/analytics-v2/index.ts | 2 +- .../components/analytics-v2/insight-card.tsx | 16 +- .../analytics-v2/insight-table/data-table.tsx | 109 +++---- .../analytics-v2/insight-table/index.ts | 2 +- .../analytics-v2/insight-table/loader.tsx | 62 ++-- .../analytics-v2/insight-table/root.tsx | 99 ++++--- web/core/components/analytics-v2/loaders.tsx | 4 +- .../overview/active-project-item.tsx | 98 +++---- .../analytics-v2/overview/active-projects.tsx | 70 ++--- .../components/analytics-v2/overview/index.ts | 2 +- .../overview/project-insights.tsx | 152 +++++----- .../components/analytics-v2/overview/root.tsx | 25 +- .../analytics-v2/select/analytics-params.tsx | 43 +-- .../analytics-v2/select/duration.tsx | 50 ++-- .../analytics-v2/select/project.tsx | 19 +- .../analytics-v2/select/select-x-axis.tsx | 7 +- .../analytics-v2/select/select-y-axis.tsx | 14 +- .../analytics-v2/total-insights.tsx | 86 +++--- .../components/analytics-v2/trend-piece.tsx | 81 +++--- .../work-items/created-vs-resolved.tsx | 191 +++++++------ .../work-items/customized-insights.tsx | 46 +-- .../analytics-v2/work-items/index.ts | 2 +- .../analytics-v2/work-items/modal/content.tsx | 21 +- .../analytics-v2/work-items/modal/header.tsx | 2 +- .../analytics-v2/work-items/modal/index.tsx | 16 +- .../work-items/priority-chart.tsx | 265 ++++++++++-------- .../analytics-v2/work-items/root.tsx | 24 +- .../analytics-v2/work-items/utils.ts | 36 ++- .../work-items/workitems-insight-table.tsx | 157 ++++++----- 36 files changed, 1057 insertions(+), 1042 deletions(-) diff --git a/packages/constants/src/analytics-v2/common.ts b/packages/constants/src/analytics-v2/common.ts index 24d0c46cd45..6eab3ab2966 100644 --- a/packages/constants/src/analytics-v2/common.ts +++ b/packages/constants/src/analytics-v2/common.ts @@ -2,99 +2,104 @@ import { TAnalyticsTabsV2Base } from "@plane/types"; import { ChartXAxisProperty, ChartYAxisMetric } from "../chart"; export const insightsFields: Record = { - "overview": ["total_users", "total_admins", "total_members", "total_guests", "total_projects", "total_work_items", "total_cycles", "total_intake"], - "work-items": ["total_work_items", "started_work_items", "backlog_work_items", "un_started_work_items", "completed_work_items"], -} - + overview: [ + "total_users", + "total_admins", + "total_members", + "total_guests", + "total_projects", + "total_work_items", + "total_cycles", + "total_intake", + ], + "work-items": [ + "total_work_items", + "started_work_items", + "backlog_work_items", + "un_started_work_items", + "completed_work_items", + ], +}; export const ANALYTICS_V2_DURATION_FILTER_OPTIONS = [ - { - name: "Yesterday", - value: "yesterday", - }, - { - name: "Last 7 days", - value: "last_7_days", - }, - { - name: "Last 30 days", - value: "last_30_days", - }, - { - name: "Last 3 months", - value: "last_3_months", - } + { + name: "Yesterday", + value: "yesterday", + }, + { + name: "Last 7 days", + value: "last_7_days", + }, + { + name: "Last 30 days", + value: "last_30_days", + }, + { + name: "Last 3 months", + value: "last_3_months", + }, ]; -export const ANALYTICS_V2_X_AXIS_VALUES: { value: ChartXAxisProperty; label: string }[] = - [ - { - value: ChartXAxisProperty.STATES, - label: "State name", - }, - { - value: ChartXAxisProperty.STATE_GROUPS, - label: "State group", - }, - { - value: ChartXAxisProperty.PRIORITY, - label: "Priority", - }, - { - value: ChartXAxisProperty.LABELS, - label: "Label", - }, - { - value: ChartXAxisProperty.ASSIGNEES, - label: "Assignee", - }, - { - value: ChartXAxisProperty.ESTIMATE_POINTS, - label: "Estimate point", - }, - { - value: ChartXAxisProperty.CYCLES, - label: "Cycle", - }, - { - value: ChartXAxisProperty.MODULES, - label: "Module", - }, - { - value: ChartXAxisProperty.COMPLETED_AT, - label: "Completed date", - }, - { - value: ChartXAxisProperty.TARGET_DATE, - label: "Due date", - }, - { - value: ChartXAxisProperty.START_DATE, - label: "Start date", - }, - { - value: ChartXAxisProperty.CREATED_AT, - label: "Created date", - }, - ]; - -export const ANALYTICS_V2_Y_AXIS_VALUES: { value: ChartYAxisMetric; label: string }[] = - [ - { - value: ChartYAxisMetric.WORK_ITEM_COUNT, - label: "Work item", - }, - { - value: ChartYAxisMetric.ESTIMATE_POINT_COUNT, - label: "Estimate", - }, - ]; - -export const ANALYTICS_V2_DATE_KEYS = [ - "completed_at", - "target_date", - "start_date", - "created_at", +export const ANALYTICS_V2_X_AXIS_VALUES: { value: ChartXAxisProperty; label: string }[] = [ + { + value: ChartXAxisProperty.STATES, + label: "State name", + }, + { + value: ChartXAxisProperty.STATE_GROUPS, + label: "State group", + }, + { + value: ChartXAxisProperty.PRIORITY, + label: "Priority", + }, + { + value: ChartXAxisProperty.LABELS, + label: "Label", + }, + { + value: ChartXAxisProperty.ASSIGNEES, + label: "Assignee", + }, + { + value: ChartXAxisProperty.ESTIMATE_POINTS, + label: "Estimate point", + }, + { + value: ChartXAxisProperty.CYCLES, + label: "Cycle", + }, + { + value: ChartXAxisProperty.MODULES, + label: "Module", + }, + { + value: ChartXAxisProperty.COMPLETED_AT, + label: "Completed date", + }, + { + value: ChartXAxisProperty.TARGET_DATE, + label: "Due date", + }, + { + value: ChartXAxisProperty.START_DATE, + label: "Start date", + }, + { + value: ChartXAxisProperty.CREATED_AT, + label: "Created date", + }, ]; +export const ANALYTICS_V2_Y_AXIS_VALUES: { value: ChartYAxisMetric; label: string }[] = [ + { + value: ChartYAxisMetric.WORK_ITEM_COUNT, + label: "Work item", + }, + { + value: ChartYAxisMetric.ESTIMATE_POINT_COUNT, + label: "Estimate", + }, +]; +export const ANALYTICS_V2_DATE_KEYS = ["completed_at", "target_date", "start_date", "created_at"]; diff --git a/packages/constants/src/workspace.ts b/packages/constants/src/workspace.ts index 257a1247b87..f1e87507878 100644 --- a/packages/constants/src/workspace.ts +++ b/packages/constants/src/workspace.ts @@ -134,13 +134,13 @@ export const WORKSPACE_SETTINGS_LINKS: { access: EUserWorkspaceRoles[]; highlight: (pathname: string, baseUrl: string) => boolean; }[] = [ - WORKSPACE_SETTINGS["general"], - WORKSPACE_SETTINGS["members"], - WORKSPACE_SETTINGS["billing-and-plans"], - WORKSPACE_SETTINGS["export"], - WORKSPACE_SETTINGS["webhooks"], - WORKSPACE_SETTINGS["api-tokens"], - ]; + WORKSPACE_SETTINGS["general"], + WORKSPACE_SETTINGS["members"], + WORKSPACE_SETTINGS["billing-and-plans"], + WORKSPACE_SETTINGS["export"], + WORKSPACE_SETTINGS["webhooks"], + WORKSPACE_SETTINGS["api-tokens"], +]; export const ROLE = { [EUserWorkspaceRoles.GUEST]: "Guest", @@ -237,23 +237,23 @@ export const DEFAULT_GLOBAL_VIEWS_LIST: { key: TStaticViewTypes; i18n_label: string; }[] = [ - { - key: "all-issues", - i18n_label: "default_global_view.all_issues", - }, - { - key: "assigned", - i18n_label: "default_global_view.assigned", - }, - { - key: "created", - i18n_label: "default_global_view.created", - }, - { - key: "subscribed", - i18n_label: "default_global_view.subscribed", - }, - ]; + { + key: "all-issues", + i18n_label: "default_global_view.all_issues", + }, + { + key: "assigned", + i18n_label: "default_global_view.assigned", + }, + { + key: "created", + i18n_label: "default_global_view.created", + }, + { + key: "subscribed", + i18n_label: "default_global_view.subscribed", + }, +]; export interface IWorkspaceSidebarNavigationItem { key: string; diff --git a/web/app/[workspaceSlug]/(projects)/analytics-v2/page.tsx b/web/app/[workspaceSlug]/(projects)/analytics-v2/page.tsx index 5e89d17b60e..b4972363355 100644 --- a/web/app/[workspaceSlug]/(projects)/analytics-v2/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/analytics-v2/page.tsx @@ -40,14 +40,18 @@ const AnalyticsPage = observer(() => { EUserPermissionsLevel.WORKSPACE ); - const tabs = useMemo(() => ANALYTICS_TABS.map((tab) => ({ - key: tab.key, - label: t(tab.i18nKey), - content: , - onClick: () => { - router.push(`?tab=${tab.key}`); - } - })), [router, t]); + const tabs = useMemo( + () => + ANALYTICS_TABS.map((tab) => ({ + key: tab.key, + label: t(tab.i18nKey), + content: , + onClick: () => { + router.push(`?tab=${tab.key}`); + }, + })), + [router, t] + ); const defaultTab = searchParams.get("tab") || ANALYTICS_TABS[0].key; return ( diff --git a/web/core/components/analytics-v2/analytics-filter-actions.tsx b/web/core/components/analytics-v2/analytics-filter-actions.tsx index bcc5955e762..b9b69bed912 100644 --- a/web/core/components/analytics-v2/analytics-filter-actions.tsx +++ b/web/core/components/analytics-v2/analytics-filter-actions.tsx @@ -1,4 +1,3 @@ - // plane web components import { observer } from "mobx-react-lite"; // hooks @@ -9,27 +8,27 @@ import DurationDropdown from "./select/duration"; import { ProjectSelect } from "./select/project"; const AnalyticsFilterActions = observer(() => { - const { selectedProjects, selectedDuration, updateSelectedProjects, updateSelectedDuration } = useAnalyticsV2() - const { workspaceProjectIds } = useProject() - return ( -
- { - updateSelectedProjects(val ?? []) - }} - projectIds={workspaceProjectIds} - /> - { - updateSelectedDuration(val) - }} - dropdownArrow - /> -
- ) -}) + const { selectedProjects, selectedDuration, updateSelectedProjects, updateSelectedDuration } = useAnalyticsV2(); + const { workspaceProjectIds } = useProject(); + return ( +
+ { + updateSelectedProjects(val ?? []); + }} + projectIds={workspaceProjectIds} + /> + { + updateSelectedDuration(val); + }} + dropdownArrow + /> +
+ ); +}); -export default AnalyticsFilterActions \ No newline at end of file +export default AnalyticsFilterActions; diff --git a/web/core/components/analytics-v2/analytics-section-wrapper.tsx b/web/core/components/analytics-v2/analytics-section-wrapper.tsx index 85057dccf40..deb691644c8 100644 --- a/web/core/components/analytics-v2/analytics-section-wrapper.tsx +++ b/web/core/components/analytics-v2/analytics-section-wrapper.tsx @@ -1,28 +1,30 @@ -import { cn } from '@plane/utils' +import { cn } from "@plane/utils"; type Props = { - title?: string, - children: React.ReactNode - className?: string - subtitle?: string | null, - actions?: React.ReactNode - headerClassName?: string -} + title?: string; + children: React.ReactNode; + className?: string; + subtitle?: string | null; + actions?: React.ReactNode; + headerClassName?: string; +}; const AnalyticsSectionWrapper: React.FC = (props) => { - const { title, children, className, subtitle, actions, headerClassName } = props + const { title, children, className, subtitle, actions, headerClassName } = props; return (
-
- {title &&
-

{title}

- {subtitle &&

• {subtitle}

} -
} +
+ {title && ( +
+

{title}

+ {subtitle &&

• {subtitle}

} +
+ )} {actions}
{children}
- ) -} + ); +}; -export default AnalyticsSectionWrapper \ No newline at end of file +export default AnalyticsSectionWrapper; diff --git a/web/core/components/analytics-v2/analytics-wrapper.tsx b/web/core/components/analytics-v2/analytics-wrapper.tsx index 09ce1b1932e..d6193a2b324 100644 --- a/web/core/components/analytics-v2/analytics-wrapper.tsx +++ b/web/core/components/analytics-v2/analytics-wrapper.tsx @@ -1,22 +1,22 @@ -import React from 'react' +import React from "react"; // plane package imports -import { cn } from '@plane/utils'; +import { cn } from "@plane/utils"; type Props = { title: string; children: React.ReactNode; - className?: string -} + className?: string; +}; const AnalyticsWrapper: React.FC = (props) => { const { title, children, className } = props; return ( -
-

{title}

+
+

{title}

{children}
- ) -} + ); +}; -export default AnalyticsWrapper; \ No newline at end of file +export default AnalyticsWrapper; diff --git a/web/core/components/analytics-v2/empty-state.tsx b/web/core/components/analytics-v2/empty-state.tsx index ac8d0e17748..1a1ee86e821 100644 --- a/web/core/components/analytics-v2/empty-state.tsx +++ b/web/core/components/analytics-v2/empty-state.tsx @@ -1,42 +1,30 @@ -import React from 'react' -import Image from 'next/image'; +import React from "react"; +import Image from "next/image"; // plane package imports -import { cn } from '@plane/utils' -import { useResolvedAssetPath } from '@/hooks/use-resolved-asset-path'; +import { cn } from "@plane/utils"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; type Props = { title: string; description?: string; assetPath?: string; className?: string; -} +}; -const AnalyticsV2EmptyState = ({ - title, - description, - assetPath, - className, -}: Props) => { +const AnalyticsV2EmptyState = ({ title, description, assetPath, className }: Props) => { const backgroundReolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics-v2/empty-grid-background" }); return (
{assetPath && ( -
- {title} +
+ {title}
-
)} -
+

{title}

{description &&

{description}

}
-
-
- ) -} -export default AnalyticsV2EmptyState - - +
+ ); +}; +export default AnalyticsV2EmptyState; diff --git a/web/core/components/analytics-v2/index.ts b/web/core/components/analytics-v2/index.ts index 314c01ccf02..8ac82df5dfb 100644 --- a/web/core/components/analytics-v2/index.ts +++ b/web/core/components/analytics-v2/index.ts @@ -1 +1 @@ -export * from "./overview/root"; \ No newline at end of file +export * from "./overview/root"; diff --git a/web/core/components/analytics-v2/insight-card.tsx b/web/core/components/analytics-v2/insight-card.tsx index 12300d49062..859bb2a61f7 100644 --- a/web/core/components/analytics-v2/insight-card.tsx +++ b/web/core/components/analytics-v2/insight-card.tsx @@ -1,16 +1,16 @@ // plane package imports -import React, { useMemo } from 'react' -import { IAnalyticsResponseFieldsV2 } from '@plane/types' -import { Loader } from '@plane/ui' +import React, { useMemo } from "react"; +import { IAnalyticsResponseFieldsV2 } from "@plane/types"; +import { Loader } from "@plane/ui"; // components -import TrendPiece from './trend-piece' +import TrendPiece from "./trend-piece"; export type InsightCardProps = { data?: IAnalyticsResponseFieldsV2; label: string; isLoading?: boolean; versus?: string | null; -} +}; const InsightCard = (props: InsightCardProps) => { const { data, label, isLoading, versus } = props; @@ -38,10 +38,10 @@ const InsightCard = (props: InsightCardProps) => { )}
) : ( - + )}
); -} +}; -export default InsightCard; \ No newline at end of file +export default InsightCard; diff --git a/web/core/components/analytics-v2/insight-table/data-table.tsx b/web/core/components/analytics-v2/insight-table/data-table.tsx index bf0722cf5a8..6b2053a1523 100644 --- a/web/core/components/analytics-v2/insight-table/data-table.tsx +++ b/web/core/components/analytics-v2/insight-table/data-table.tsx @@ -1,6 +1,6 @@ -"use client" +"use client"; -import * as React from "react" +import * as React from "react"; import { ColumnDef, ColumnFiltersState, @@ -15,49 +15,33 @@ import { getPaginationRowModel, getSortedRowModel, useReactTable, -} from "@tanstack/react-table" -import { Search, X } from "lucide-react" +} from "@tanstack/react-table"; +import { Search, X } from "lucide-react"; // plane package imports -import { useTranslation } from "@plane/i18n" -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@plane/propel/table" -import { cn } from "@plane/utils" +import { useTranslation } from "@plane/i18n"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@plane/propel/table"; +import { cn } from "@plane/utils"; // plane web components -import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path" -import AnalyticsV2EmptyState from "../empty-state" +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; +import AnalyticsV2EmptyState from "../empty-state"; interface DataTableProps { - columns: ColumnDef[] - data: TData[] - searchPlaceholder: string - actions?: (table: TanstackTable) => React.ReactNode + columns: ColumnDef[]; + data: TData[]; + searchPlaceholder: string; + actions?: (table: TanstackTable) => React.ReactNode; } -export function DataTable({ - columns, - data, - searchPlaceholder, - actions, -}: DataTableProps) { - const [rowSelection, setRowSelection] = React.useState({}) - const [columnVisibility, setColumnVisibility] = - React.useState({}) - const [columnFilters, setColumnFilters] = React.useState( - [] - ) - const [sorting, setSorting] = React.useState([]) - const { t } = useTranslation() - const inputRef = React.useRef(null) - const [isSearchOpen, setIsSearchOpen] = React.useState(false) +export function DataTable({ columns, data, searchPlaceholder, actions }: DataTableProps) { + const [rowSelection, setRowSelection] = React.useState({}); + const [columnVisibility, setColumnVisibility] = React.useState({}); + const [columnFilters, setColumnFilters] = React.useState([]); + const [sorting, setSorting] = React.useState([]); + const { t } = useTranslation(); + const inputRef = React.useRef(null); + const [isSearchOpen, setIsSearchOpen] = React.useState(false); const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics-v2/empty-table" }); - const table = useReactTable({ data, columns, @@ -78,19 +62,21 @@ export function DataTable({ getSortedRowModel: getSortedRowModel(), getFacetedRowModel: getFacetedRowModel(), getFacetedUniqueValues: getFacetedUniqueValues(), - }) + }); return (
-
-
- {table.getHeaderGroups()?.[0]?.headers?.[0]?.id &&
- {searchPlaceholder} -
} +
+
+ {table.getHeaderGroups()?.[0]?.headers?.[0]?.id && ( +
+ {searchPlaceholder} +
+ )} {!isSearchOpen && ( } - /> :
No data
} -
- ); + return ( +
+ {data ? ( + ) => ( + + )} + /> + ) : ( +
No data
+ )} +
+ ); }; - diff --git a/web/core/components/analytics-v2/loaders.tsx b/web/core/components/analytics-v2/loaders.tsx index 406aaf39410..e35d235cecb 100644 --- a/web/core/components/analytics-v2/loaders.tsx +++ b/web/core/components/analytics-v2/loaders.tsx @@ -5,7 +5,7 @@ export const ProjectInsightsLoader = () => ( -
+
@@ -21,5 +21,3 @@ export const ChartLoader = () => ( ); - - diff --git a/web/core/components/analytics-v2/overview/active-project-item.tsx b/web/core/components/analytics-v2/overview/active-project-item.tsx index e82f6e3b8ad..088bf61852e 100644 --- a/web/core/components/analytics-v2/overview/active-project-item.tsx +++ b/web/core/components/analytics-v2/overview/active-project-item.tsx @@ -1,57 +1,57 @@ -import { Briefcase } from 'lucide-react'; +import { Briefcase } from "lucide-react"; // plane package imports -import { Logo } from '@plane/ui'; -import { cn } from '@plane/utils'; +import { Logo } from "@plane/ui"; +import { cn } from "@plane/utils"; // plane web hooks -import { useProject } from '@/hooks/store'; - +import { useProject } from "@/hooks/store"; type Props = { - project: { - id: string, - completed_issues?: number, - total_issues?: number, - } - isLoading?: boolean -} + project: { + id: string; + completed_issues?: number; + total_issues?: number; + }; + isLoading?: boolean; +}; const CompletionPercentage = ({ percentage }: { percentage: number }) => { - const percentageColor = percentage > 50 ? 'bg-green-500/30 text-green-500' : 'bg-red-500/30 text-red-500'; - return ( -
- {percentage}% -
- ) -} - - + const percentageColor = percentage > 50 ? "bg-green-500/30 text-green-500" : "bg-red-500/30 text-red-500"; + return ( +
+ {percentage}% +
+ ); +}; const ActiveProjectItem = (props: Props) => { - const { project } = props; - const { getProjectById } = useProject(); - const { id, completed_issues, total_issues } = project; - - const projectDetails = getProjectById(id); - - if (!projectDetails) return null; - - return ( -
-
-
- - {projectDetails?.logo_props ? : ( - - - - )} - -
-

{projectDetails?.name}

-
- + const { project } = props; + const { getProjectById } = useProject(); + const { id, completed_issues, total_issues } = project; + + const projectDetails = getProjectById(id); + + if (!projectDetails) return null; + + return ( +
+
+
+ + {projectDetails?.logo_props ? ( + + ) : ( + + + + )} +
- ) -} - -export default ActiveProjectItem \ No newline at end of file +

{projectDetails?.name}

+
+ +
+ ); +}; + +export default ActiveProjectItem; diff --git a/web/core/components/analytics-v2/overview/active-projects.tsx b/web/core/components/analytics-v2/overview/active-projects.tsx index dfddaeac30d..2d5d7111669 100644 --- a/web/core/components/analytics-v2/overview/active-projects.tsx +++ b/web/core/components/analytics-v2/overview/active-projects.tsx @@ -1,38 +1,44 @@ -import React from 'react' -import { observer } from 'mobx-react' -import { useParams } from 'next/navigation' -import useSWR from 'swr' +import React from "react"; +import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; +import useSWR from "swr"; // plane package imports -import { useTranslation } from '@plane/i18n' -import { Loader } from '@plane/ui' +import { useTranslation } from "@plane/i18n"; +import { Loader } from "@plane/ui"; // plane web hooks -import { useAnalyticsV2, useProject } from '@/hooks/store' +import { useAnalyticsV2, useProject } from "@/hooks/store"; // plane web components -import AnalyticsSectionWrapper from '../analytics-section-wrapper' -import ActiveProjectItem from './active-project-item' +import AnalyticsSectionWrapper from "../analytics-section-wrapper"; +import ActiveProjectItem from "./active-project-item"; const ActiveProjects = observer(() => { - const { t } = useTranslation() - const { fetchProjectAnalyticsCount } = useProject() - const { workspaceSlug } = useParams() - const { selectedDurationLabel } = useAnalyticsV2() - const { data: projectAnalyticsCount, isLoading: isProjectAnalyticsCountLoading } = useSWR(workspaceSlug ? ["projectAnalyticsCount", workspaceSlug] : null, workspaceSlug ? () => fetchProjectAnalyticsCount(workspaceSlug.toString(), { - fields: "total_work_items,total_completed_work_items" - }) : null) - return ( - -
- {isProjectAnalyticsCountLoading && Array.from({ length: 5 }).map((_, index) => ( - - ))} - {!isProjectAnalyticsCountLoading && projectAnalyticsCount?.map((project) => ( - - ))} -
-
- ) -}) + const { t } = useTranslation(); + const { fetchProjectAnalyticsCount } = useProject(); + const { workspaceSlug } = useParams(); + const { selectedDurationLabel } = useAnalyticsV2(); + const { data: projectAnalyticsCount, isLoading: isProjectAnalyticsCountLoading } = useSWR( + workspaceSlug ? ["projectAnalyticsCount", workspaceSlug] : null, + workspaceSlug + ? () => + fetchProjectAnalyticsCount(workspaceSlug.toString(), { + fields: "total_work_items,total_completed_work_items", + }) + : null + ); + return ( + +
+ {isProjectAnalyticsCountLoading && + Array.from({ length: 5 }).map((_, index) => )} + {!isProjectAnalyticsCountLoading && + projectAnalyticsCount?.map((project) => )} +
+
+ ); +}); - - -export default ActiveProjects +export default ActiveProjects; diff --git a/web/core/components/analytics-v2/overview/index.ts b/web/core/components/analytics-v2/overview/index.ts index 50a9c47c01f..1efe34c51ec 100644 --- a/web/core/components/analytics-v2/overview/index.ts +++ b/web/core/components/analytics-v2/overview/index.ts @@ -1 +1 @@ -export * from "./root"; \ No newline at end of file +export * from "./root"; diff --git a/web/core/components/analytics-v2/overview/project-insights.tsx b/web/core/components/analytics-v2/overview/project-insights.tsx index 376a1fde8c7..a5e18909258 100644 --- a/web/core/components/analytics-v2/overview/project-insights.tsx +++ b/web/core/components/analytics-v2/overview/project-insights.tsx @@ -1,101 +1,107 @@ -import { observer } from 'mobx-react' -import dynamic from 'next/dynamic' -import { useParams } from 'next/navigation' -import useSWR from 'swr' +import { observer } from "mobx-react"; +import dynamic from "next/dynamic"; +import { useParams } from "next/navigation"; +import useSWR from "swr"; // plane package imports -import { useTranslation } from '@plane/i18n' -import { TChartData } from '@plane/types' +import { useTranslation } from "@plane/i18n"; +import { TChartData } from "@plane/types"; // hooks -import { useAnalyticsV2 } from '@/hooks/store/use-analytics-v2' +import { useAnalyticsV2 } from "@/hooks/store/use-analytics-v2"; // services -import { useResolvedAssetPath } from '@/hooks/use-resolved-asset-path' -import { AnalyticsV2Service } from '@/services/analytics-v2.service' +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; +import { AnalyticsV2Service } from "@/services/analytics-v2.service"; // plane web components -import AnalyticsSectionWrapper from '../analytics-section-wrapper' -import AnalyticsV2EmptyState from '../empty-state' -import { ProjectInsightsLoader } from '../loaders' +import AnalyticsSectionWrapper from "../analytics-section-wrapper"; +import AnalyticsV2EmptyState from "../empty-state"; +import { ProjectInsightsLoader } from "../loaders"; const RadarChart = dynamic(() => import("@plane/propel/charts/radar-chart").then((mod) => ({ default: mod.RadarChart, })) -) +); - -const analyticsV2Service = new AnalyticsV2Service() +const analyticsV2Service = new AnalyticsV2Service(); const ProjectInsights = observer(() => { const params = useParams(); - const { t } = useTranslation() + const { t } = useTranslation(); const workspaceSlug = params.workspaceSlug as string; - const { selectedDuration, selectedDurationLabel, selectedProjects } = useAnalyticsV2() + const { selectedDuration, selectedDurationLabel, selectedProjects } = useAnalyticsV2(); const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics-v2/empty-chart-radar" }); - - const { data: projectInsightsData, isLoading: isLoadingProjectInsight } = useSWR( - `radar-chart-${workspaceSlug}`, - () => analyticsV2Service.getAdvanceAnalyticsCharts[]>(workspaceSlug, 'projects', { + const { data: projectInsightsData, isLoading: isLoadingProjectInsight } = useSWR(`radar-chart-${workspaceSlug}`, () => + analyticsV2Service.getAdvanceAnalyticsCharts[]>(workspaceSlug, "projects", { created_at: selectedDuration, - project_ids: selectedProjects?.join(','), - }), - ) - + project_ids: selectedProjects?.join(","), + }) + ); return ( - - {isLoadingProjectInsight ? : - projectInsightsData && projectInsightsData?.length == 0 ? - : -
- {projectInsightsData && + {isLoadingProjectInsight ? ( + + ) : projectInsightsData && projectInsightsData?.length == 0 ? ( + + ) : ( +
+ {projectInsightsData && ( + } -
-
{t('workspace_analytics.summary_of_projects')}
-
{t('workspace_analytics.all_projects')}
-
-
-
{t('workspace_analytics.trend_on_charts')}
-
{t('common.work_items')}
-
- {projectInsightsData?.map((item) => ( -
-
{item.name}
-
- {/* */} -
{item.count}
-
-
- ))} + /> + )} +
+
{t("workspace_analytics.summary_of_projects")}
+
{t("workspace_analytics.all_projects")}
+
+
+
{t("workspace_analytics.trend_on_charts")}
+
{t("common.work_items")}
+ {projectInsightsData?.map((item) => ( +
+
{item.name}
+
+ {/* */} +
{item.count}
+
+
+ ))}
-
} +
+
+ )} + ); +}); - ) -}) - -export default ProjectInsights \ No newline at end of file +export default ProjectInsights; diff --git a/web/core/components/analytics-v2/overview/root.tsx b/web/core/components/analytics-v2/overview/root.tsx index 1e05f26e5e3..3856353aa54 100644 --- a/web/core/components/analytics-v2/overview/root.tsx +++ b/web/core/components/analytics-v2/overview/root.tsx @@ -1,22 +1,19 @@ -import React from 'react' -import AnalyticsWrapper from '../analytics-wrapper' -import TotalInsights from '../total-insights' -import ActiveProjects from "./active-projects" -import ProjectInsights from './project-insights' - - +import React from "react"; +import AnalyticsWrapper from "../analytics-wrapper"; +import TotalInsights from "../total-insights"; +import ActiveProjects from "./active-projects"; +import ProjectInsights from "./project-insights"; const Overview: React.FC = () => ( - -
- -
+ +
+ +
-) - +); -export { Overview } \ No newline at end of file +export { Overview }; diff --git a/web/core/components/analytics-v2/select/analytics-params.tsx b/web/core/components/analytics-v2/select/analytics-params.tsx index eda4e0edf6f..61a9d1b1f9e 100644 --- a/web/core/components/analytics-v2/select/analytics-params.tsx +++ b/web/core/components/analytics-v2/select/analytics-params.tsx @@ -12,7 +12,6 @@ import { AnalyticsV2Service } from "@/services/analytics-v2.service"; import { SelectXAxis } from "./select-x-axis"; import { SelectYAxis } from "./select-y-axis"; - type Props = { control: Control; setValue: UseFormSetValue; @@ -23,15 +22,18 @@ type Props = { export const AnalyticsV2SelectParams: React.FC = observer((props) => { const { control, params, classNames } = props; - const xAxisOptions = useMemo(() => ANALYTICS_V2_X_AXIS_VALUES.filter((option) => option.value !== params.group_by), [params.group_by]); - const groupByOptions = useMemo(() => ANALYTICS_V2_X_AXIS_VALUES.filter((option) => option.value !== params.x_axis), [params.x_axis]); - + const xAxisOptions = useMemo( + () => ANALYTICS_V2_X_AXIS_VALUES.filter((option) => option.value !== params.group_by), + [params.group_by] + ); + const groupByOptions = useMemo( + () => ANALYTICS_V2_X_AXIS_VALUES.filter((option) => option.value !== params.x_axis), + [params.x_axis] + ); return ( -
-
+
+
= observer((props) => { onChange={(val) => { onChange(val); }} - label={
- - {xAxisOptions.find((v) => v.value === value)?.label || "Add Property"} -
} + label={ +
+ + + {xAxisOptions.find((v) => v.value === value)?.label || "Add Property"} + +
+ } options={xAxisOptions} /> )} @@ -72,10 +78,14 @@ export const AnalyticsV2SelectParams: React.FC = observer((props) => { onChange={(val) => { onChange(val); }} - label={
- - {groupByOptions.find((v) => v.value === value)?.label || "Add Property"} -
} + label={ +
+ + + {groupByOptions.find((v) => v.value === value)?.label || "Add Property"} + +
+ } options={groupByOptions} placeholder="Group By" allowNoValue @@ -84,6 +94,5 @@ export const AnalyticsV2SelectParams: React.FC = observer((props) => { />
- ); }); diff --git a/web/core/components/analytics-v2/select/duration.tsx b/web/core/components/analytics-v2/select/duration.tsx index e673491d65e..de18ab2023f 100644 --- a/web/core/components/analytics-v2/select/duration.tsx +++ b/web/core/components/analytics-v2/select/duration.tsx @@ -1,39 +1,33 @@ // plane package imports -import React, { ReactNode } from 'react' -import { Calendar } from 'lucide-react' +import React, { ReactNode } from "react"; +import { Calendar } from "lucide-react"; // plane package imports -import { ANALYTICS_V2_DURATION_FILTER_OPTIONS } from '@plane/constants' -import { useTranslation } from '@plane/i18n' -import { CustomSearchSelect } from '@plane/ui' +import { ANALYTICS_V2_DURATION_FILTER_OPTIONS } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import { CustomSearchSelect } from "@plane/ui"; // types -import { TDropdownProps } from '@/components/dropdowns/types' - - +import { TDropdownProps } from "@/components/dropdowns/types"; type Props = TDropdownProps & { - value: string | null - onChange: (val: typeof ANALYTICS_V2_DURATION_FILTER_OPTIONS[number]['value']) => void + value: string | null; + onChange: (val: (typeof ANALYTICS_V2_DURATION_FILTER_OPTIONS)[number]["value"]) => void; //optional - button?: ReactNode - dropdownArrow?: boolean - dropdownArrowClassName?: string - onClose?: () => void - renderByDefault?: boolean - tabIndex?: number -} + button?: ReactNode; + dropdownArrow?: boolean; + dropdownArrowClassName?: string; + onClose?: () => void; + renderByDefault?: boolean; + tabIndex?: number; +}; -function DurationDropdown({ - placeholder = "Duration", - onChange, - value -}: Props) { - useTranslation() +function DurationDropdown({ placeholder = "Duration", onChange, value }: Props) { + useTranslation(); const options = ANALYTICS_V2_DURATION_FILTER_OPTIONS.map((option) => ({ value: option.value, query: option.name, content: ( -
+
{option.name}
), @@ -45,12 +39,12 @@ function DurationDropdown({ options={options} label={
- - {value ? ANALYTICS_V2_DURATION_FILTER_OPTIONS.find(opt => opt.value === value)?.name : placeholder} + + {value ? ANALYTICS_V2_DURATION_FILTER_OPTIONS.find((opt) => opt.value === value)?.name : placeholder}
} /> - ) + ); } -export default DurationDropdown \ No newline at end of file +export default DurationDropdown; diff --git a/web/core/components/analytics-v2/select/project.tsx b/web/core/components/analytics-v2/select/project.tsx index 794c905ad2e..61a9942081e 100644 --- a/web/core/components/analytics-v2/select/project.tsx +++ b/web/core/components/analytics-v2/select/project.tsx @@ -7,7 +7,6 @@ import { CustomSearchSelect, Logo } from "@plane/ui"; // hooks import { useProject } from "@/hooks/store"; - type Props = { value: string[] | undefined; onChange: (val: string[] | null) => void; @@ -25,8 +24,12 @@ export const ProjectSelect: React.FC = observer((props) => { value: projectDetails?.id, query: `${projectDetails?.name} ${projectDetails?.identifier}`, content: ( -
- {projectDetails?.logo_props ? : } +
+ {projectDetails?.logo_props ? ( + + ) : ( + + )} {projectDetails?.name}
), @@ -40,15 +43,15 @@ export const ProjectSelect: React.FC = observer((props) => { options={options} label={
- - {value && value.length > 3 ? - `3+ projects` + + {value && value.length > 3 + ? `3+ projects` : value && value.length > 0 - ? projectIds + ? projectIds ?.filter((p) => value.includes(p)) .map((p) => getProjectById(p)?.name) .join(", ") - : "All projects"} + : "All projects"}
} multiple diff --git a/web/core/components/analytics-v2/select/select-x-axis.tsx b/web/core/components/analytics-v2/select/select-x-axis.tsx index 7b3dc7606e6..a655c9a13de 100644 --- a/web/core/components/analytics-v2/select/select-x-axis.tsx +++ b/web/core/components/analytics-v2/select/select-x-axis.tsx @@ -16,12 +16,7 @@ type Props = { export const SelectXAxis: React.FC = (props) => { const { value, onChange, options, hiddenOptions, allowNoValue, label } = props; return ( - + {allowNoValue && No value} {options.map((item) => { if (hiddenOptions?.includes(item.value)) return null; diff --git a/web/core/components/analytics-v2/select/select-y-axis.tsx b/web/core/components/analytics-v2/select/select-y-axis.tsx index 37e4acbbfbf..c80e2a1e47e 100644 --- a/web/core/components/analytics-v2/select/select-y-axis.tsx +++ b/web/core/components/analytics-v2/select/select-y-axis.tsx @@ -45,23 +45,23 @@ export const SelectYAxis: React.FC = observer(({ value, onChange, hiddenO value={value} label={
- + {options.find((v) => v.value === value)?.label ?? "Add Metric"}
} onChange={onChange} maxHeight="lg" > - {options.map( - (item) => { - if (hiddenOptions?.includes(item.value)) return null; - return isEstimateEnabled(item.value) && ( + {options.map((item) => { + if (hiddenOptions?.includes(item.value)) return null; + return ( + isEstimateEnabled(item.value) && ( {item.label} ) - } - )} + ); + })}
); }); diff --git a/web/core/components/analytics-v2/total-insights.tsx b/web/core/components/analytics-v2/total-insights.tsx index 9a86bd35c6b..ce8dd357652 100644 --- a/web/core/components/analytics-v2/total-insights.tsx +++ b/web/core/components/analytics-v2/total-insights.tsx @@ -1,52 +1,58 @@ - // plane package imports -import { observer } from 'mobx-react-lite'; -import { useParams } from 'next/navigation'; -import useSWR from 'swr'; -import { insightsFields } from '@plane/constants'; -import { useTranslation } from '@plane/i18n'; -import { IAnalyticsResponseV2, TAnalyticsTabsV2Base } from '@plane/types'; +import { observer } from "mobx-react-lite"; +import { useParams } from "next/navigation"; +import useSWR from "swr"; +import { insightsFields } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import { IAnalyticsResponseV2, TAnalyticsTabsV2Base } from "@plane/types"; //hooks -import { cn } from '@/helpers/common.helper'; -import { useAnalyticsV2 } from '@/hooks/store/use-analytics-v2'; +import { cn } from "@/helpers/common.helper"; +import { useAnalyticsV2 } from "@/hooks/store/use-analytics-v2"; //services -import { AnalyticsV2Service } from '@/services/analytics-v2.service'; +import { AnalyticsV2Service } from "@/services/analytics-v2.service"; // plane web components -import InsightCard from './insight-card'; - +import InsightCard from "./insight-card"; const analyticsV2Service = new AnalyticsV2Service(); -const TotalInsights: React.FC<{ analyticsType: TAnalyticsTabsV2Base, peekView?: boolean }> = observer(({ analyticsType, peekView }) => { +const TotalInsights: React.FC<{ analyticsType: TAnalyticsTabsV2Base; peekView?: boolean }> = observer( + ({ analyticsType, peekView }) => { const params = useParams(); const workspaceSlug = params.workspaceSlug as string; - const { t } = useTranslation() - const { selectedDuration, selectedProjects, selectedDurationLabel } = useAnalyticsV2() + const { t } = useTranslation(); + const { selectedDuration, selectedProjects, selectedDurationLabel } = useAnalyticsV2(); - const { data: totalInsightsData, isLoading } = useSWR(`total-insights-${analyticsType}-${selectedDuration}-${selectedProjects}`, - () => analyticsV2Service.getAdvanceAnalytics(workspaceSlug, analyticsType, { - date_filter: selectedDuration, - ...(selectedProjects ? { project_ids: selectedProjects.join(',') } : {}) - })) + const { data: totalInsightsData, isLoading } = useSWR( + `total-insights-${analyticsType}-${selectedDuration}-${selectedProjects}`, + () => + analyticsV2Service.getAdvanceAnalytics(workspaceSlug, analyticsType, { + date_filter: selectedDuration, + ...(selectedProjects ? { project_ids: selectedProjects.join(",") } : {}), + }) + ); return ( -
- {insightsFields[analyticsType].map((item: string) => ( - - ))} -
- ) -}) +
+ {insightsFields[analyticsType].map((item: string) => ( + + ))} +
+ ); + } +); -export default TotalInsights; \ No newline at end of file +export default TotalInsights; diff --git a/web/core/components/analytics-v2/trend-piece.tsx b/web/core/components/analytics-v2/trend-piece.tsx index 896003b4c26..23daa89be44 100644 --- a/web/core/components/analytics-v2/trend-piece.tsx +++ b/web/core/components/analytics-v2/trend-piece.tsx @@ -1,54 +1,47 @@ // plane package imports -import React from 'react' -import { TrendingDown, TrendingUp } from 'lucide-react' -import { cn } from '@plane/utils' +import React from "react"; +import { TrendingDown, TrendingUp } from "lucide-react"; +import { cn } from "@plane/utils"; // plane web components type Props = { - percentage: number - className?: string - size?: 'xs' | 'sm' | 'md' | 'lg' -} + percentage: number; + className?: string; + size?: "xs" | "sm" | "md" | "lg"; +}; const sizeConfig = { - xs: { - text: 'text-xs', - icon: 'w-3 h-3' - }, - sm: { - text: 'text-sm', - icon: 'w-4 h-4' - }, - md: { - text: 'text-base', - icon: 'w-5 h-5' - }, - lg: { - text: 'text-lg', - icon: 'w-6 h-6' - } -} as const + xs: { + text: "text-xs", + icon: "w-3 h-3", + }, + sm: { + text: "text-sm", + icon: "w-4 h-4", + }, + md: { + text: "text-base", + icon: "w-5 h-5", + }, + lg: { + text: "text-lg", + icon: "w-6 h-6", + }, +} as const; const TrendPiece = (props: Props) => { - const { percentage, className, size = 'sm' } = props - const isPositive = percentage > 0 - const config = sizeConfig[size] + const { percentage, className, size = "sm" } = props; + const isPositive = percentage > 0; + const config = sizeConfig[size]; - return ( -
- {isPositive ? ( - - ) : ( - - )} - {Math.round(Math.abs(percentage))}% -
- ) -} + return ( +
+ {isPositive ? : } + {Math.round(Math.abs(percentage))}% +
+ ); +}; -export default TrendPiece \ No newline at end of file +export default TrendPiece; diff --git a/web/core/components/analytics-v2/work-items/created-vs-resolved.tsx b/web/core/components/analytics-v2/work-items/created-vs-resolved.tsx index fe439f48ef2..59f671d8b70 100644 --- a/web/core/components/analytics-v2/work-items/created-vs-resolved.tsx +++ b/web/core/components/analytics-v2/work-items/created-vs-resolved.tsx @@ -1,112 +1,119 @@ - -import { useMemo } from 'react' -import { observer } from 'mobx-react' -import { useParams } from 'next/navigation' -import useSWR from 'swr' +import { useMemo } from "react"; +import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; +import useSWR from "swr"; // plane package imports -import { useTranslation } from '@plane/i18n' -import { AreaChart } from '@plane/propel/charts/area-chart' -import { IChartResponseV2, TChartData } from '@plane/types' -import { renderFormattedDate } from '@plane/utils' +import { useTranslation } from "@plane/i18n"; +import { AreaChart } from "@plane/propel/charts/area-chart"; +import { IChartResponseV2, TChartData } from "@plane/types"; +import { renderFormattedDate } from "@plane/utils"; // hooks -import { useAnalyticsV2 } from '@/hooks/store/use-analytics-v2' +import { useAnalyticsV2 } from "@/hooks/store/use-analytics-v2"; // services -import { useResolvedAssetPath } from '@/hooks/use-resolved-asset-path' -import { AnalyticsV2Service } from '@/services/analytics-v2.service' +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; +import { AnalyticsV2Service } from "@/services/analytics-v2.service"; // plane web components -import AnalyticsSectionWrapper from '../analytics-section-wrapper' -import AnalyticsV2EmptyState from '../empty-state' -import { ChartLoader } from '../loaders' - - +import AnalyticsSectionWrapper from "../analytics-section-wrapper"; +import AnalyticsV2EmptyState from "../empty-state"; +import { ChartLoader } from "../loaders"; -const analyticsV2Service = new AnalyticsV2Service() +const analyticsV2Service = new AnalyticsV2Service(); const CreatedVsResolved = observer(() => { - const { selectedDuration, selectedDurationLabel, selectedProjects } = useAnalyticsV2() + const { selectedDuration, selectedDurationLabel, selectedProjects } = useAnalyticsV2(); const params = useParams(); - const { t } = useTranslation() + const { t } = useTranslation(); const workspaceSlug = params.workspaceSlug as string; const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics-v2/empty-chart-area" }); const { data: createdVsResolvedData, isLoading: isCreatedVsResolvedLoading } = useSWR( `created-vs-resolved-${workspaceSlug}-${selectedDuration}`, - () => analyticsV2Service.getAdvanceAnalyticsCharts(workspaceSlug, 'work-items', { - date_filter: selectedDuration, - project_ids: selectedProjects?.join(','), - }), - ) + () => + analyticsV2Service.getAdvanceAnalyticsCharts(workspaceSlug, "work-items", { + date_filter: selectedDuration, + project_ids: selectedProjects?.join(","), + }) + ); const parsedData: TChartData[] = useMemo(() => { - if (!createdVsResolvedData?.data) return [] + if (!createdVsResolvedData?.data) return []; return createdVsResolvedData.data.map((datum) => ({ ...datum, [datum.key]: datum.count, - name: renderFormattedDate(datum.key) ?? datum.key - })) - }, [createdVsResolvedData]) + name: renderFormattedDate(datum.key) ?? datum.key, + })); + }, [createdVsResolvedData]); - const areas = useMemo(() => [ - { - key: "completed_issues", - label: "Resolved", - fill: "#19803833", - fillOpacity: 1, - stackId: "bar-one", - showDot: false, - smoothCurves: true, - strokeColor: "#198038", - strokeOpacity: 1, - }, - { - key: "created_issues", - label: "Created", - fill: "#1192E833", - fillOpacity: 1, - stackId: "bar-one", - showDot: false, - smoothCurves: true, - strokeColor: "#1192E8", - strokeOpacity: 1, - }, - ], []); + const areas = useMemo( + () => [ + { + key: "completed_issues", + label: "Resolved", + fill: "#19803833", + fillOpacity: 1, + stackId: "bar-one", + showDot: false, + smoothCurves: true, + strokeColor: "#198038", + strokeOpacity: 1, + }, + { + key: "created_issues", + label: "Created", + fill: "#1192E833", + fillOpacity: 1, + stackId: "bar-one", + showDot: false, + smoothCurves: true, + strokeColor: "#1192E8", + strokeOpacity: 1, + }, + ], + [] + ); return ( - - {isCreatedVsResolvedLoading ? : - parsedData && parsedData.length > 0 ? - : - - } + + {isCreatedVsResolvedLoading ? ( + + ) : parsedData && parsedData.length > 0 ? ( + + ) : ( + + )} - ) -}) + ); +}); -export default CreatedVsResolved \ No newline at end of file +export default CreatedVsResolved; diff --git a/web/core/components/analytics-v2/work-items/customized-insights.tsx b/web/core/components/analytics-v2/work-items/customized-insights.tsx index 17bbdb2dd34..86fea0c83b7 100644 --- a/web/core/components/analytics-v2/work-items/customized-insights.tsx +++ b/web/core/components/analytics-v2/work-items/customized-insights.tsx @@ -1,39 +1,41 @@ -import { observer } from 'mobx-react' -import { useParams } from 'next/navigation' -import { useForm } from 'react-hook-form' +import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; +import { useForm } from "react-hook-form"; // plane package imports -import { ChartXAxisProperty, ChartYAxisMetric } from '@plane/constants' -import { useTranslation } from '@plane/i18n' -import { IAnalyticsV2Params } from '@plane/types' -import { cn } from '@plane/utils' +import { ChartXAxisProperty, ChartYAxisMetric } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import { IAnalyticsV2Params } from "@plane/types"; +import { cn } from "@plane/utils"; // plane web components -import AnalyticsSectionWrapper from '../analytics-section-wrapper' -import { AnalyticsV2SelectParams } from '../select/analytics-params' -import PriorityChart from './priority-chart' +import AnalyticsSectionWrapper from "../analytics-section-wrapper"; +import { AnalyticsV2SelectParams } from "../select/analytics-params"; +import PriorityChart from "./priority-chart"; const defaultValues: IAnalyticsV2Params = { x_axis: ChartXAxisProperty.PRIORITY, y_axis: ChartYAxisMetric.WORK_ITEM_COUNT, -} +}; const CustomizedInsights = observer(({ peekView }: { peekView?: boolean }) => { - const { t } = useTranslation() + const { t } = useTranslation(); const { workspaceSlug } = useParams(); const { control, watch, setValue } = useForm({ defaultValues: { ...defaultValues, }, - }) + }); const params = { - x_axis: watch('x_axis'), - y_axis: watch('y_axis'), - group_by: watch('group_by'), - } + x_axis: watch("x_axis"), + y_axis: watch("y_axis"), + group_by: watch("group_by"), + }; return ( - { > - ) -}) + ); +}); -export default CustomizedInsights \ No newline at end of file +export default CustomizedInsights; diff --git a/web/core/components/analytics-v2/work-items/index.ts b/web/core/components/analytics-v2/work-items/index.ts index c8711b96a4c..1efe34c51ec 100644 --- a/web/core/components/analytics-v2/work-items/index.ts +++ b/web/core/components/analytics-v2/work-items/index.ts @@ -1 +1 @@ -export * from './root' \ No newline at end of file +export * from "./root"; diff --git a/web/core/components/analytics-v2/work-items/modal/content.tsx b/web/core/components/analytics-v2/work-items/modal/content.tsx index db667a3ea85..85004d9af68 100644 --- a/web/core/components/analytics-v2/work-items/modal/content.tsx +++ b/web/core/components/analytics-v2/work-items/modal/content.tsx @@ -19,21 +19,26 @@ type Props = { export const WorkItemsModalMainContent: React.FC = observer((props) => { const { projectDetails, fullScreen } = props; - const { updateSelectedProjects } = useAnalyticsV2() - const [isProjectConfigured, setIsProjectConfigured] = useState(false) + const { updateSelectedProjects } = useAnalyticsV2(); + const [isProjectConfigured, setIsProjectConfigured] = useState(false); useEffect(() => { if (!projectDetails?.id) return; - updateSelectedProjects([projectDetails?.id ?? '']) - setIsProjectConfigured(true) - }, [projectDetails?.id, updateSelectedProjects]) + updateSelectedProjects([projectDetails?.id ?? ""]); + setIsProjectConfigured(true); + }, [projectDetails?.id, updateSelectedProjects]); - if (!isProjectConfigured) return
+ if (!isProjectConfigured) + return ( +
+ +
+ ); return ( -
- +
+ diff --git a/web/core/components/analytics-v2/work-items/modal/header.tsx b/web/core/components/analytics-v2/work-items/modal/header.tsx index e57b4ef97a9..f4bcdee3819 100644 --- a/web/core/components/analytics-v2/work-items/modal/header.tsx +++ b/web/core/components/analytics-v2/work-items/modal/header.tsx @@ -19,7 +19,7 @@ export const WorkItemsModalHeader: React.FC = observer((props) => {
} - /> - : - + )} /> - } + + ) : ( + + )}
+ ); +}); - ) -}) - -export default PriorityChart \ No newline at end of file +export default PriorityChart; diff --git a/web/core/components/analytics-v2/work-items/root.tsx b/web/core/components/analytics-v2/work-items/root.tsx index 7648070c368..80e8aef6207 100644 --- a/web/core/components/analytics-v2/work-items/root.tsx +++ b/web/core/components/analytics-v2/work-items/root.tsx @@ -1,21 +1,19 @@ -import React from 'react' -import AnalyticsWrapper from '../analytics-wrapper' -import TotalInsights from '../total-insights' -import CreatedVsResolved from './created-vs-resolved' -import CustomizedInsights from './customized-insights' -import WorkItemsInsightTable from './workitems-insight-table' - +import React from "react"; +import AnalyticsWrapper from "../analytics-wrapper"; +import TotalInsights from "../total-insights"; +import CreatedVsResolved from "./created-vs-resolved"; +import CustomizedInsights from "./customized-insights"; +import WorkItemsInsightTable from "./workitems-insight-table"; const WorkItems: React.FC = () => ( - -
- + +
+
-) - +); -export { WorkItems } \ No newline at end of file +export { WorkItems }; diff --git a/web/core/components/analytics-v2/work-items/utils.ts b/web/core/components/analytics-v2/work-items/utils.ts index 3e3b3692dc5..1d3ffddfa94 100644 --- a/web/core/components/analytics-v2/work-items/utils.ts +++ b/web/core/components/analytics-v2/work-items/utils.ts @@ -3,49 +3,47 @@ import { ChartXAxisProperty, ChartYAxisMetric } from "@plane/constants"; import { IState } from "@plane/types"; interface ParamsProps { - x_axis: ChartXAxisProperty - y_axis: ChartYAxisMetric - group_by?: ChartXAxisProperty + x_axis: ChartXAxisProperty; + y_axis: ChartYAxisMetric; + group_by?: ChartXAxisProperty; } export const generateBarColor = ( value: string, params: ParamsProps, baseColors: string[], - workspaceStates?: IState[], + workspaceStates?: IState[] ): string => { - - let color = baseColors[0] + let color = baseColors[0]; // Priority if (params.x_axis === ChartXAxisProperty.PRIORITY) { color = value === "urgent" ? "#ef4444" : value === "high" - ? "#f97316" - : value === "medium" - ? "#eab308" - : value === "low" - ? "#22c55e" - : "#ced4da"; + ? "#f97316" + : value === "medium" + ? "#eab308" + : value === "low" + ? "#22c55e" + : "#ced4da"; } // State if (params.x_axis === ChartXAxisProperty.STATES) { - const state = workspaceStates?.find((s) => s.id === value) + const state = workspaceStates?.find((s) => s.id === value); if (state) { - color = state.color + color = state.color; } } // Label if (params.x_axis === ChartXAxisProperty.LABELS) { - const label = workspaceStates?.find((l) => l.id === value) + const label = workspaceStates?.find((l) => l.id === value); if (label) { - color = label.color + color = label.color; } } - - return color -}; \ No newline at end of file + return color; +}; diff --git a/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx b/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx index 2b2dfb318ed..00aad240471 100644 --- a/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx +++ b/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx @@ -1,97 +1,102 @@ -import { useMemo } from 'react'; -import { ColumnDef } from '@tanstack/react-table'; -import { observer } from 'mobx-react'; -import { useParams } from 'next/navigation'; -import useSWR from 'swr'; -import { Briefcase } from 'lucide-react'; +import { useMemo } from "react"; +import { ColumnDef } from "@tanstack/react-table"; +import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; +import useSWR from "swr"; +import { Briefcase } from "lucide-react"; // plane package imports -import { useTranslation } from '@plane/i18n'; -import { WorkItemInsightColumns, AnalyticsTableDataMap } from '@plane/types'; +import { useTranslation } from "@plane/i18n"; +import { WorkItemInsightColumns, AnalyticsTableDataMap } from "@plane/types"; // plane web components -import { Logo } from '@/components/common/logo'; +import { Logo } from "@/components/common/logo"; // hooks -import { useAnalyticsV2 } from '@/hooks/store/use-analytics-v2'; -import { useProject } from '@/hooks/store/use-project'; -import { AnalyticsV2Service } from '@/services/analytics-v2.service'; +import { useAnalyticsV2 } from "@/hooks/store/use-analytics-v2"; +import { useProject } from "@/hooks/store/use-project"; +import { AnalyticsV2Service } from "@/services/analytics-v2.service"; // plane web components -import { InsightTable } from '../insight-table'; +import { InsightTable } from "../insight-table"; const analyticsV2Service = new AnalyticsV2Service(); const WorkItemsInsightTable = observer(() => { - // router - const params = useParams(); - const workspaceSlug = params.workspaceSlug as string; - const { t } = useTranslation(); - // store hooks - const { getProjectById } = useProject(); - const { selectedDuration, selectedProjects } = useAnalyticsV2() - const { data: workItemsData, isLoading } = useSWR(`insights-table-work-items-${selectedDuration}-${selectedProjects}`, - () => analyticsV2Service.getAdvanceAnalyticsStats(workspaceSlug, "work-items", { - date_filter: selectedDuration, - ...(selectedProjects ? { project_ids: selectedProjects.join(',') } : {}) - })) - // derived values - const columnsLabels: Record = { - backlog_work_items: t("workspace_projects.state.backlog"), - started_work_items: t("workspace_projects.state.started"), - un_started_work_items: t("workspace_projects.state.unstarted"), - completed_work_items: t("workspace_projects.state.completed"), - cancelled_work_items: t("workspace_projects.state.cancelled"), - project__name: t("common.project") - } - const columns = useMemo(() => [ + // router + const params = useParams(); + const workspaceSlug = params.workspaceSlug as string; + const { t } = useTranslation(); + // store hooks + const { getProjectById } = useProject(); + const { selectedDuration, selectedProjects } = useAnalyticsV2(); + const { data: workItemsData, isLoading } = useSWR( + `insights-table-work-items-${selectedDuration}-${selectedProjects}`, + () => + analyticsV2Service.getAdvanceAnalyticsStats(workspaceSlug, "work-items", { + date_filter: selectedDuration, + ...(selectedProjects ? { project_ids: selectedProjects.join(",") } : {}), + }) + ); + // derived values + const columnsLabels: Record = { + backlog_work_items: t("workspace_projects.state.backlog"), + started_work_items: t("workspace_projects.state.started"), + un_started_work_items: t("workspace_projects.state.unstarted"), + completed_work_items: t("workspace_projects.state.completed"), + cancelled_work_items: t("workspace_projects.state.cancelled"), + project__name: t("common.project"), + }; + const columns = useMemo( + () => + [ { - accessorKey: "project__name", - header: () =>
{columnsLabels["project__name"]}
, - cell: ({ row }) => { - const project = getProjectById(row.original.project_id); - return
- {project?.logo_props ? ( - - ) : ( - - )} - {project?.name} -
- }, + accessorKey: "project__name", + header: () =>
{columnsLabels["project__name"]}
, + cell: ({ row }) => { + const project = getProjectById(row.original.project_id); + return ( +
+ {project?.logo_props ? : } + {project?.name} +
+ ); + }, }, { - accessorKey: "backlog_work_items", - header: () =>
{columnsLabels["backlog_work_items"]}
, - cell: ({ row }) =>
{row.original.backlog_work_items}
+ accessorKey: "backlog_work_items", + header: () =>
{columnsLabels["backlog_work_items"]}
, + cell: ({ row }) =>
{row.original.backlog_work_items}
, }, { - accessorKey: "started_work_items", - header: () =>
{columnsLabels["started_work_items"]}
, - cell: ({ row }) =>
{row.original.started_work_items}
+ accessorKey: "started_work_items", + header: () =>
{columnsLabels["started_work_items"]}
, + cell: ({ row }) =>
{row.original.started_work_items}
, }, { - accessorKey: "un_started_work_items", - header: () =>
{columnsLabels["un_started_work_items"]}
, - cell: ({ row }) =>
{row.original.un_started_work_items}
+ accessorKey: "un_started_work_items", + header: () =>
{columnsLabels["un_started_work_items"]}
, + cell: ({ row }) =>
{row.original.un_started_work_items}
, }, { - accessorKey: "completed_work_items", - header: () =>
{columnsLabels["completed_work_items"]}
, - cell: ({ row }) =>
{row.original.completed_work_items}
+ accessorKey: "completed_work_items", + header: () =>
{columnsLabels["completed_work_items"]}
, + cell: ({ row }) =>
{row.original.completed_work_items}
, }, { - accessorKey: "cancelled_work_items", - header: () =>
{columnsLabels["cancelled_work_items"]}
, - cell: ({ row }) =>
{row.original.cancelled_work_items}
- } - ] as ColumnDef[], [getProjectById]) + accessorKey: "cancelled_work_items", + header: () =>
{columnsLabels["cancelled_work_items"]}
, + cell: ({ row }) =>
{row.original.cancelled_work_items}
, + }, + ] as ColumnDef[], + [getProjectById] + ); - return ( - - analyticsType="work-items" - data={workItemsData} - isLoading={isLoading} - columns={columns} - columnsLabels={columnsLabels} - /> - ) -}) + return ( + + analyticsType="work-items" + data={workItemsData} + isLoading={isLoading} + columns={columns} + columnsLabels={columnsLabels} + /> + ); +}); -export default WorkItemsInsightTable \ No newline at end of file +export default WorkItemsInsightTable; From 073d7e3f05569f6dd3fcbde169872e0ee8d9781f Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Fri, 9 May 2025 15:55:09 +0530 Subject: [PATCH 59/69] fixed empty project_ids in payload --- .../components/analytics-v2/overview/project-insights.tsx | 2 +- web/core/components/analytics-v2/total-insights.tsx | 2 +- .../analytics-v2/work-items/created-vs-resolved.tsx | 2 +- .../components/analytics-v2/work-items/priority-chart.tsx | 2 +- .../analytics-v2/work-items/workitems-insight-table.tsx | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/web/core/components/analytics-v2/overview/project-insights.tsx b/web/core/components/analytics-v2/overview/project-insights.tsx index a5e18909258..93b2a8ca3f2 100644 --- a/web/core/components/analytics-v2/overview/project-insights.tsx +++ b/web/core/components/analytics-v2/overview/project-insights.tsx @@ -33,7 +33,7 @@ const ProjectInsights = observer(() => { const { data: projectInsightsData, isLoading: isLoadingProjectInsight } = useSWR(`radar-chart-${workspaceSlug}`, () => analyticsV2Service.getAdvanceAnalyticsCharts[]>(workspaceSlug, "projects", { created_at: selectedDuration, - project_ids: selectedProjects?.join(","), + ...(selectedProjects?.length > 0 && { project_ids: selectedProjects?.join(",") }), }) ); diff --git a/web/core/components/analytics-v2/total-insights.tsx b/web/core/components/analytics-v2/total-insights.tsx index ce8dd357652..e911ac30f3d 100644 --- a/web/core/components/analytics-v2/total-insights.tsx +++ b/web/core/components/analytics-v2/total-insights.tsx @@ -27,7 +27,7 @@ const TotalInsights: React.FC<{ analyticsType: TAnalyticsTabsV2Base; peekView?: () => analyticsV2Service.getAdvanceAnalytics(workspaceSlug, analyticsType, { date_filter: selectedDuration, - ...(selectedProjects ? { project_ids: selectedProjects.join(",") } : {}), + ...(selectedProjects?.length > 0 ? { project_ids: selectedProjects.join(",") } : {}), }) ); return ( diff --git a/web/core/components/analytics-v2/work-items/created-vs-resolved.tsx b/web/core/components/analytics-v2/work-items/created-vs-resolved.tsx index 59f671d8b70..1c8ac63cd76 100644 --- a/web/core/components/analytics-v2/work-items/created-vs-resolved.tsx +++ b/web/core/components/analytics-v2/work-items/created-vs-resolved.tsx @@ -29,7 +29,7 @@ const CreatedVsResolved = observer(() => { () => analyticsV2Service.getAdvanceAnalyticsCharts(workspaceSlug, "work-items", { date_filter: selectedDuration, - project_ids: selectedProjects?.join(","), + ...(selectedProjects?.length > 0 && { project_ids: selectedProjects?.join(",") }), }) ); const parsedData: TChartData[] = useMemo(() => { diff --git a/web/core/components/analytics-v2/work-items/priority-chart.tsx b/web/core/components/analytics-v2/work-items/priority-chart.tsx index 27786f41b15..a51c7e78990 100644 --- a/web/core/components/analytics-v2/work-items/priority-chart.tsx +++ b/web/core/components/analytics-v2/work-items/priority-chart.tsx @@ -58,7 +58,7 @@ const PriorityChart = observer((props: Props) => { () => analyticsV2Service.getAdvanceAnalyticsCharts(workspaceSlug, "custom-work-items", { date_filter: selectedDuration, - project_ids: selectedProjects?.join(","), + ...(selectedProjects?.length > 0 && { project_ids: selectedProjects?.join(",") }), ...props, }) ); diff --git a/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx b/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx index 00aad240471..cfd5a0aa0b2 100644 --- a/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx +++ b/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx @@ -31,12 +31,12 @@ const WorkItemsInsightTable = observer(() => { () => analyticsV2Service.getAdvanceAnalyticsStats(workspaceSlug, "work-items", { date_filter: selectedDuration, - ...(selectedProjects ? { project_ids: selectedProjects.join(",") } : {}), + ...(selectedProjects?.length > 0 ? { project_ids: selectedProjects.join(",") } : {}), }) ); // derived values const columnsLabels: Record = { - backlog_work_items: t("workspace_projects.state.backlog"), + backlog_work_items: t("workspace_projects.statex.backlog"), started_work_items: t("workspace_projects.state.started"), un_started_work_items: t("workspace_projects.state.unstarted"), completed_work_items: t("workspace_projects.state.completed"), From 5d6315e4a12535b344920fdf6fb19ff20059d921 Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Fri, 9 May 2025 18:28:03 +0530 Subject: [PATCH 60/69] improved null checks --- .../propel/src/charts/area-chart/root.tsx | 25 +- packages/propel/src/charts/bar-chart/root.tsx | 2 +- .../propel/src/charts/scatter-chart/root.tsx | 269 +++++++++--------- .../work-items/workitems-insight-table.tsx | 2 +- 4 files changed, 143 insertions(+), 155 deletions(-) diff --git a/packages/propel/src/charts/area-chart/root.tsx b/packages/propel/src/charts/area-chart/root.tsx index 02e2b070719..713f546d4de 100644 --- a/packages/propel/src/charts/area-chart/root.tsx +++ b/packages/propel/src/charts/area-chart/root.tsx @@ -31,20 +31,11 @@ export const AreaChart = React.memo((props: const [activeLegend, setActiveLegend] = useState(null); // derived values - const itemKeys = useMemo( - () => Array.from(areas, area => area.key), - [areas] - ); + const itemKeys = useMemo(() => Array.from(areas, (area) => area.key), [areas]); - const itemLabels = useMemo( - () => Object.fromEntries(areas.map(area => [area.key, area.label])), - [areas] - ); + const itemLabels = useMemo(() => Object.fromEntries(areas.map((area) => [area.key, area.label])), [areas]); - const itemDotColors = useMemo( - () => Object.fromEntries(areas.map(area => [area.key, area.fill])), - [areas] - ); + const itemDotColors = useMemo(() => Object.fromEntries(areas.map((area) => [area.key, area.fill])), [areas]); const renderAreas = useMemo( () => areas.map((area) => ( @@ -63,9 +54,9 @@ export const AreaChart = React.memo((props: dot={ area.showDot ? { - fill: area.fill, - fillOpacity: 1, - } + fill: area.fill, + fillOpacity: 1, + } : false } activeDot={{ @@ -85,7 +76,7 @@ export const AreaChart = React.memo((props: // get the last data point const lastPoint = data[data.length - 1]; // for the y-value in the last point, use its yAxis key value - const lastYValue = lastPoint[yAxis.key] || 0; + const lastYValue = lastPoint[yAxis.key] ?? 0; // create data for a straight line that has points at each x-axis position return data.map((item, index) => { // calculate the y value for this point on the straight line @@ -121,7 +112,7 @@ export const AreaChart = React.memo((props: xAxis.label && { value: xAxis.label, dy: 28, - className: AXIS_LABEL_CLASSNAME + className: AXIS_LABEL_CLASSNAME, } } tickCount={tickCount.x} diff --git a/packages/propel/src/charts/bar-chart/root.tsx b/packages/propel/src/charts/bar-chart/root.tsx index a18dd042f23..dc454152d43 100644 --- a/packages/propel/src/charts/bar-chart/root.tsx +++ b/packages/propel/src/charts/bar-chart/root.tsx @@ -102,7 +102,7 @@ export const BarChart = React.memo((props: T axisLine={false} label={{ value: xAxis.label, - dy: xAxis.dy || 28, + dy: xAxis.dy ?? 28, className: AXIS_LABEL_CLASSNAME, }} tickCount={tickCount.x} diff --git a/packages/propel/src/charts/scatter-chart/root.tsx b/packages/propel/src/charts/scatter-chart/root.tsx index 3a2cc3c5785..69bfaee059b 100644 --- a/packages/propel/src/charts/scatter-chart/root.tsx +++ b/packages/propel/src/charts/scatter-chart/root.tsx @@ -3,15 +3,15 @@ import React, { useMemo, useState } from "react"; import { - CartesianGrid, - ScatterChart as CoreScatterChart, - Legend, - Scatter, - ResponsiveContainer, - Tooltip, - XAxis, - YAxis, - ZAxis, + CartesianGrid, + ScatterChart as CoreScatterChart, + Legend, + Scatter, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, + ZAxis, } from "recharts"; // plane imports import { AXIS_LABEL_CLASSNAME } from "@plane/constants"; @@ -22,134 +22,131 @@ import { CustomXAxisTick, CustomYAxisTick } from "../components/tick"; import { CustomTooltip } from "../components/tooltip"; export const ScatterChart = React.memo((props: TScatterChartProps) => { - const { - data, - scatterPoints, - margin, - xAxis, - yAxis, + const { + data, + scatterPoints, + margin, + xAxis, + yAxis, - className, - tickCount = { - x: undefined, - y: 10, - }, - legend, - showTooltip = true, - } = props; - // states - const [activePoint, setActivePoint] = useState(null); - const [activeLegend, setActiveLegend] = useState(null); - // derived values - const itemKeys = useMemo( - () => Array.from(scatterPoints, point => point.key), - [scatterPoints] - ); - const itemLabels = useMemo( - () => Object.fromEntries(scatterPoints.map(point => [point.key, point.label])), - [scatterPoints] - ); + className, + tickCount = { + x: undefined, + y: 10, + }, + legend, + showTooltip = true, + } = props; + // states + const [activePoint, setActivePoint] = useState(null); + const [activeLegend, setActiveLegend] = useState(null); + // derived values + const itemKeys = useMemo(() => Array.from(scatterPoints, (point) => point.key), [scatterPoints]); + const itemLabels = useMemo( + () => Object.fromEntries(scatterPoints.map((point) => [point.key, point.label])), + [scatterPoints] + ); - const itemDotColors = useMemo( - () => Object.fromEntries(scatterPoints.map(point => [point.key, point.fill])), - [scatterPoints] - ); - const renderPoints = useMemo( - () => - scatterPoints.map((point) => ( - setActivePoint(point.key)} - onMouseLeave={() => setActivePoint(null)} - /> - )), - [activeLegend, scatterPoints] - ); + const itemDotColors = useMemo( + () => Object.fromEntries(scatterPoints.map((point) => [point.key, point.fill])), + [scatterPoints] + ); + const renderPoints = useMemo( + () => + scatterPoints.map((point) => ( + setActivePoint(point.key)} + onMouseLeave={() => setActivePoint(null)} + /> + )), + [activeLegend, scatterPoints] + ); - return ( -
- - - - } - tickLine={false} - axisLine={false} - label={ - xAxis.label && { - value: xAxis.label, - dy: 28, - className: AXIS_LABEL_CLASSNAME, - } - } - tickCount={tickCount.x} - /> - } - tickCount={tickCount.y} - allowDecimals={!!yAxis.allowDecimals} - /> - {legend && ( - // @ts-expect-error recharts types are not up to date - setActiveLegend(payload.value)} - onMouseLeave={() => setActiveLegend(null)} - formatter={(value) => itemLabels[value]} - {...getLegendProps(legend)} - /> - )} - {showTooltip && ( - ( - - )} - /> - )} - {renderPoints} - - -
- ); + return ( +
+ + + + } + tickLine={false} + axisLine={false} + label={ + xAxis.label && { + value: xAxis.label, + dy: 28, + className: AXIS_LABEL_CLASSNAME, + } + } + tickCount={tickCount.x} + /> + } + tickCount={tickCount.y} + allowDecimals={!!yAxis.allowDecimals} + /> + {legend && ( + // @ts-expect-error recharts types are not up to date + setActiveLegend(payload.value)} + onMouseLeave={() => setActiveLegend(null)} + formatter={(value) => itemLabels[value]} + {...getLegendProps(legend)} + /> + )} + {showTooltip && ( + ( + + )} + /> + )} + {renderPoints} + + +
+ ); }); -ScatterChart.displayName = "ScatterChart"; \ No newline at end of file +ScatterChart.displayName = "ScatterChart"; diff --git a/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx b/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx index cfd5a0aa0b2..beeb23065cd 100644 --- a/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx +++ b/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx @@ -36,7 +36,7 @@ const WorkItemsInsightTable = observer(() => { ); // derived values const columnsLabels: Record = { - backlog_work_items: t("workspace_projects.statex.backlog"), + backlog_work_items: t("workspace_projects.state.backlog"), started_work_items: t("workspace_projects.state.started"), un_started_work_items: t("workspace_projects.state.unstarted"), completed_work_items: t("workspace_projects.state.completed"), From edb6834a124e43a5d3c8f034cdc4bc2fd1333707 Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Sun, 11 May 2025 15:51:02 +0530 Subject: [PATCH 61/69] optimized charts --- .../propel/src/charts/area-chart/root.tsx | 15 ++++-- packages/propel/src/charts/bar-chart/root.tsx | 21 +++++--- .../propel/src/charts/line-chart/root.tsx | 20 +++++--- .../propel/src/charts/radar-chart/root.tsx | 48 ++++++++++++------- .../propel/src/charts/scatter-chart/root.tsx | 24 ++++++---- 5 files changed, 86 insertions(+), 42 deletions(-) diff --git a/packages/propel/src/charts/area-chart/root.tsx b/packages/propel/src/charts/area-chart/root.tsx index 713f546d4de..b90de27cfea 100644 --- a/packages/propel/src/charts/area-chart/root.tsx +++ b/packages/propel/src/charts/area-chart/root.tsx @@ -31,11 +31,20 @@ export const AreaChart = React.memo((props: const [activeLegend, setActiveLegend] = useState(null); // derived values - const itemKeys = useMemo(() => Array.from(areas, (area) => area.key), [areas]); + const { itemKeys, itemLabels, itemDotColors } = useMemo(() => { + const keys: string[] = []; + const labels: Record = {}; + const colors: Record = {}; - const itemLabels = useMemo(() => Object.fromEntries(areas.map((area) => [area.key, area.label])), [areas]); + for (const area of areas) { + keys.push(area.key); + labels[area.key] = area.label; + colors[area.key] = area.fill; + } + + return { itemKeys: keys, itemLabels: labels, itemDotColors: colors }; + }, [areas]); - const itemDotColors = useMemo(() => Object.fromEntries(areas.map((area) => [area.key, area.fill])), [areas]); const renderAreas = useMemo( () => areas.map((area) => ( diff --git a/packages/propel/src/charts/bar-chart/root.tsx b/packages/propel/src/charts/bar-chart/root.tsx index dc454152d43..e3dbe1d8c26 100644 --- a/packages/propel/src/charts/bar-chart/root.tsx +++ b/packages/propel/src/charts/bar-chart/root.tsx @@ -40,13 +40,22 @@ export const BarChart = React.memo((props: T // states const [activeBar, setActiveBar] = useState(null); const [activeLegend, setActiveLegend] = useState(null); + // derived values - const stackKeys = useMemo(() => bars.map((bar) => bar.key), [bars]); - const stackLabels: Record = useMemo( - () => bars.reduce((acc, bar) => ({ ...acc, [bar.key]: bar.label }), {}), - [bars] - ); - const stackDotColors = useMemo(() => bars.reduce((acc, bar) => ({ ...acc, [bar.key]: bar.fill }), {}), [bars]); + const { stackKeys, stackLabels, stackDotColors } = useMemo(() => { + const keys: string[] = []; + const labels: Record = {}; + const colors: Record = {}; + + for (const bar of bars) { + keys.push(bar.key); + labels[bar.key] = bar.label; + // For tooltip, we need a string color. If fill is a function, use a default color + colors[bar.key] = typeof bar.fill === "function" ? "#000000" : bar.fill; + } + + return { stackKeys: keys, stackLabels: labels, stackDotColors: colors }; + }, [bars]); const renderBars = useMemo( () => diff --git a/packages/propel/src/charts/line-chart/root.tsx b/packages/propel/src/charts/line-chart/root.tsx index 6812797b792..f3b2ef72c47 100644 --- a/packages/propel/src/charts/line-chart/root.tsx +++ b/packages/propel/src/charts/line-chart/root.tsx @@ -38,13 +38,21 @@ export const LineChart = React.memo((props: // states const [activeLine, setActiveLine] = useState(null); const [activeLegend, setActiveLegend] = useState(null); + // derived values - const itemKeys = useMemo(() => lines.map((line) => line.key), [lines]); - const itemLabels: Record = useMemo( - () => lines.reduce((acc, line) => ({ ...acc, [line.key]: line.label }), {}), - [lines] - ); - const itemDotColors = useMemo(() => lines.reduce((acc, line) => ({ ...acc, [line.key]: line.stroke }), {}), [lines]); + const { itemKeys, itemLabels, itemDotColors } = useMemo(() => { + const keys: string[] = []; + const labels: Record = {}; + const colors: Record = {}; + + for (const line of lines) { + keys.push(line.key); + labels[line.key] = line.label; + colors[line.key] = line.stroke; + } + + return { itemKeys: keys, itemLabels: labels, itemDotColors: colors }; + }, [lines]); const renderLines = useMemo( () => diff --git a/packages/propel/src/charts/radar-chart/root.tsx b/packages/propel/src/charts/radar-chart/root.tsx index 4036e08cf10..b8a1b95d716 100644 --- a/packages/propel/src/charts/radar-chart/root.tsx +++ b/packages/propel/src/charts/radar-chart/root.tsx @@ -1,9 +1,17 @@ -import { useMemo, useState } from 'react' -import { PolarGrid, Radar, RadarChart as CoreRadarChart, ResponsiveContainer, PolarAngleAxis, Tooltip, Legend } from 'recharts'; -import { TRadarChartProps } from '@plane/types'; -import { getLegendProps } from '../components/legend'; -import { CustomRadarAxisTick } from '../components/tick'; -import { CustomTooltip } from '../components/tooltip'; +import { useMemo, useState } from "react"; +import { + PolarGrid, + Radar, + RadarChart as CoreRadarChart, + ResponsiveContainer, + PolarAngleAxis, + Tooltip, + Legend, +} from "recharts"; +import { TRadarChartProps } from "@plane/types"; +import { getLegendProps } from "../components/legend"; +import { CustomRadarAxisTick } from "../components/tick"; +import { CustomTooltip } from "../components/tooltip"; const RadarChart = (props: TRadarChartProps) => { const { data, radars, margin, showTooltip, legend, className, angleAxis } = props; @@ -12,18 +20,25 @@ const RadarChart = (props: TRadarChartProps< const [, setActiveIndex] = useState(null); const [activeLegend, setActiveLegend] = useState(null); - const itemKeys = useMemo(() => radars.map((radar) => radar.key), [radars]); - const itemLabels = useMemo(() => radars.reduce((acc, radar) => ({ ...acc, [radar.key]: radar.name }), {}), [radars]); - const itemDotColors = useMemo(() => radars.reduce((acc, radar) => ({ ...acc, [radar.key]: radar.stroke }), {}), [radars]); + const { itemKeys, itemLabels, itemDotColors } = useMemo(() => { + const keys: string[] = []; + const labels: Record = {}; + const colors: Record = {}; + + for (const radar of radars) { + keys.push(radar.key); + labels[radar.key] = radar.name; + colors[radar.key] = radar.stroke ?? radar.fill ?? "#000000"; + } + return { itemKeys: keys, itemLabels: labels, itemDotColors: colors }; + }, [radars]); return (
- - } - /> + + } /> {showTooltip && ( (props: TRadarChartProps< dot={radar.dot} /> ))} -
- ) -} + ); +}; -export { RadarChart }; \ No newline at end of file +export { RadarChart }; diff --git a/packages/propel/src/charts/scatter-chart/root.tsx b/packages/propel/src/charts/scatter-chart/root.tsx index 69bfaee059b..73ec24bd649 100644 --- a/packages/propel/src/charts/scatter-chart/root.tsx +++ b/packages/propel/src/charts/scatter-chart/root.tsx @@ -40,17 +40,21 @@ export const ScatterChart = React.memo((prop // states const [activePoint, setActivePoint] = useState(null); const [activeLegend, setActiveLegend] = useState(null); - // derived values - const itemKeys = useMemo(() => Array.from(scatterPoints, (point) => point.key), [scatterPoints]); - const itemLabels = useMemo( - () => Object.fromEntries(scatterPoints.map((point) => [point.key, point.label])), - [scatterPoints] - ); - const itemDotColors = useMemo( - () => Object.fromEntries(scatterPoints.map((point) => [point.key, point.fill])), - [scatterPoints] - ); + //derived values + const { itemKeys, itemLabels, itemDotColors } = useMemo(() => { + const keys: string[] = []; + const labels: Record = {}; + const colors: Record = {}; + + for (const point of scatterPoints) { + keys.push(point.key); + labels[point.key] = point.label; + colors[point.key] = point.fill; + } + return { itemKeys: keys, itemLabels: labels, itemDotColors: colors }; + }, [scatterPoints]); + const renderPoints = useMemo( () => scatterPoints.map((point) => ( From ba62fc2ca63dd17a43653eb8a967c8b54dfe6228 Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Mon, 12 May 2025 15:04:01 +0530 Subject: [PATCH 62/69] modified relevant variables to observable.ref --- web/core/store/analytics-v2.store.ts | 101 +++++++++++++-------------- 1 file changed, 50 insertions(+), 51 deletions(-) diff --git a/web/core/store/analytics-v2.store.ts b/web/core/store/analytics-v2.store.ts index 8684a01eeaf..dbbc85f8338 100644 --- a/web/core/store/analytics-v2.store.ts +++ b/web/core/store/analytics-v2.store.ts @@ -3,67 +3,66 @@ import { ANALYTICS_V2_DURATION_FILTER_OPTIONS, PROJECT_CREATED_AT_FILTER_OPTIONS import { TAnalyticsTabsV2Base } from "@plane/types"; import { CoreRootStore } from "./root.store"; - -type DurationType = typeof PROJECT_CREATED_AT_FILTER_OPTIONS[number]['value'] +type DurationType = (typeof PROJECT_CREATED_AT_FILTER_OPTIONS)[number]["value"]; export interface IAnalyticsStoreV2 { - //observables - currentTab: TAnalyticsTabsV2Base - selectedProjects: string[] - selectedDuration: DurationType, + //observables + currentTab: TAnalyticsTabsV2Base; + selectedProjects: string[]; + selectedDuration: DurationType; - //computed - selectedDurationLabel: string | null, + //computed + selectedDurationLabel: string | null; - //actions - updateSelectedProjects: (projects: string[]) => void, - updateSelectedDuration: (duration: DurationType) => void, + //actions + updateSelectedProjects: (projects: string[]) => void; + updateSelectedDuration: (duration: DurationType) => void; } export class AnalyticsStoreV2 implements IAnalyticsStoreV2 { - //observables - currentTab: TAnalyticsTabsV2Base = "overview"; - selectedProjects: string[] = []; - selectedDuration: typeof ANALYTICS_V2_DURATION_FILTER_OPTIONS[number]['value'] = "last_30_days"; + //observables + currentTab: TAnalyticsTabsV2Base = "overview"; + selectedProjects: string[] = []; + selectedDuration: (typeof ANALYTICS_V2_DURATION_FILTER_OPTIONS)[number]["value"] = "last_30_days"; - constructor(_rootStore: CoreRootStore) { - makeObservable(this, { - // observables - currentTab: observable, - selectedDuration: observable, - selectedProjects: observable, - // computed - selectedDurationLabel: computed, - // actions - updateSelectedProjects: action, - updateSelectedDuration: action - }) - } + constructor(_rootStore: CoreRootStore) { + makeObservable(this, { + // observables + currentTab: observable.ref, + selectedDuration: observable.ref, + selectedProjects: observable.ref, + // computed + selectedDurationLabel: computed, + // actions + updateSelectedProjects: action, + updateSelectedDuration: action, + }); + } - get selectedDurationLabel() { - return ANALYTICS_V2_DURATION_FILTER_OPTIONS.find(item => item.value === this.selectedDuration)?.name ?? null - } + get selectedDurationLabel() { + return ANALYTICS_V2_DURATION_FILTER_OPTIONS.find((item) => item.value === this.selectedDuration)?.name ?? null; + } - updateSelectedProjects = (projects: string[]) => { - const initialState = this.selectedProjects; - try { - runInAction(() => { - this.selectedProjects = projects; - }) - } catch (error) { - console.error("Failed to update selected project"); - throw error; - } + updateSelectedProjects = (projects: string[]) => { + const initialState = this.selectedProjects; + try { + runInAction(() => { + this.selectedProjects = projects; + }); + } catch (error) { + console.error("Failed to update selected project"); + throw error; } + }; - updateSelectedDuration = (duration: DurationType) => { - try { - runInAction(() => { - this.selectedDuration = duration; - }) - } catch (error) { - console.error("Failed to update selected duration"); - throw error; - } + updateSelectedDuration = (duration: DurationType) => { + try { + runInAction(() => { + this.selectedDuration = duration; + }); + } catch (error) { + console.error("Failed to update selected duration"); + throw error; } -} \ No newline at end of file + }; +} From 4de46e8eb44ba531787d5ec21b904114c752c627 Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Mon, 12 May 2025 15:29:34 +0530 Subject: [PATCH 63/69] fixed the duration type --- web/core/store/analytics-v2.store.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/web/core/store/analytics-v2.store.ts b/web/core/store/analytics-v2.store.ts index dbbc85f8338..bf8f91a72f0 100644 --- a/web/core/store/analytics-v2.store.ts +++ b/web/core/store/analytics-v2.store.ts @@ -1,9 +1,9 @@ import { action, computed, makeObservable, observable, runInAction } from "mobx"; -import { ANALYTICS_V2_DURATION_FILTER_OPTIONS, PROJECT_CREATED_AT_FILTER_OPTIONS } from "@plane/constants"; +import { ANALYTICS_V2_DURATION_FILTER_OPTIONS } from "@plane/constants"; import { TAnalyticsTabsV2Base } from "@plane/types"; import { CoreRootStore } from "./root.store"; -type DurationType = (typeof PROJECT_CREATED_AT_FILTER_OPTIONS)[number]["value"]; +type DurationType = (typeof ANALYTICS_V2_DURATION_FILTER_OPTIONS)[number]["value"]; export interface IAnalyticsStoreV2 { //observables @@ -12,7 +12,7 @@ export interface IAnalyticsStoreV2 { selectedDuration: DurationType; //computed - selectedDurationLabel: string | null; + selectedDurationLabel: DurationType | null; //actions updateSelectedProjects: (projects: string[]) => void; @@ -22,8 +22,8 @@ export interface IAnalyticsStoreV2 { export class AnalyticsStoreV2 implements IAnalyticsStoreV2 { //observables currentTab: TAnalyticsTabsV2Base = "overview"; - selectedProjects: string[] = []; - selectedDuration: (typeof ANALYTICS_V2_DURATION_FILTER_OPTIONS)[number]["value"] = "last_30_days"; + selectedProjects: DurationType[] = []; + selectedDuration: DurationType = "last_30_days"; constructor(_rootStore: CoreRootStore) { makeObservable(this, { From b90792de9686fe552cd27af5d392d4bbae6263ee Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Mon, 12 May 2025 16:02:41 +0530 Subject: [PATCH 64/69] optimized some code --- .../analytics-v2/work-items/utils.ts | 23 ++-- web/core/services/analytics-v2.service.ts | 102 +++++++++--------- 2 files changed, 65 insertions(+), 60 deletions(-) diff --git a/web/core/components/analytics-v2/work-items/utils.ts b/web/core/components/analytics-v2/work-items/utils.ts index 1d3ffddfa94..08fc855c279 100644 --- a/web/core/components/analytics-v2/work-items/utils.ts +++ b/web/core/components/analytics-v2/work-items/utils.ts @@ -21,19 +21,24 @@ export const generateBarColor = ( value === "urgent" ? "#ef4444" : value === "high" - ? "#f97316" - : value === "medium" - ? "#eab308" - : value === "low" - ? "#22c55e" - : "#ced4da"; + ? "#f97316" + : value === "medium" + ? "#eab308" + : value === "low" + ? "#22c55e" + : "#ced4da"; } // State if (params.x_axis === ChartXAxisProperty.STATES) { - const state = workspaceStates?.find((s) => s.id === value); - if (state) { - color = state.color; + if (workspaceStates && workspaceStates.length > 0) { + const state = workspaceStates.find((s) => s.id === value); + if (state) { + color = state.color; + } else { + const index = Math.abs(value.split("").reduce((acc, char) => acc + char.charCodeAt(0), 0)) % baseColors.length; + color = baseColors[index]; + } } } diff --git a/web/core/services/analytics-v2.service.ts b/web/core/services/analytics-v2.service.ts index 9c8d0139045..87257cbc6b0 100644 --- a/web/core/services/analytics-v2.service.ts +++ b/web/core/services/analytics-v2.service.ts @@ -2,59 +2,59 @@ import { API_BASE_URL } from "@plane/constants"; import { IAnalyticsResponseV2, TAnalyticsTabsV2Base, TAnalyticsGraphsV2Base } from "@plane/types"; import { APIService } from "./api.service"; - export class AnalyticsV2Service extends APIService { - constructor() { - super(API_BASE_URL) - } - - async getAdvanceAnalytics(workspaceSlug: string, tab: TAnalyticsTabsV2Base, params?: Record): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/advance-analytics/`, { - params: { - tab, - ...params - } - }) - .then((res) => res?.data) - .catch((err) => { - throw err?.response?.data; - }) - } + constructor() { + super(API_BASE_URL); + } - async getAdvanceAnalyticsStats(workspaceSlug: string, tab: Exclude, params?: Record): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/advance-analytics-stats/`, { - params: { - type: tab, - ...params - } - }) - .then((res) => res?.data) - .catch((err) => { - throw err?.response?.data; - }) - } + async getAdvanceAnalytics( + workspaceSlug: string, + tab: TAnalyticsTabsV2Base, + params?: Record + ): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/advance-analytics/`, { + params: { + tab, + ...params, + }, + }) + .then((res) => res?.data) + .catch((err) => { + throw err?.response?.data; + }); + } - async getAdvanceAnalyticsCharts(workspaceSlug: string, tab: TAnalyticsGraphsV2Base, params?: Record): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/advance-analytics-charts/`, { - params: { - type: tab, - ...params - } - }) - .then((res) => res?.data) - .catch((err) => { - throw err?.response?.data; - }) - } + async getAdvanceAnalyticsStats( + workspaceSlug: string, + tab: Exclude, + params?: Record + ): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/advance-analytics-stats/`, { + params: { + type: tab, + ...params, + }, + }) + .then((res) => res?.data) + .catch((err) => { + throw err?.response?.data; + }); + } - async exportAnalytics(workspaceSlug: string, params?: Record): Promise { - return this.post(`/api/workspaces/${workspaceSlug}/advance-analytics-export/`, { - params - }) - .then((res) => res?.data) - .catch((err) => { - throw err?.response?.data; - }) - } + async getAdvanceAnalyticsCharts( + workspaceSlug: string, + tab: TAnalyticsGraphsV2Base, + params?: Record + ): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/advance-analytics-charts/`, { + params: { + type: tab, + ...params, + }, + }) + .then((res) => res?.data) + .catch((err) => { + throw err?.response?.data; + }); + } } - From ed7a9a6352a861652835c92eec78558c97684559 Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Mon, 12 May 2025 17:47:42 +0530 Subject: [PATCH 65/69] updated query key in project-insight --- .../analytics-v2/overview/project-insights.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/web/core/components/analytics-v2/overview/project-insights.tsx b/web/core/components/analytics-v2/overview/project-insights.tsx index 93b2a8ca3f2..a767cf47657 100644 --- a/web/core/components/analytics-v2/overview/project-insights.tsx +++ b/web/core/components/analytics-v2/overview/project-insights.tsx @@ -30,11 +30,13 @@ const ProjectInsights = observer(() => { const { selectedDuration, selectedDurationLabel, selectedProjects } = useAnalyticsV2(); const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics-v2/empty-chart-radar" }); - const { data: projectInsightsData, isLoading: isLoadingProjectInsight } = useSWR(`radar-chart-${workspaceSlug}`, () => - analyticsV2Service.getAdvanceAnalyticsCharts[]>(workspaceSlug, "projects", { - created_at: selectedDuration, - ...(selectedProjects?.length > 0 && { project_ids: selectedProjects?.join(",") }), - }) + const { data: projectInsightsData, isLoading: isLoadingProjectInsight } = useSWR( + `radar-chart-${workspaceSlug}-${selectedDuration}-${selectedProjects}`, + () => + analyticsV2Service.getAdvanceAnalyticsCharts[]>(workspaceSlug, "projects", { + date_filter: selectedDuration, + ...(selectedProjects?.length > 0 && { project_ids: selectedProjects?.join(",") }), + }) ); return ( From aa908547f5e1d309347cf9a5a7efa127861ef850 Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Mon, 12 May 2025 17:49:21 +0530 Subject: [PATCH 66/69] updated query key in project-insight --- .../components/analytics-v2/work-items/created-vs-resolved.tsx | 2 +- web/core/components/analytics-v2/work-items/priority-chart.tsx | 2 +- .../analytics-v2/work-items/workitems-insight-table.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/web/core/components/analytics-v2/work-items/created-vs-resolved.tsx b/web/core/components/analytics-v2/work-items/created-vs-resolved.tsx index 1c8ac63cd76..bd4673f4650 100644 --- a/web/core/components/analytics-v2/work-items/created-vs-resolved.tsx +++ b/web/core/components/analytics-v2/work-items/created-vs-resolved.tsx @@ -25,7 +25,7 @@ const CreatedVsResolved = observer(() => { const workspaceSlug = params.workspaceSlug as string; const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics-v2/empty-chart-area" }); const { data: createdVsResolvedData, isLoading: isCreatedVsResolvedLoading } = useSWR( - `created-vs-resolved-${workspaceSlug}-${selectedDuration}`, + `created-vs-resolved-${workspaceSlug}-${selectedDuration}-${selectedProjects}`, () => analyticsV2Service.getAdvanceAnalyticsCharts(workspaceSlug, "work-items", { date_filter: selectedDuration, diff --git a/web/core/components/analytics-v2/work-items/priority-chart.tsx b/web/core/components/analytics-v2/work-items/priority-chart.tsx index a51c7e78990..e97c4adee16 100644 --- a/web/core/components/analytics-v2/work-items/priority-chart.tsx +++ b/web/core/components/analytics-v2/work-items/priority-chart.tsx @@ -54,7 +54,7 @@ const PriorityChart = observer((props: Props) => { const workspaceSlug = params.workspaceSlug as string; const { data: priorityChartData, isLoading: priorityChartLoading } = useSWR( - `customized-insights-chart-${workspaceSlug}-${selectedDuration}-${props.x_axis}-${props.y_axis}-${props.group_by}`, + `customized-insights-chart-${workspaceSlug}-${selectedDuration}-${selectedProjects}-${props.x_axis}-${props.y_axis}-${props.group_by}`, () => analyticsV2Service.getAdvanceAnalyticsCharts(workspaceSlug, "custom-work-items", { date_filter: selectedDuration, diff --git a/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx b/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx index beeb23065cd..cd3e7ae4e40 100644 --- a/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx +++ b/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx @@ -27,7 +27,7 @@ const WorkItemsInsightTable = observer(() => { const { getProjectById } = useProject(); const { selectedDuration, selectedProjects } = useAnalyticsV2(); const { data: workItemsData, isLoading } = useSWR( - `insights-table-work-items-${selectedDuration}-${selectedProjects}`, + `insights-table-work-items-${workspaceSlug}-${selectedDuration}-${selectedProjects}`, () => analyticsV2Service.getAdvanceAnalyticsStats(workspaceSlug, "work-items", { date_filter: selectedDuration, From 97c8d79498501f7b5c8bb356ff4aecfe1a7f7847 Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Mon, 12 May 2025 19:18:09 +0530 Subject: [PATCH 67/69] updated formatting --- web/app/profile/sidebar.tsx | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/web/app/profile/sidebar.tsx b/web/app/profile/sidebar.tsx index f869fde3dd7..59e3daa4855 100644 --- a/web/app/profile/sidebar.tsx +++ b/web/app/profile/sidebar.tsx @@ -134,8 +134,9 @@ export const ProfileLayoutSidebar = observer(() => {
@@ -194,17 +195,20 @@ export const ProfileLayoutSidebar = observer(() => { {workspace?.logo_url && workspace.logo_url !== "" ? ( { isMobile={isMobile} >
{} {!sidebarCollapsed && t(link.i18n_label)} @@ -248,8 +253,9 @@ export const ProfileLayoutSidebar = observer(() => {
); -}); \ No newline at end of file +}); From 56975b22aeadfb665cd45e0a55d71e26a9e17d5f Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Mon, 12 May 2025 20:07:09 +0530 Subject: [PATCH 68/69] chore: replaced analytics route with new one and done some optimizations --- packages/constants/src/workspace.ts | 2 +- .../propel/src/charts/scatter-chart/root.tsx | 1 - .../header.tsx | 0 .../layout.tsx | 3 +- .../{analytics-v2 => analytics-old}/page.tsx | 72 ++++++++++--------- .../(projects)/analytics/layout.tsx | 3 +- .../(projects)/analytics/page.tsx | 72 +++++++++---------- .../components/analytics-v2/insight-card.tsx | 2 +- .../analytics-v2/insight-table/data-table.tsx | 12 ++-- .../analytics-v2/total-insights.tsx | 2 +- .../work-items/priority-chart.tsx | 2 +- .../analytics-v2/work-items/utils.ts | 11 +-- .../empty-state/detailed-empty-state-root.tsx | 4 +- .../workspace/sidebar/workspace-menu.tsx | 2 +- 14 files changed, 91 insertions(+), 97 deletions(-) rename web/app/[workspaceSlug]/(projects)/{analytics-v2 => analytics-old}/header.tsx (100%) rename web/app/[workspaceSlug]/(projects)/{analytics-v2 => analytics-old}/layout.tsx (90%) rename web/app/[workspaceSlug]/(projects)/{analytics-v2 => analytics-old}/page.tsx (54%) diff --git a/packages/constants/src/workspace.ts b/packages/constants/src/workspace.ts index f1e87507878..c1c60f392a5 100644 --- a/packages/constants/src/workspace.ts +++ b/packages/constants/src/workspace.ts @@ -278,7 +278,7 @@ export const WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS: Record { - const router = useRouter(); const searchParams = useSearchParams(); + const analytics_tab = searchParams.get("analytics_tab"); // plane imports const { t } = useTranslation(); // store hooks @@ -40,38 +40,44 @@ const AnalyticsPage = observer(() => { EUserPermissionsLevel.WORKSPACE ); - const tabs = useMemo( - () => - ANALYTICS_TABS.map((tab) => ({ - key: tab.key, - label: t(tab.i18nKey), - content: , - onClick: () => { - router.push(`?tab=${tab.key}`); - }, - })), - [router, t] - ); - const defaultTab = searchParams.get("tab") || ANALYTICS_TABS[0].key; - + // TODO: refactor loader implementation return ( <> {workspaceProjectIds && ( <> {workspaceProjectIds.length > 0 || loader === "init-loader" ? ( -
- } - /> +
+ +
+ + {ANALYTICS_TABS.map((tab) => ( + + {({ selected }) => ( + + )} + + ))} + +
+ + + + + + + + +
) : ( { + const router = useRouter(); const searchParams = useSearchParams(); - const analytics_tab = searchParams.get("analytics_tab"); // plane imports const { t } = useTranslation(); // store hooks @@ -40,44 +40,38 @@ const AnalyticsPage = observer(() => { EUserPermissionsLevel.WORKSPACE ); - // TODO: refactor loader implementation + const tabs = useMemo( + () => + ANALYTICS_TABS.map((tab) => ({ + key: tab.key, + label: t(tab.i18nKey), + content: , + onClick: () => { + router.push(`?tab=${tab.key}`); + }, + })), + [router, t] + ); + const defaultTab = searchParams.get("tab") || ANALYTICS_TABS[0].key; + return ( <> {workspaceProjectIds && ( <> {workspaceProjectIds.length > 0 || loader === "init-loader" ? ( -
- -
- - {ANALYTICS_TABS.map((tab) => ( - - {({ selected }) => ( - - )} - - ))} - -
- - - - - - - - -
+
+ } + />
) : ( { const percentage = useMemo(() => { if (count != null && filter_count != null) { const result = ((count - filter_count) / count) * 100; - const isFiniteAndNotNaNOrZero = isFinite(result) && !isNaN(result) && result !== 0; + const isFiniteAndNotNaNOrZero = Number.isFinite(result) && !Number.isNaN(result) && result !== 0; return isFiniteAndNotNaNOrZero ? result : null; } return null; diff --git a/web/core/components/analytics-v2/insight-table/data-table.tsx b/web/core/components/analytics-v2/insight-table/data-table.tsx index 6b2053a1523..c811c92659d 100644 --- a/web/core/components/analytics-v2/insight-table/data-table.tsx +++ b/web/core/components/analytics-v2/insight-table/data-table.tsx @@ -99,9 +99,10 @@ export function DataTable({ columns, data, searchPlaceholder, act className="w-full max-w-[234px] border-none bg-transparent text-sm text-custom-text-100 placeholder:text-custom-text-400 focus:outline-none" placeholder="Search" value={table.getColumn(table.getHeaderGroups()?.[0].headers[0].id)?.getFilterValue() as string} - onChange={(e) => - table.getColumn(table.getHeaderGroups()?.[0].headers[0].id)?.setFilterValue(e.target.value) - } + onChange={(e) => { + const columnId = table.getHeaderGroups()?.[0]?.headers?.[0]?.id; + if (columnId) table.getColumn(columnId)?.setFilterValue(e.target.value); + }} onKeyDown={(e) => { if (e.key === "Enter") { setIsSearchOpen(true); @@ -113,7 +114,10 @@ export function DataTable({ columns, data, searchPlaceholder, act type="button" className="grid place-items-center" onClick={() => { - table.getColumn(table.getHeaderGroups()?.[0].headers[0].id)?.setFilterValue(""); + const columnId = table.getHeaderGroups()?.[0]?.headers?.[0]?.id; + if (columnId) { + table.getColumn(columnId)?.setFilterValue(""); + } setIsSearchOpen(false); }} > diff --git a/web/core/components/analytics-v2/total-insights.tsx b/web/core/components/analytics-v2/total-insights.tsx index e911ac30f3d..ac8914e1137 100644 --- a/web/core/components/analytics-v2/total-insights.tsx +++ b/web/core/components/analytics-v2/total-insights.tsx @@ -41,7 +41,7 @@ const TotalInsights: React.FC<{ analyticsType: TAnalyticsTabsV2Base; peekView?: : "grid-cols-2" )} > - {insightsFields[analyticsType].map((item: string) => ( + {insightsFields[analyticsType]?.map((item: string) => ( { xAxis={{ key: "name", label: xAxisLabel.replace("_", " "), - dy: 0, + dy: 30, }} yAxis={{ key: "count", diff --git a/web/core/components/analytics-v2/work-items/utils.ts b/web/core/components/analytics-v2/work-items/utils.ts index 08fc855c279..6e0b47a440d 100644 --- a/web/core/components/analytics-v2/work-items/utils.ts +++ b/web/core/components/analytics-v2/work-items/utils.ts @@ -9,11 +9,12 @@ interface ParamsProps { } export const generateBarColor = ( - value: string, + value: string | null | undefined, params: ParamsProps, baseColors: string[], workspaceStates?: IState[] ): string => { + if (!value) return baseColors[0]; let color = baseColors[0]; // Priority if (params.x_axis === ChartXAxisProperty.PRIORITY) { @@ -42,13 +43,5 @@ export const generateBarColor = ( } } - // Label - if (params.x_axis === ChartXAxisProperty.LABELS) { - const label = workspaceStates?.find((l) => l.id === value); - if (label) { - color = label.color; - } - } - return color; }; diff --git a/web/core/components/empty-state/detailed-empty-state-root.tsx b/web/core/components/empty-state/detailed-empty-state-root.tsx index 4ae97e839c4..887a1eb4228 100644 --- a/web/core/components/empty-state/detailed-empty-state-root.tsx +++ b/web/core/components/empty-state/detailed-empty-state-root.tsx @@ -85,9 +85,7 @@ export const DetailedEmptyState: React.FC = observer((props) => { {description &&

{description}

}
- {assetPath && ( - {title} - )} + {assetPath && {title}} {hasButtons && (
diff --git a/web/core/components/workspace/sidebar/workspace-menu.tsx b/web/core/components/workspace/sidebar/workspace-menu.tsx index dd2ba54990a..2b2fa90b317 100644 --- a/web/core/components/workspace/sidebar/workspace-menu.tsx +++ b/web/core/components/workspace/sidebar/workspace-menu.tsx @@ -55,7 +55,7 @@ export const SidebarWorkspaceMenu = observer(() => { { key: "analytics", labelTranslationKey: "sidebar.analytics", - href: `/${workspaceSlug}/analytics-v2/`, + href: `/${workspaceSlug}/analytics/`, access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER], Icon: BarChart2, }, From df46d1fe42588d98b1c8ce4e93908afdd2301323 Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Mon, 12 May 2025 20:31:53 +0530 Subject: [PATCH 69/69] removed the old analytics --- .../(projects)/analytics-old/header.tsx | 72 ------------ .../(projects)/analytics-old/layout.tsx | 13 --- .../(projects)/analytics-old/page.tsx | 107 ------------------ 3 files changed, 192 deletions(-) delete mode 100644 web/app/[workspaceSlug]/(projects)/analytics-old/header.tsx delete mode 100644 web/app/[workspaceSlug]/(projects)/analytics-old/layout.tsx delete mode 100644 web/app/[workspaceSlug]/(projects)/analytics-old/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/analytics-old/header.tsx b/web/app/[workspaceSlug]/(projects)/analytics-old/header.tsx deleted file mode 100644 index 2c3247bd7f6..00000000000 --- a/web/app/[workspaceSlug]/(projects)/analytics-old/header.tsx +++ /dev/null @@ -1,72 +0,0 @@ -"use client"; - -import { useEffect } from "react"; -import { observer } from "mobx-react"; -import { useSearchParams } from "next/navigation"; -import { BarChart2, PanelRight } from "lucide-react"; -import { useTranslation } from "@plane/i18n"; -// ui -import { Breadcrumbs, Header } from "@plane/ui"; -// components -import { BreadcrumbLink } from "@/components/common"; -// helpers -import { cn } from "@/helpers/common.helper"; -// hooks -import { useAppTheme } from "@/hooks/store"; -export const WorkspaceAnalyticsHeader = observer(() => { - const { t } = useTranslation(); - const searchParams = useSearchParams(); - const analytics_tab = searchParams.get("analytics_tab"); - // store hooks - const { workspaceAnalyticsSidebarCollapsed, toggleWorkspaceAnalyticsSidebar } = useAppTheme(); - - useEffect(() => { - const handleToggleWorkspaceAnalyticsSidebar = () => { - if (window && window.innerWidth < 768) { - toggleWorkspaceAnalyticsSidebar(true); - } - if (window && workspaceAnalyticsSidebarCollapsed && window.innerWidth >= 768) { - toggleWorkspaceAnalyticsSidebar(false); - } - }; - - window.addEventListener("resize", handleToggleWorkspaceAnalyticsSidebar); - handleToggleWorkspaceAnalyticsSidebar(); - return () => window.removeEventListener("resize", handleToggleWorkspaceAnalyticsSidebar); - }, [toggleWorkspaceAnalyticsSidebar, workspaceAnalyticsSidebarCollapsed]); - - return ( -
- - - } - /> - } - /> - - {analytics_tab === "custom" ? ( - - ) : ( - <> - )} - -
- ); -}); diff --git a/web/app/[workspaceSlug]/(projects)/analytics-old/layout.tsx b/web/app/[workspaceSlug]/(projects)/analytics-old/layout.tsx deleted file mode 100644 index 8dfc8b3b0f3..00000000000 --- a/web/app/[workspaceSlug]/(projects)/analytics-old/layout.tsx +++ /dev/null @@ -1,13 +0,0 @@ -"use client"; - -import { AppHeader, ContentWrapper } from "@/components/core"; -import { WorkspaceAnalyticsHeader } from "./header"; - -export default function WorkspaceAnalyticsLayout({ children }: { children: React.ReactNode }) { - return ( - <> - } /> - {children} - - ); -} diff --git a/web/app/[workspaceSlug]/(projects)/analytics-old/page.tsx b/web/app/[workspaceSlug]/(projects)/analytics-old/page.tsx deleted file mode 100644 index 8875e1465f3..00000000000 --- a/web/app/[workspaceSlug]/(projects)/analytics-old/page.tsx +++ /dev/null @@ -1,107 +0,0 @@ -"use client"; - -import React, { Fragment } from "react"; -import { observer } from "mobx-react"; -import { useSearchParams } from "next/navigation"; -import { Tab } from "@headlessui/react"; -// plane package imports -import { ANALYTICS_TABS, EUserPermissionsLevel, EUserPermissions } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { Header, EHeaderVariant } from "@plane/ui"; -// components -import { CustomAnalytics, ScopeAndDemand } from "@/components/analytics"; -import { PageHead } from "@/components/core"; -import { ComicBoxButton, DetailedEmptyState } from "@/components/empty-state"; -// hooks -import { useCommandPalette, useEventTracker, useProject, useUserPermissions, useWorkspace } from "@/hooks/store"; -import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; - -const AnalyticsPage = observer(() => { - const searchParams = useSearchParams(); - const analytics_tab = searchParams.get("analytics_tab"); - // plane imports - const { t } = useTranslation(); - // store hooks - const { toggleCreateProjectModal } = useCommandPalette(); - const { setTrackElement } = useEventTracker(); - const { workspaceProjectIds, loader } = useProject(); - const { currentWorkspace } = useWorkspace(); - const { allowPermissions } = useUserPermissions(); - // helper hooks - const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/onboarding/analytics" }); - // derived values - const pageTitle = currentWorkspace?.name - ? t(`workspace_analytics.page_label`, { workspace: currentWorkspace?.name }) - : undefined; - - // permissions - const canPerformEmptyStateActions = allowPermissions( - [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - EUserPermissionsLevel.WORKSPACE - ); - - // TODO: refactor loader implementation - return ( - <> - - {workspaceProjectIds && ( - <> - {workspaceProjectIds.length > 0 || loader === "init-loader" ? ( -
- -
- - {ANALYTICS_TABS.map((tab) => ( - - {({ selected }) => ( - - )} - - ))} - -
- - - - - - - - -
-
- ) : ( - { - setTrackElement("Analytics empty state"); - toggleCreateProjectModal(true); - }} - disabled={!canPerformEmptyStateActions} - /> - } - /> - )} - - )} - - ); -}); - -export default AnalyticsPage;